3 include_once("aur.inc.php");
6 * This class defines a remote interface for fetching data from the AUR using
7 * JSON formatted elements.
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', 'maintainer'
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(
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
> 5) {
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.');
90 if (isset($http_data['search_by']) && !isset($http_data['by'])) {
91 $http_data['by'] = $http_data['search_by'];
93 if (isset($http_data['by']) && !in_array($http_data['by'], self
::$exposed_fields)) {
94 return $this->json_error('Incorrect by field specified.');
97 $this->dbh
= DB
::connect();
99 $type = str_replace('-', '_', $http_data['type']);
100 if ($type == 'info' && $this->version
>= 5) {
103 $json = call_user_func(array(&$this, $type), $http_data);
106 header("Etag: \"$etag\"");
108 * Make sure to strip a few things off the
109 * if-none-match header. Stripping whitespace may not
110 * be required, but removing the quote on the incoming
111 * header is required to make the equality test.
113 $if_none_match = isset($_SERVER['HTTP_IF_NONE_MATCH']) ?
114 trim($_SERVER['HTTP_IF_NONE_MATCH'], "\t\n\r\" ") : false;
115 if ($if_none_match && $if_none_match == $etag) {
116 header('HTTP/1.1 304 Not Modified');
120 if (isset($http_data['callback'])) {
121 $callback = $http_data['callback'];
122 if (!preg_match('/^[a-zA-Z0-9()_.]{1,128}$/D', $callback)) {
123 return $this->json_error('Invalid callback name.');
125 header('content-type: text/javascript');
126 return '/**/' . $callback . '(' . $json . ')';
128 header('content-type: application/json');
134 * Returns a JSON formatted error string.
136 * @param $msg The error string to return
138 * @return mixed A json formatted error response.
140 private function json_error($msg) {
141 header('content-type: application/json');
142 if ($this->version
< 3) {
143 return $this->json_results('error', 0, $msg, NULL);
144 } elseif ($this->version
>= 3) {
145 return $this->json_results('error', 0, array(), $msg);
150 * Returns a JSON formatted result data.
152 * @param $type The response method type.
153 * @param $count The number of results to return
154 * @param $data The result data to return
155 * @param $error An error message to include in the response
157 * @return mixed A json formatted result response.
159 private function json_results($type, $count, $data, $error) {
161 'version' => $this->version
,
163 'resultcount' => $count,
168 $json_array['error'] = $error;
171 return json_encode($json_array);
175 * Get extended package details (for info and multiinfo queries).
177 * @param $pkgid The ID of the package to retrieve details for.
178 * @param $base_id The ID of the package base to retrieve details for.
180 * @return array An array containing package details.
182 private function get_extended_fields($pkgid, $base_id) {
183 $query = "SELECT DependencyTypes.Name AS Type, " .
184 "PackageDepends.DepName AS Name, " .
185 "PackageDepends.DepCondition AS Cond " .
186 "FROM PackageDepends " .
187 "LEFT JOIN DependencyTypes " .
188 "ON DependencyTypes.ID = PackageDepends.DepTypeID " .
189 "WHERE PackageDepends.PackageID = " . $pkgid . " " .
190 "UNION SELECT RelationTypes.Name AS Type, " .
191 "PackageRelations.RelName AS Name, " .
192 "PackageRelations.RelCondition AS Cond " .
193 "FROM PackageRelations " .
194 "LEFT JOIN RelationTypes " .
195 "ON RelationTypes.ID = PackageRelations.RelTypeID " .
196 "WHERE PackageRelations.PackageID = " . $pkgid . " " .
197 "UNION SELECT 'groups' AS Type, Groups.Name, '' AS Cond " .
198 "FROM Groups INNER JOIN PackageGroups " .
199 "ON PackageGroups.PackageID = " . $pkgid . " " .
200 "AND PackageGroups.GroupID = Groups.ID " .
201 "UNION SELECT 'license' AS Type, Licenses.Name, '' AS Cond " .
202 "FROM Licenses INNER JOIN PackageLicenses " .
203 "ON PackageLicenses.PackageID = " . $pkgid . " " .
204 "AND PackageLicenses.LicenseID = Licenses.ID";
205 $result = $this->dbh
->query($query);
212 'depends' => 'Depends',
213 'makedepends' => 'MakeDepends',
214 'checkdepends' => 'CheckDepends',
215 'optdepends' => 'OptDepends',
216 'conflicts' => 'Conflicts',
217 'provides' => 'Provides',
218 'replaces' => 'Replaces',
219 'groups' => 'Groups',
220 'license' => 'License',
223 while ($row = $result->fetch(PDO
::FETCH_ASSOC
)) {
224 $type = $type_map[$row['Type']];
225 $data[$type][] = $row['Name'] . $row['Cond'];
228 if ($this->version
>= 5) {
229 $query = "SELECT Keyword FROM PackageKeywords " .
230 "WHERE PackageBaseID = " . intval($base_id) . " " .
231 "ORDER BY Keyword ASC";
232 $result = $this->dbh
->query($query);
238 $data['Keywords'] = $result->fetchAll(PDO
::FETCH_COLUMN
, 0);
245 * Retrieve package information (used in info, multiinfo, search and
248 * @param $type The request type.
249 * @param $where_condition An SQL WHERE-condition to filter packages.
251 * @return mixed Returns an array of package matches.
253 private function process_query($type, $where_condition) {
254 $max_results = config_get_int('options', 'max_rpc_results');
256 if ($this->version
== 1) {
257 $fields = implode(',', self
::$fields_v1);
258 $query = "SELECT {$fields} " .
259 "FROM Packages LEFT JOIN PackageBases " .
260 "ON PackageBases.ID = Packages.PackageBaseID " .
262 "ON PackageBases.MaintainerUID = Users.ID " .
263 "LEFT JOIN PackageLicenses " .
264 "ON PackageLicenses.PackageID = Packages.ID " .
265 "LEFT JOIN Licenses " .
266 "ON Licenses.ID = PackageLicenses.LicenseID " .
267 "WHERE ${where_condition} " .
268 "AND PackageBases.PackagerUID IS NOT NULL " .
269 "LIMIT $max_results";
270 } elseif ($this->version
>= 2) {
271 if ($this->version
== 2 ||
$this->version
== 3) {
272 $fields = implode(',', self
::$fields_v2);
273 } else if ($this->version
== 4 ||
$this->version
== 5) {
274 $fields = implode(',', self
::$fields_v4);
276 $query = "SELECT {$fields} " .
277 "FROM Packages LEFT JOIN PackageBases " .
278 "ON PackageBases.ID = Packages.PackageBaseID " .
280 "ON PackageBases.MaintainerUID = Users.ID " .
281 "WHERE ${where_condition} " .
282 "AND PackageBases.PackagerUID IS NOT NULL " .
283 "LIMIT $max_results";
285 $result = $this->dbh
->query($query);
289 $search_data = array();
290 while ($row = $result->fetch(PDO
::FETCH_ASSOC
)) {
292 $row['URLPath'] = sprintf(config_get('options', 'snapshot_uri'), urlencode($row['PackageBase']));
293 if ($this->version
< 4) {
294 $row['CategoryID'] = 1;
298 * Unfortunately, mysql_fetch_assoc() returns
299 * all fields as strings. We need to coerce
300 * numeric values into integers to provide
301 * proper data types in the JSON response.
303 foreach (self
::$numeric_fields as $field) {
304 if (isset($row[$field])) {
305 $row[$field] = intval($row[$field]);
309 foreach (self
::$decimal_fields as $field) {
310 if (isset($row[$field])) {
311 $row[$field] = floatval($row[$field]);
315 if ($this->version
>= 2 && ($type == 'info' ||
$type == 'multiinfo')) {
316 $row = array_merge($row, $this->get_extended_fields($row['ID'], $row['PackageBaseID']));
319 if ($this->version
< 3) {
320 if ($type == 'info') {
324 array_push($search_data, $row);
326 } elseif ($this->version
>= 3) {
327 array_push($search_data, $row);
331 if ($resultcount === $max_results) {
332 return $this->json_error('Too many package results.');
335 return $this->json_results($type, $resultcount, $search_data, NULL);
337 return $this->json_results($type, 0, array(), NULL);
342 * Parse the args to the multiinfo function. We may have a string or an
343 * array, so do the appropriate thing. Within the elements, both * package
344 * IDs and package names are valid; sort them into the relevant arrays and
345 * escape/quote the names.
347 * @param array $args Query parameters.
349 * @return mixed An array containing 'ids' and 'names'.
351 private function parse_multiinfo_args($args) {
352 if (!is_array($args)) {
353 $args = array($args);
357 $name_args = array();
358 foreach ($args as $arg) {
362 if ($this->version
< 5 && is_numeric($arg)) {
363 $id_args[] = intval($arg);
365 $name_args[] = $this->dbh
->quote($arg);
369 return array('ids' => $id_args, 'names' => $name_args);
373 * Performs a fulltext mysql search of the package database.
375 * @param array $http_data Query parameters.
377 * @return mixed Returns an array of package matches.
379 private function search($http_data) {
380 $keyword_string = $http_data['arg'];
382 if (isset($http_data['by'])) {
383 $search_by = $http_data['by'];
385 $search_by = 'name-desc';
388 if ($search_by === 'name' ||
$search_by === 'name-desc') {
389 if (strlen($keyword_string) < 2) {
390 return $this->json_error('Query arg too small.');
392 $keyword_string = $this->dbh
->quote("%" . addcslashes($keyword_string, '%_') . "%");
394 if ($search_by === 'name') {
395 $where_condition = "(Packages.Name LIKE $keyword_string)";
396 } else if ($search_by === 'name-desc') {
397 $where_condition = "(Packages.Name LIKE $keyword_string OR ";
398 $where_condition .= "Description LIKE $keyword_string)";
400 } else if ($search_by === 'maintainer') {
401 if (empty($keyword_string)) {
402 $where_condition = "Users.ID is NULL";
404 $keyword_string = $this->dbh
->quote($keyword_string);
405 $where_condition = "Users.Username = $keyword_string ";
409 return $this->process_query('search', $where_condition);
413 * Returns the info on a specific package.
415 * @param array $http_data Query parameters.
417 * @return mixed Returns an array of value data containing the package data
419 private function info($http_data) {
420 $pqdata = $http_data['arg'];
421 if ($this->version
< 5 && is_numeric($pqdata)) {
422 $where_condition = "Packages.ID = $pqdata";
424 $where_condition = "Packages.Name = " . $this->dbh
->quote($pqdata);
427 return $this->process_query('info', $where_condition);
431 * Returns the info on multiple packages.
433 * @param array $http_data Query parameters.
435 * @return mixed Returns an array of results containing the package data
437 private function multiinfo($http_data) {
438 $pqdata = $http_data['arg'];
439 $args = $this->parse_multiinfo_args($pqdata);
441 $names = $args['names'];
443 if (!$ids && !$names) {
444 return $this->json_error('Invalid query arguments.');
447 $where_condition = "";
449 $ids_value = implode(',', $args['ids']);
450 $where_condition .= "Packages.ID IN ($ids_value) ";
452 if ($ids && $names) {
453 $where_condition .= "OR ";
457 * Individual names were quoted in
458 * parse_multiinfo_args().
460 $names_value = implode(',', $args['names']);
461 $where_condition .= "Packages.Name IN ($names_value) ";
464 return $this->process_query('multiinfo', $where_condition);
468 * Returns all the packages for a specific maintainer.
470 * @param array $http_data Query parameters.
472 * @return mixed Returns an array of value data containing the package data
474 private function msearch($http_data) {
475 $http_data['by'] = 'maintainer';
476 return $this->search($http_data);
480 * Get all package names that start with $search.
482 * @param array $http_data Query parameters.
484 * @return string The JSON formatted response data.
486 private function suggest($http_data) {
487 $search = $http_data['arg'];
488 $query = "SELECT Packages.Name FROM Packages ";
489 $query.= "LEFT JOIN PackageBases ";
490 $query.= "ON PackageBases.ID = Packages.PackageBaseID ";
491 $query.= "WHERE Packages.Name LIKE ";
492 $query.= $this->dbh
->quote(addcslashes($search, '%_') . '%');
493 $query.= " AND PackageBases.PackagerUID IS NOT NULL ";
494 $query.= "ORDER BY Name ASC LIMIT 20";
496 $result = $this->dbh
->query($query);
497 $result_array = array();
500 $result_array = $result->fetchAll(PDO
::FETCH_COLUMN
, 0);
503 return json_encode($result_array);
507 * Get all package base names that start with $search.
509 * @param array $http_data Query parameters.
511 * @return string The JSON formatted response data.
513 private function suggest_pkgbase($http_data) {
514 $search = $http_data['arg'];
515 $query = "SELECT Name FROM PackageBases WHERE Name LIKE ";
516 $query.= $this->dbh
->quote(addcslashes($search, '%_') . '%');
517 $query.= " AND PackageBases.PackagerUID IS NOT NULL ";
518 $query.= "ORDER BY Name ASC LIMIT 20";
520 $result = $this->dbh
->query($query);
521 $result_array = array();
524 $result_array = $result->fetchAll(PDO
::FETCH_COLUMN
, 0);
527 return json_encode($result_array);
531 * Get the HTML markup of the comment form.
533 * @param array $http_data Query parameters.
535 * @return string The JSON formatted response data.
537 private function get_comment_form($http_data) {
538 if (!isset($http_data['base_id']) ||
!isset($http_data['pkgbase_name'])) {
541 'error' => __('Package base ID or package base name missing.')
543 return json_encode($output);
546 $comment_id = intval($http_data['arg']);
547 $base_id = intval($http_data['base_id']);
548 $pkgbase_name = $http_data['pkgbase_name'];
550 list($user_id, $comment) = comment_by_id($comment_id);
552 if (!has_credential(CRED_COMMENT_EDIT
, array($user_id))) {
555 'error' => __('You are not allowed to edit this comment.')
557 return json_encode($output);
558 } elseif (is_null($comment)) {
561 'error' => __('Comment does not exist.')
563 return json_encode($output);
567 include('pkg_comment_form.php');
568 $html = ob_get_clean();
574 return json_encode($output);