aurjson.class.php: Add missing PHPDoc
[aur.git] / web / lib / aurjson.class.php
blob7d94daba317690ac7657a276a60ebbb38b16e3fb
1 <?php
3 include_once("aur.inc.php");
5 /*
6 * This class defines a remote interface for fetching data from the AUR using
7 * JSON formatted elements.
9 * @package rpc
10 * @subpackage classes
12 class AurJSON {
13 private $dbh = false;
14 private $version = 1;
15 private static $exposed_methods = array(
16 'search', 'info', 'multiinfo', 'msearch', 'suggest',
17 'suggest-pkgbase', 'get-comment-form'
19 private static $exposed_fields = array(
20 'name', 'name-desc'
22 private static $fields_v1 = array(
23 'Packages.ID', 'Packages.Name',
24 'PackageBases.ID AS PackageBaseID',
25 'PackageBases.Name AS PackageBase', 'Version',
26 'Description', 'URL', 'NumVotes', 'OutOfDateTS AS OutOfDate',
27 'Users.UserName AS Maintainer',
28 'SubmittedTS AS FirstSubmitted', 'ModifiedTS AS LastModified',
29 'Licenses.Name AS License'
31 private static $fields_v2 = array(
32 'Packages.ID', 'Packages.Name',
33 'PackageBases.ID AS PackageBaseID',
34 'PackageBases.Name AS PackageBase', 'Version',
35 'Description', 'URL', 'NumVotes', 'OutOfDateTS AS OutOfDate',
36 'Users.UserName AS Maintainer',
37 'SubmittedTS AS FirstSubmitted', 'ModifiedTS AS LastModified'
39 private static $fields_v4 = array(
40 'Packages.ID', 'Packages.Name',
41 'PackageBases.ID AS PackageBaseID',
42 'PackageBases.Name AS PackageBase', 'Version',
43 'Description', 'URL', 'NumVotes', 'Popularity',
44 'OutOfDateTS AS OutOfDate', 'Users.UserName AS Maintainer',
45 'SubmittedTS AS FirstSubmitted', 'ModifiedTS AS LastModified'
47 private static $numeric_fields = array(
48 'ID', 'PackageBaseID', 'NumVotes', 'OutOfDate',
49 'FirstSubmitted', 'LastModified'
51 private static $decimal_fields = array(
52 'Popularity'
56 * Handles post data, and routes the request.
58 * @param string $post_data The post data to parse and handle.
60 * @return string The JSON formatted response data.
62 public function handle($http_data) {
64 * Unset global aur.inc.php Pragma header. We want to allow
65 * caching of data in proxies, but require validation of data
66 * (if-none-match) if possible.
68 header_remove('Pragma');
70 * Overwrite cache-control header set in aur.inc.php to allow
71 * caching, but require validation.
73 header('Cache-Control: public, must-revalidate, max-age=0');
74 header('Content-Type: application/json, charset=utf-8');
76 if (isset($http_data['v'])) {
77 $this->version = intval($http_data['v']);
79 if ($this->version < 1 || $this->version > 4) {
80 return $this->json_error('Invalid version specified.');
83 if (!isset($http_data['type']) || !isset($http_data['arg'])) {
84 return $this->json_error('No request type/data specified.');
86 if (!in_array($http_data['type'], self::$exposed_methods)) {
87 return $this->json_error('Incorrect request type specified.');
89 if (isset($http_data['search_by']) && !in_array($http_data['search_by'], self::$exposed_fields)) {
90 return $this->json_error('Incorrect search_by field specified.');
93 $this->dbh = DB::connect();
95 $type = str_replace('-', '_', $http_data['type']);
96 $json = call_user_func(array(&$this, $type), $http_data);
98 $etag = md5($json);
99 header("Etag: \"$etag\"");
101 * Make sure to strip a few things off the
102 * if-none-match header. Stripping whitespace may not
103 * be required, but removing the quote on the incoming
104 * header is required to make the equality test.
106 $if_none_match = isset($_SERVER['HTTP_IF_NONE_MATCH']) ?
107 trim($_SERVER['HTTP_IF_NONE_MATCH'], "\t\n\r\" ") : false;
108 if ($if_none_match && $if_none_match == $etag) {
109 header('HTTP/1.1 304 Not Modified');
110 return;
113 if (isset($http_data['callback'])) {
114 header('content-type: text/javascript');
115 return $http_data['callback'] . "({$json})";
116 } else {
117 header('content-type: application/json');
118 return $json;
123 * Returns a JSON formatted error string.
125 * @param $msg The error string to return
127 * @return mixed A json formatted error response.
129 private function json_error($msg) {
130 header('content-type: application/json');
131 if ($this->version < 3) {
132 return $this->json_results('error', 0, $msg, NULL);
133 } elseif ($this->version >= 3) {
134 return $this->json_results('error', 0, array(), $msg);
139 * Returns a JSON formatted result data.
141 * @param $type The response method type.
142 * @param $count The number of results to return
143 * @param $data The result data to return
144 * @param $error An error message to include in the response
146 * @return mixed A json formatted result response.
148 private function json_results($type, $count, $data, $error) {
149 $json_array = array(
150 'version' => $this->version,
151 'type' => $type,
152 'resultcount' => $count,
153 'results' => $data
156 if ($error) {
157 $json_array['error'] = $error;
160 return json_encode($json_array);
164 * Get extended package details (for info and multiinfo queries).
166 * @param $pkgid The ID of the package to retrieve details for.
168 * @return array An array containing package details.
170 private function get_extended_fields($pkgid) {
171 $query = "SELECT DependencyTypes.Name AS Type, " .
172 "PackageDepends.DepName AS Name, " .
173 "PackageDepends.DepCondition AS Cond " .
174 "FROM PackageDepends " .
175 "LEFT JOIN DependencyTypes " .
176 "ON DependencyTypes.ID = PackageDepends.DepTypeID " .
177 "WHERE PackageDepends.PackageID = " . $pkgid . " " .
178 "UNION SELECT RelationTypes.Name AS Type, " .
179 "PackageRelations.RelName AS Name, " .
180 "PackageRelations.RelCondition AS Cond " .
181 "FROM PackageRelations " .
182 "LEFT JOIN RelationTypes " .
183 "ON RelationTypes.ID = PackageRelations.RelTypeID " .
184 "WHERE PackageRelations.PackageID = " . $pkgid . " " .
185 "UNION SELECT 'groups' AS Type, Groups.Name, '' AS Cond " .
186 "FROM Groups INNER JOIN PackageGroups " .
187 "ON PackageGroups.PackageID = " . $pkgid . " " .
188 "AND PackageGroups.GroupID = Groups.ID " .
189 "UNION SELECT 'license' AS Type, Licenses.Name, '' AS Cond " .
190 "FROM Licenses INNER JOIN PackageLicenses " .
191 "ON PackageLicenses.PackageID = " . $pkgid . " " .
192 "AND PackageLicenses.LicenseID = Licenses.ID";
193 $result = $this->dbh->query($query);
195 if (!$result) {
196 return null;
199 $type_map = array(
200 'depends' => 'Depends',
201 'makedepends' => 'MakeDepends',
202 'checkdepends' => 'CheckDepends',
203 'optdepends' => 'OptDepends',
204 'conflicts' => 'Conflicts',
205 'provides' => 'Provides',
206 'replaces' => 'Replaces',
207 'groups' => 'Groups',
208 'license' => 'License',
210 $data = array();
211 while ($row = $result->fetch(PDO::FETCH_ASSOC)) {
212 $type = $type_map[$row['Type']];
213 $data[$type][] = $row['Name'] . $row['Cond'];
216 return $data;
220 * Retrieve package information (used in info, multiinfo, search and
221 * msearch requests).
223 * @param $type The request type.
224 * @param $where_condition An SQL WHERE-condition to filter packages.
226 * @return mixed Returns an array of package matches.
228 private function process_query($type, $where_condition) {
229 $max_results = config_get_int('options', 'max_rpc_results');
231 if ($this->version == 1) {
232 $fields = implode(',', self::$fields_v1);
233 $query = "SELECT {$fields} " .
234 "FROM Packages LEFT JOIN PackageBases " .
235 "ON PackageBases.ID = Packages.PackageBaseID " .
236 "LEFT JOIN Users " .
237 "ON PackageBases.MaintainerUID = Users.ID " .
238 "LEFT JOIN PackageLicenses " .
239 "ON PackageLicenses.PackageID = Packages.ID " .
240 "LEFT JOIN Licenses " .
241 "ON Licenses.ID = PackageLicenses.LicenseID " .
242 "WHERE ${where_condition} " .
243 "AND PackageBases.PackagerUID IS NOT NULL " .
244 "GROUP BY Packages.ID " .
245 "LIMIT $max_results";
246 } elseif ($this->version >= 2) {
247 if ($this->version == 2 || $this->version == 3) {
248 $fields = implode(',', self::$fields_v2);
249 } else if ($this->version == 4) {
250 $fields = implode(',', self::$fields_v4);
252 $query = "SELECT {$fields} " .
253 "FROM Packages LEFT JOIN PackageBases " .
254 "ON PackageBases.ID = Packages.PackageBaseID " .
255 "LEFT JOIN Users " .
256 "ON PackageBases.MaintainerUID = Users.ID " .
257 "WHERE ${where_condition} " .
258 "AND PackageBases.PackagerUID IS NOT NULL " .
259 "LIMIT $max_results";
261 $result = $this->dbh->query($query);
263 if ($result) {
264 $resultcount = 0;
265 $search_data = array();
266 while ($row = $result->fetch(PDO::FETCH_ASSOC)) {
267 $resultcount++;
268 $row['URLPath'] = sprintf(config_get('options', 'snapshot_uri'), urlencode($row['PackageBase']));
269 if ($this->version < 4) {
270 $row['CategoryID'] = 1;
274 * Unfortunately, mysql_fetch_assoc() returns
275 * all fields as strings. We need to coerce
276 * numeric values into integers to provide
277 * proper data types in the JSON response.
279 foreach (self::$numeric_fields as $field) {
280 $row[$field] = intval($row[$field]);
283 foreach (self::$decimal_fields as $field) {
284 $row[$field] = floatval($row[$field]);
287 if ($this->version >= 2 && ($type == 'info' || $type == 'multiinfo')) {
288 $row = array_merge($row, $this->get_extended_fields($row['ID']));
291 if ($this->version < 3) {
292 if ($type == 'info') {
293 $search_data = $row;
294 break;
295 } else {
296 array_push($search_data, $row);
298 } elseif ($this->version >= 3) {
299 array_push($search_data, $row);
303 if ($resultcount === $max_results) {
304 return $this->json_error('Too many package results.');
307 return $this->json_results($type, $resultcount, $search_data, NULL);
308 } else {
309 return $this->json_results($type, 0, array(), NULL);
314 * Parse the args to the multiinfo function. We may have a string or an
315 * array, so do the appropriate thing. Within the elements, both * package
316 * IDs and package names are valid; sort them into the relevant arrays and
317 * escape/quote the names.
319 * @param array $http_data Query parameters.
321 * @return mixed An array containing 'ids' and 'names'.
323 private function parse_multiinfo_args($http_data) {
324 $args = $http_data['arg'];
325 if (!is_array($args)) {
326 $args = array($args);
329 $id_args = array();
330 $name_args = array();
331 foreach ($args as $arg) {
332 if (!$arg) {
333 continue;
335 if (is_numeric($arg)) {
336 $id_args[] = intval($arg);
337 } else {
338 $name_args[] = $this->dbh->quote($arg);
342 return array('ids' => $id_args, 'names' => $name_args);
346 * Performs a fulltext mysql search of the package database.
348 * @param array $http_data Query parameters.
350 * @return mixed Returns an array of package matches.
352 private function search($http_data) {
353 $keyword_string = $http_data['arg'];
354 if (isset($http_data['search_by'])) {
355 $search_by = $http_data['search_by'];
356 } else {
357 $search_by = 'name-desc';
360 if (strlen($keyword_string) < 2) {
361 return $this->json_error('Query arg too small');
364 $keyword_string = $this->dbh->quote("%" . addcslashes($keyword_string, '%_') . "%");
366 if ($search_by === 'name') {
367 $where_condition = "(Packages.Name LIKE $keyword_string)";
368 } else if ($search_by === 'name-desc') {
369 $where_condition = "(Packages.Name LIKE $keyword_string OR ";
370 $where_condition .= "Description LIKE $keyword_string)";
373 return $this->process_query('search', $where_condition);
377 * Returns the info on a specific package.
379 * @param array $http_data Query parameters.
381 * @return mixed Returns an array of value data containing the package data
383 private function info($http_data) {
384 $pqdata = $http_data['arg'];
385 if (is_numeric($pqdata)) {
386 $where_condition = "Packages.ID = $pqdata";
387 } else {
388 $where_condition = "Packages.Name = " . $this->dbh->quote($pqdata);
391 return $this->process_query('info', $where_condition);
395 * Returns the info on multiple packages.
397 * @param array $http_data Query parameters.
399 * @return mixed Returns an array of results containing the package data
401 private function multiinfo($http_data) {
402 $pqdata = $http_data['arg'];
403 $args = $this->parse_multiinfo_args($pqdata);
404 $ids = $args['ids'];
405 $names = $args['names'];
407 if (!$ids && !$names) {
408 return $this->json_error('Invalid query arguments');
411 $where_condition = "";
412 if ($ids) {
413 $ids_value = implode(',', $args['ids']);
414 $where_condition .= "Packages.ID IN ($ids_value) ";
416 if ($ids && $names) {
417 $where_condition .= "OR ";
419 if ($names) {
421 * Individual names were quoted in
422 * parse_multiinfo_args().
424 $names_value = implode(',', $args['names']);
425 $where_condition .= "Packages.Name IN ($names_value) ";
428 return $this->process_query('multiinfo', $where_condition);
432 * Returns all the packages for a specific maintainer.
434 * @param array $http_data Query parameters.
436 * @return mixed Returns an array of value data containing the package data
438 private function msearch($http_data) {
439 $maintainer = $http_data['arg'];
440 $maintainer = $this->dbh->quote($maintainer);
442 $where_condition = "Users.Username = $maintainer ";
444 return $this->process_query('msearch', $where_condition);
448 * Get all package names that start with $search.
450 * @param array $http_data Query parameters.
452 * @return string The JSON formatted response data.
454 private function suggest($http_data) {
455 $search = $http_data['arg'];
456 $query = "SELECT Packages.Name FROM Packages ";
457 $query.= "LEFT JOIN PackageBases ";
458 $query.= "ON PackageBases.ID = Packages.PackageBaseID ";
459 $query.= "WHERE Packages.Name LIKE ";
460 $query.= $this->dbh->quote(addcslashes($search, '%_') . '%');
461 $query.= " AND PackageBases.PackagerUID IS NOT NULL ";
462 $query.= "ORDER BY Name ASC LIMIT 20";
464 $result = $this->dbh->query($query);
465 $result_array = array();
467 if ($result) {
468 $result_array = $result->fetchAll(PDO::FETCH_COLUMN, 0);
471 return json_encode($result_array);
475 * Get all package base names that start with $search.
477 * @param array $http_data Query parameters.
479 * @return string The JSON formatted response data.
481 private function suggest_pkgbase($http_data) {
482 $search = $http_data['arg'];
483 $query = "SELECT Name FROM PackageBases WHERE Name LIKE ";
484 $query.= $this->dbh->quote(addcslashes($search, '%_') . '%');
485 $query.= " AND PackageBases.PackagerUID IS NOT NULL ";
486 $query.= "ORDER BY Name ASC LIMIT 20";
488 $result = $this->dbh->query($query);
489 $result_array = array();
491 if ($result) {
492 $result_array = $result->fetchAll(PDO::FETCH_COLUMN, 0);
495 return json_encode($result_array);
499 * Get the HTML markup of the comment form.
501 * @param array $http_data Query parameters.
503 * @return string The JSON formatted response data.
505 private function get_comment_form($http_data) {
506 if (!isset($http_data['base_id']) || !isset($http_data['pkgbase_name'])) {
507 $output = array(
508 'success' => 0,
509 'error' => __('Package base ID or package base name missing.')
511 return json_encode($output);
514 $comment_id = intval($http_data['arg']);
515 $base_id = intval($http_data['base_id']);
516 $pkgbase_name = $http_data['pkgbase_name'];
518 list($user_id, $comment) = comment_by_id($comment_id);
520 if (!has_credential(CRED_COMMENT_EDIT, array($user_id))) {
521 $output = array(
522 'success' => 0,
523 'error' => __('You do not have the right to edit this comment.')
525 return json_encode($output);
526 } elseif (is_null($comment)) {
527 $output = array(
528 'success' => 0,
529 'error' => __('Comment does not exist.')
531 return json_encode($output);
534 ob_start();
535 include('pkg_comment_form.php');
536 $html = ob_get_clean();
537 $output = array(
538 'success' => 1,
539 'form' => $html
542 return json_encode($output);