aurjson.class.php: Exclude hidden package bases
[aur.git] / web / lib / aurjson.class.php
blob5fb89a9feadb90713a47474c6016baf5d644f298
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'
19 private static $fields_v1 = array(
20 'Packages.ID', 'Packages.Name',
21 'PackageBases.ID AS PackageBaseID',
22 'PackageBases.Name AS PackageBase', 'Version', 'CategoryID',
23 'Description', 'URL', 'NumVotes', 'OutOfDateTS AS OutOfDate',
24 'Users.UserName AS Maintainer',
25 'SubmittedTS AS FirstSubmitted', 'ModifiedTS AS LastModified',
26 'Licenses.Name AS License'
28 private static $fields_v2 = array(
29 'Packages.ID', 'Packages.Name',
30 'PackageBases.ID AS PackageBaseID',
31 'PackageBases.Name AS PackageBase', 'Version', 'CategoryID',
32 'Description', 'URL', 'NumVotes', 'OutOfDateTS AS OutOfDate',
33 'Users.UserName AS Maintainer',
34 'SubmittedTS AS FirstSubmitted', 'ModifiedTS AS LastModified'
36 private static $numeric_fields = array(
37 'ID', 'PackageBaseID', 'CategoryID', 'NumVotes', 'OutOfDate',
38 'FirstSubmitted', 'LastModified'
42 * Handles post data, and routes the request.
44 * @param string $post_data The post data to parse and handle.
46 * @return string The JSON formatted response data.
48 public function handle($http_data) {
50 * Unset global aur.inc.php Pragma header. We want to allow
51 * caching of data in proxies, but require validation of data
52 * (if-none-match) if possible.
54 header_remove('Pragma');
56 * Overwrite cache-control header set in aur.inc.php to allow
57 * caching, but require validation.
59 header('Cache-Control: public, must-revalidate, max-age=0');
60 header('Content-Type: application/json, charset=utf-8');
62 if (isset($http_data['v'])) {
63 $this->version = intval($http_data['v']);
65 if ($this->version < 1 || $this->version > 3) {
66 return $this->json_error('Invalid version specified.');
69 if (!isset($http_data['type']) || !isset($http_data['arg'])) {
70 return $this->json_error('No request type/data specified.');
72 if (!in_array($http_data['type'], self::$exposed_methods)) {
73 return $this->json_error('Incorrect request type specified.');
76 $this->dbh = DB::connect();
78 $type = str_replace('-', '_', $http_data['type']);
79 $json = call_user_func(array(&$this, $type), $http_data['arg']);
81 $etag = md5($json);
82 header("Etag: \"$etag\"");
84 * Make sure to strip a few things off the
85 * if-none-match header. Stripping whitespace may not
86 * be required, but removing the quote on the incoming
87 * header is required to make the equality test.
89 $if_none_match = isset($_SERVER['HTTP_IF_NONE_MATCH']) ?
90 trim($_SERVER['HTTP_IF_NONE_MATCH'], "\t\n\r\" ") : false;
91 if ($if_none_match && $if_none_match == $etag) {
92 header('HTTP/1.1 304 Not Modified');
93 return;
96 if (isset($http_data['callback'])) {
97 header('content-type: text/javascript');
98 return $http_data['callback'] . "({$json})";
99 } else {
100 header('content-type: application/json');
101 return $json;
106 * Returns a JSON formatted error string.
108 * @param $msg The error string to return
110 * @return mixed A json formatted error response.
112 private function json_error($msg) {
113 header('content-type: application/json');
114 if ($this->version < 3) {
115 return $this->json_results('error', 0, $msg, NULL);
116 } elseif ($this->version >= 3) {
117 return $this->json_results('error', 0, array(), $msg);
122 * Returns a JSON formatted result data.
124 * @param $type The response method type.
125 * @param $data The result data to return
126 * @param $error An error message to include in the response
128 * @return mixed A json formatted result response.
130 private function json_results($type, $count, $data, $error) {
131 $json_array = array(
132 'version' => $this->version,
133 'type' => $type,
134 'resultcount' => $count,
135 'results' => $data
138 if ($error) {
139 $json_array['error'] = $error;
142 return json_encode($json_array);
145 private function get_extended_fields($pkgid) {
146 $query = "SELECT DependencyTypes.Name AS Type, " .
147 "PackageDepends.DepName AS Name, " .
148 "PackageDepends.DepCondition AS Cond " .
149 "FROM PackageDepends " .
150 "LEFT JOIN DependencyTypes " .
151 "ON DependencyTypes.ID = PackageDepends.DepTypeID " .
152 "WHERE PackageDepends.PackageID = " . $pkgid . " " .
153 "UNION SELECT RelationTypes.Name AS Type, " .
154 "PackageRelations.RelName AS Name, " .
155 "PackageRelations.RelCondition AS Cond " .
156 "FROM PackageRelations " .
157 "LEFT JOIN RelationTypes " .
158 "ON RelationTypes.ID = PackageRelations.RelTypeID " .
159 "WHERE PackageRelations.PackageID = " . $pkgid . " " .
160 "UNION SELECT 'groups' AS Type, Groups.Name, '' AS Cond " .
161 "FROM Groups INNER JOIN PackageGroups " .
162 "ON PackageGroups.PackageID = " . $pkgid . " " .
163 "AND PackageGroups.GroupID = Groups.ID " .
164 "UNION SELECT 'license' AS Type, Licenses.Name, '' AS Cond " .
165 "FROM Licenses INNER JOIN PackageLicenses " .
166 "ON PackageLicenses.PackageID = " . $pkgid . " " .
167 "AND PackageLicenses.LicenseID = Licenses.ID";
168 $result = $this->dbh->query($query);
170 if (!$result) {
171 return null;
174 $type_map = array(
175 'depends' => 'Depends',
176 'makedepends' => 'MakeDepends',
177 'checkdepends' => 'CheckDepends',
178 'optdepends' => 'OptDepends',
179 'conflicts' => 'Conflicts',
180 'provides' => 'Provides',
181 'replaces' => 'Replaces',
182 'groups' => 'Groups',
183 'license' => 'License',
185 $data = array();
186 while ($row = $result->fetch(PDO::FETCH_ASSOC)) {
187 $type = $type_map[$row['Type']];
188 $data[$type][] = $row['Name'] . $row['Cond'];
191 return $data;
194 private function process_query($type, $where_condition) {
195 $max_results = config_get_int('options', 'max_rpc_results');
196 $package_url = config_get('options', 'package_url');
198 if ($this->version == 1) {
199 $fields = implode(',', self::$fields_v1);
200 $query = "SELECT {$fields} " .
201 "FROM Packages LEFT JOIN PackageBases " .
202 "ON PackageBases.ID = Packages.PackageBaseID " .
203 "LEFT JOIN Users " .
204 "ON PackageBases.MaintainerUID = Users.ID " .
205 "LEFT JOIN PackageLicenses " .
206 "ON PackageLicenses.PackageID = Packages.ID " .
207 "LEFT JOIN Licenses " .
208 "ON Licenses.ID = PackageLicenses.LicenseID " .
209 "WHERE ${where_condition} " .
210 "AND PackageBases.PackagerUID IS NOT NULL " .
211 "GROUP BY Packages.ID " .
212 "LIMIT $max_results";
213 } elseif ($this->version >= 2) {
214 $fields = implode(',', self::$fields_v2);
215 $query = "SELECT {$fields} " .
216 "FROM Packages LEFT JOIN PackageBases " .
217 "ON PackageBases.ID = Packages.PackageBaseID " .
218 "LEFT JOIN Users " .
219 "ON PackageBases.MaintainerUID = Users.ID " .
220 "WHERE ${where_condition} " .
221 "AND PackageBases.PackagerUID IS NOT NULL " .
222 "LIMIT $max_results";
224 $result = $this->dbh->query($query);
226 if ($result) {
227 $resultcount = 0;
228 $search_data = array();
229 while ($row = $result->fetch(PDO::FETCH_ASSOC)) {
230 $resultcount++;
231 $pkgbase_name = $row['PackageBase'];
234 * Unfortunately, mysql_fetch_assoc() returns
235 * all fields as strings. We need to coerce
236 * numeric values into integers to provide
237 * proper data types in the JSON response.
239 foreach (self::$numeric_fields as $field) {
240 $row[$field] = intval($row[$field]);
243 if ($this->version >= 2 && ($type == 'info' || $type == 'multiinfo')) {
244 $row = array_merge($row, $this->get_extended_fields($row['ID']));
247 if ($this->version < 3) {
248 if ($type == 'info') {
249 $search_data = $row;
250 break;
251 } else {
252 array_push($search_data, $row);
254 } elseif ($this->version >= 3) {
255 array_push($search_data, $row);
259 if ($resultcount === $max_results) {
260 return $this->json_error('Too many package results.');
263 return $this->json_results($type, $resultcount, $search_data, NULL);
264 } else {
265 return $this->json_results($type, 0, array(), NULL);
270 * Parse the args to the multiinfo function. We may have a string or an
271 * array, so do the appropriate thing. Within the elements, both * package
272 * IDs and package names are valid; sort them into the relevant arrays and
273 * escape/quote the names.
275 * @param $args the arg string or array to parse.
277 * @return mixed An array containing 'ids' and 'names'.
279 private function parse_multiinfo_args($args) {
280 if (!is_array($args)) {
281 $args = array($args);
284 $id_args = array();
285 $name_args = array();
286 foreach ($args as $arg) {
287 if (!$arg) {
288 continue;
290 if (is_numeric($arg)) {
291 $id_args[] = intval($arg);
292 } else {
293 $name_args[] = $this->dbh->quote($arg);
297 return array('ids' => $id_args, 'names' => $name_args);
301 * Performs a fulltext mysql search of the package database.
303 * @param $keyword_string A string of keywords to search with.
305 * @return mixed Returns an array of package matches.
307 private function search($keyword_string) {
308 if (strlen($keyword_string) < 2) {
309 return $this->json_error('Query arg too small');
312 $keyword_string = $this->dbh->quote("%" . addcslashes($keyword_string, '%_') . "%");
314 $where_condition = "(Packages.Name LIKE $keyword_string OR ";
315 $where_condition .= "Description LIKE $keyword_string)";
317 return $this->process_query('search', $where_condition);
321 * Returns the info on a specific package.
323 * @param $pqdata The ID or name of the package. Package Query Data.
325 * @return mixed Returns an array of value data containing the package data
327 private function info($pqdata) {
328 if (is_numeric($pqdata)) {
329 $where_condition = "Packages.ID = $pqdata";
330 } else {
331 $where_condition = "Packages.Name = " . $this->dbh->quote($pqdata);
334 return $this->process_query('info', $where_condition);
338 * Returns the info on multiple packages.
340 * @param $pqdata A comma-separated list of IDs or names of the packages.
342 * @return mixed Returns an array of results containing the package data
344 private function multiinfo($pqdata) {
345 $args = $this->parse_multiinfo_args($pqdata);
346 $ids = $args['ids'];
347 $names = $args['names'];
349 if (!$ids && !$names) {
350 return $this->json_error('Invalid query arguments');
353 $where_condition = "";
354 if ($ids) {
355 $ids_value = implode(',', $args['ids']);
356 $where_condition .= "Packages.ID IN ($ids_value) ";
358 if ($ids && $names) {
359 $where_condition .= "OR ";
361 if ($names) {
363 * Individual names were quoted in
364 * parse_multiinfo_args().
366 $names_value = implode(',', $args['names']);
367 $where_condition .= "Packages.Name IN ($names_value) ";
370 return $this->process_query('multiinfo', $where_condition);
374 * Returns all the packages for a specific maintainer.
376 * @param $maintainer The name of the maintainer.
378 * @return mixed Returns an array of value data containing the package data
380 private function msearch($maintainer) {
381 $maintainer = $this->dbh->quote($maintainer);
383 $where_condition = "Users.Username = $maintainer ";
385 return $this->process_query('msearch', $where_condition);
389 * Get all package names that start with $search.
391 * @param string $search Search string.
393 * @return string The JSON formatted response data.
395 private function suggest($search) {
396 $query = "SELECT Packages.Name FROM Packages ";
397 $query.= "LEFT JOIN PackageBases ";
398 $query.= "ON PackageBases.ID = Packages.PackageBaseID ";
399 $query.= "WHERE Packages.Name LIKE ";
400 $query.= $this->dbh->quote(addcslashes($search, '%_') . '%');
401 $query.= " AND PackageBases.PackagerUID IS NOT NULL ";
402 $query.= "ORDER BY Name ASC LIMIT 20";
404 $result = $this->dbh->query($query);
405 $result_array = array();
407 if ($result) {
408 $result_array = $result->fetchAll(PDO::FETCH_COLUMN, 0);
411 return json_encode($result_array);
415 * Get all package base names that start with $search.
417 * @param string $search Search string.
419 * @return string The JSON formatted response data.
421 private function suggest_pkgbase($search) {
422 $query = "SELECT Name FROM PackageBases WHERE Name LIKE ";
423 $query.= $this->dbh->quote(addcslashes($search, '%_') . '%');
424 $query.= " AND PackageBases.PackagerUID IS NOT NULL ";
425 $query.= "ORDER BY Name ASC LIMIT 20";
427 $result = $this->dbh->query($query);
428 $result_array = array();
430 if ($result) {
431 $result_array = $result->fetchAll(PDO::FETCH_COLUMN, 0);
434 return json_encode($result_array);