diff options
-rw-r--r-- | web/lib/aurjson.class.php | 696 |
1 files changed, 349 insertions, 347 deletions
diff --git a/web/lib/aurjson.class.php b/web/lib/aurjson.class.php index 2e5e06d..fe8044f 100644 --- a/web/lib/aurjson.class.php +++ b/web/lib/aurjson.class.php @@ -1,361 +1,363 @@ <?php -/** - * AurJSON - * - * This file contains the AurRPC remote handling class - **/ + include_once("aur.inc.php"); -/** - * This class defines a remote interface for fetching data - * from the AUR using JSON formatted elements. +/* + * This class defines a remote interface for fetching data from the AUR using + * JSON formatted elements. + * * @package rpc * @subpackage classes - **/ + */ class AurJSON { - private $dbh = false; - private static $exposed_methods = array( - 'search', 'info', 'multiinfo', 'msearch', 'suggest' - ); - private static $fields = array( - 'Packages.ID', 'Packages.Name', 'PackageBases.Name AS PackageBase', - 'Version', 'CategoryID', 'Description', 'URL', 'NumVotes', - 'OutOfDateTS AS OutOfDate', 'Users.UserName AS Maintainer', - 'SubmittedTS AS FirstSubmitted', 'ModifiedTS AS LastModified' - ); - private static $numeric_fields = array( - 'ID', 'CategoryID', 'NumVotes', 'OutOfDate', 'FirstSubmitted', - 'LastModified' - ); - - /** - * Handles post data, and routes the request. - * @param string $post_data The post data to parse and handle. - * @return string The JSON formatted response data. - **/ - public function handle($http_data) { - // unset global aur headers from aur.inc - // leave expires header to enforce validation - // header_remove('Expires'); - // unset global aur.inc pragma header. We want to allow caching of data - // in proxies, but require validation of data (if-none-match) if - // possible + private $dbh = false; + private static $exposed_methods = array( + 'search', 'info', 'multiinfo', 'msearch', 'suggest' + ); + private static $fields = array( + 'Packages.ID', 'Packages.Name', + 'PackageBases.Name AS PackageBase', 'Version', 'CategoryID', + 'Description', 'URL', 'NumVotes', 'OutOfDateTS AS OutOfDate', + 'Users.UserName AS Maintainer', + 'SubmittedTS AS FirstSubmitted', 'ModifiedTS AS LastModified' + ); + private static $numeric_fields = array( + 'ID', 'CategoryID', 'NumVotes', 'OutOfDate', 'FirstSubmitted', + 'LastModified' + ); + + /* + * Handles post data, and routes the request. + * + * @param string $post_data The post data to parse and handle. + * + * @return string The JSON formatted response data. + */ + public function handle($http_data) { + /* + * Unset global aur.inc.php Pragma header. We want to allow + * caching of data in proxies, but require validation of data + * (if-none-match) if possible. + */ header_remove('Pragma'); - // overwrite cache-control header set in aur.inc to allow caching, but - // require validation + /* + * Overwrite cache-control header set in aur.inc.php to allow + * caching, but require validation. + */ header('Cache-Control: public, must-revalidate, max-age=0'); header('Content-Type: application/json, charset=utf-8'); - // handle error states - if ( !isset($http_data['type']) || !isset($http_data['arg']) ) { - return $this->json_error('No request type/data specified.'); - } - - // do the routing - if ( in_array($http_data['type'], self::$exposed_methods) ) { - // set up db connection. - $this->dbh = DB::connect(); - - // ugh. this works. I hate you php. - $json = call_user_func(array(&$this, $http_data['type']), - $http_data['arg']); - - // calculate etag as an md5 based on the json result - // this could be optimized by calculating the etag on the - // query result object before converting to json (step into - // the above function call) and adding the 'type' to the response, - // but having all this code here is cleaner and 'good enough' - $etag = md5($json); - header("Etag: \"$etag\""); - // make sure to strip a few things off the if-none-match - // header. stripping whitespace may not be required, but - // removing the quote on the incoming header is required - // to make the equality test - $if_none_match = isset($_SERVER['HTTP_IF_NONE_MATCH']) ? - trim($_SERVER['HTTP_IF_NONE_MATCH'], "\t\n\r\" ") : false; - if ($if_none_match && $if_none_match == $etag) { - header('HTTP/1.1 304 Not Modified'); - return; + if (!isset($http_data['type']) || !isset($http_data['arg'])) { + return $this->json_error('No request type/data specified.'); + } + if (!in_array($http_data['type'], self::$exposed_methods)) { + return $this->json_error('Incorrect request type specified.'); + } + + $this->dbh = DB::connect(); + + $json = call_user_func(array(&$this, $http_data['type']), $http_data['arg']); + + $etag = md5($json); + header("Etag: \"$etag\""); + /* + * Make sure to strip a few things off the + * if-none-match header. Stripping whitespace may not + * be required, but removing the quote on the incoming + * header is required to make the equality test. + */ + $if_none_match = isset($_SERVER['HTTP_IF_NONE_MATCH']) ? + trim($_SERVER['HTTP_IF_NONE_MATCH'], "\t\n\r\" ") : false; + if ($if_none_match && $if_none_match == $etag) { + header('HTTP/1.1 304 Not Modified'); + return; + } + + if (isset($http_data['callback'])) { + header('content-type: text/javascript'); + return $http_data['callback'] . "({$json})"; + } else { + header('content-type: application/json'); + return $json; + } + } + + /* + * Returns a JSON formatted error string. + * + * @param $msg The error string to return + * + * @return mixed A json formatted error response. + */ + private function json_error($msg) { + header('content-type: application/json'); + return $this->json_results('error', 0, $msg); + } + + /* + * Returns a JSON formatted result data. + * + * @param $type The response method type. + * @param $data The result data to return + * + * @return mixed A json formatted result response. + */ + private function json_results($type, $count, $data) { + return json_encode(array( + 'version' => 2, + 'type' => $type, + 'resultcount' => $count, + 'results' => $data + )); + } + + private function get_extended_fields($pkgid) { + $query = "SELECT DependencyTypes.Name AS Type, " . + "PackageDepends.DepName AS Name, " . + "PackageDepends.DepCondition AS Cond " . + "FROM PackageDepends " . + "LEFT JOIN DependencyTypes " . + "ON DependencyTypes.ID = PackageDepends.DepTypeID " . + "WHERE PackageDepends.PackageID = " . $pkgid . " " . + "UNION SELECT RelationTypes.Name AS Type, " . + "PackageRelations.RelName AS Name, " . + "PackageRelations.RelCondition AS Cond " . + "FROM PackageRelations " . + "LEFT JOIN RelationTypes " . + "ON RelationTypes.ID = PackageRelations.RelTypeID " . + "WHERE PackageRelations.PackageID = " . $pkgid . " " . + "UNION SELECT 'groups' AS Type, Groups.Name, '' AS Cond " . + "FROM Groups INNER JOIN PackageGroups " . + "ON PackageGroups.PackageID = " . $pkgid . " " . + "AND PackageGroups.GroupID = Groups.ID " . + "UNION SELECT 'license' AS Type, Licenses.Name, '' AS Cond " . + "FROM Licenses INNER JOIN PackageLicenses " . + "ON PackageLicenses.PackageID = " . $pkgid . " " . + "AND PackageLicenses.LicenseID = Licenses.ID"; + $result = $this->dbh->query($query); + + if (!$result) { + return null; + } + + $type_map = array( + 'depends' => 'Depends', + 'makedepends' => 'MakeDepends', + 'checkdepends' => 'CheckDepends', + 'optdepends' => 'OptDepends', + 'conflicts' => 'Conflicts', + 'provides' => 'Provides', + 'replaces' => 'Replaces', + 'groups' => 'Groups', + 'license' => 'License', + ); + $data = array(); + while ($row = $result->fetch(PDO::FETCH_ASSOC)) { + $type = $type_map[$row['Type']]; + $data[$type][] = $row['Name'] . $row['Cond']; + } + + return $data; + } + + private function process_query($type, $where_condition) { + global $MAX_RPC_RESULTS; + $fields = implode(',', self::$fields); + $query = "SELECT {$fields} " . + "FROM Packages LEFT JOIN PackageBases " . + "ON PackageBases.ID = Packages.PackageBaseID " . + "LEFT JOIN Users ON PackageBases.MaintainerUID = Users.ID " . + "WHERE ${where_condition}"; + $result = $this->dbh->query($query); + + if ($result) { + $resultcount = 0; + $search_data = array(); + while ($row = $result->fetch(PDO::FETCH_ASSOC)) { + $resultcount++; + $pkgbase_name = $row['PackageBase']; + $row['URLPath'] = URL_DIR . substr($pkgbase_name, 0, 2) . "/" . $pkgbase_name . "/" . $pkgbase_name . ".tar.gz"; + + /* + * Unfortunately, mysql_fetch_assoc() returns + * all fields as strings. We need to coerce + * numeric values into integers to provide + * proper data types in the JSON response. + */ + foreach (self::$numeric_fields as $field) { + $row[$field] = intval($row[$field]); + } + + if ($type == 'info' || $type == 'multiinfo') { + $row = array_merge($row, $this->get_extended_fields($row['ID'])); + } + + if ($type == 'info') { + $search_data = $row; + break; + } else { + array_push($search_data, $row); + } } - // allow rpc callback for XDomainAjax - if ( isset($http_data['callback']) ) { - // it is more correct to send text/javascript - // content-type for jsonp-callback - header('content-type: text/javascript'); - return $http_data['callback'] . "({$json})"; - } - else { - // set content type header to app/json - header('content-type: application/json'); - return $json; - } - } - else { - return $this->json_error('Incorrect request type specified.'); - } - } - - /** - * Returns a JSON formatted error string. - * - * @param $msg The error string to return - * @return mixed A json formatted error response. - **/ - private function json_error($msg) { - // set content type header to app/json - header('content-type: application/json'); - return $this->json_results('error', 0, $msg); - } - - /** - * Returns a JSON formatted result data. - * @param $type The response method type. - * @param $data The result data to return - * @return mixed A json formatted result response. - **/ - private function json_results($type, $count, $data) { - return json_encode(array( - 'version' => 2, - 'type' => $type, - 'resultcount' => $count, - 'results' => $data - )); - } - - private function get_extended_fields($pkgid) { - $query = "SELECT DependencyTypes.Name AS Type, " . - "PackageDepends.DepName AS Name, " . - "PackageDepends.DepCondition AS Cond " . - "FROM PackageDepends " . - "LEFT JOIN DependencyTypes " . - "ON DependencyTypes.ID = PackageDepends.DepTypeID " . - "WHERE PackageDepends.PackageID = " . $pkgid . " " . - "UNION SELECT RelationTypes.Name AS Type, " . - "PackageRelations.RelName AS Name, " . - "PackageRelations.RelCondition AS Cond " . - "FROM PackageRelations " . - "LEFT JOIN RelationTypes " . - "ON RelationTypes.ID = PackageRelations.RelTypeID " . - "WHERE PackageRelations.PackageID = " . $pkgid . " " . - "UNION SELECT 'groups' AS Type, Groups.Name, '' AS Cond " . - "FROM Groups INNER JOIN PackageGroups " . - "ON PackageGroups.PackageID = " . $pkgid . " " . - "AND PackageGroups.GroupID = Groups.ID " . - "UNION SELECT 'license' AS Type, Licenses.Name, '' AS Cond " . - "FROM Licenses INNER JOIN PackageLicenses " . - "ON PackageLicenses.PackageID = " . $pkgid . " " . - "AND PackageLicenses.LicenseID = Licenses.ID"; - $result = $this->dbh->query($query); - - if (!$result) { - return null; - } - - $type_map = array( - 'depends' => 'Depends', - 'makedepends' => 'MakeDepends', - 'checkdepends' => 'CheckDepends', - 'optdepends' => 'OptDepends', - 'conflicts' => 'Conflicts', - 'provides' => 'Provides', - 'replaces' => 'Replaces', - 'groups' => 'Groups', - 'license' => 'License', - ); - $data = array(); - while ($row = $result->fetch(PDO::FETCH_ASSOC)) { - $type = $type_map[$row['Type']]; - $data[$type][] = $row['Name'] . $row['Cond']; - } - - return $data; - } - - private function process_query($type, $where_condition) { - global $MAX_RPC_RESULTS; - $fields = implode(',', self::$fields); - $query = "SELECT {$fields} " . - "FROM Packages LEFT JOIN PackageBases " . - "ON PackageBases.ID = Packages.PackageBaseID " . - "LEFT JOIN Users ON PackageBases.MaintainerUID = Users.ID " . - "WHERE ${where_condition}"; - $result = $this->dbh->query($query); - - if ($result) { - $resultcount = 0; - $search_data = array(); - while ($row = $result->fetch(PDO::FETCH_ASSOC)) { - $resultcount++; - $pkgbase_name = $row['PackageBase']; - $row['URLPath'] = URL_DIR . substr($pkgbase_name, 0, 2) . "/" . $pkgbase_name . "/" . $pkgbase_name . ".tar.gz"; - - /* Unfortunately, mysql_fetch_assoc() returns all fields as - * strings. We need to coerce numeric values into integers to - * provide proper data types in the JSON response. - */ - foreach (self::$numeric_fields as $field) { - $row[$field] = intval($row[$field]); - } - - if ($type == 'info' || $type == 'multiinfo') { - $row = array_merge($row, $this->get_extended_fields($row['ID'])); - } - - if ($type == 'info') { - $search_data = $row; - break; - } - else { - array_push($search_data, $row); - } - } - - if ($resultcount === $MAX_RPC_RESULTS) { - return $this->json_error('Too many package results.'); - } - - return $this->json_results($type, $resultcount, $search_data); - } - else { - return $this->json_results($type, 0, array()); - } - } - - /** - * Parse the args to the multiinfo function. We may have a string or an - * array, so do the appropriate thing. Within the elements, both * package - * IDs and package names are valid; sort them into the relevant arrays and - * escape/quote the names. - * @param $args the arg string or array to parse. - * @return mixed An array containing 'ids' and 'names'. - **/ - private function parse_multiinfo_args($args) { - if (!is_array($args)) { - $args = array($args); - } - - $id_args = array(); - $name_args = array(); - foreach ($args as $arg) { - if (!$arg) { - continue; - } - if (is_numeric($arg)) { - $id_args[] = intval($arg); - } else { - $name_args[] = $this->dbh->quote($arg); - } - } - - return array('ids' => $id_args, 'names' => $name_args); - } - - /** - * Performs a fulltext mysql search of the package database. - * @param $keyword_string A string of keywords to search with. - * @return mixed Returns an array of package matches. - **/ - private function search($keyword_string) { - global $MAX_RPC_RESULTS; - if (strlen($keyword_string) < 2) { - return $this->json_error('Query arg too small'); - } - - $keyword_string = $this->dbh->quote("%" . addcslashes($keyword_string, '%_') . "%"); - - $where_condition = "(Packages.Name LIKE {$keyword_string} OR "; - $where_condition.= "Description LIKE {$keyword_string}) "; - $where_condition.= "LIMIT {$MAX_RPC_RESULTS}"; - - return $this->process_query('search', $where_condition); - } - - /** - * Returns the info on a specific package. - * @param $pqdata The ID or name of the package. Package Query Data. - * @return mixed Returns an array of value data containing the package data - **/ - private function info($pqdata) { - if ( is_numeric($pqdata) ) { - // just using sprintf to coerce the pqd to an int - // should handle sql injection issues, since sprintf will - // bork if not an int, or convert the string to a number 0 - $where_condition = "Packages.ID={$pqdata}"; - } - else { - $where_condition = sprintf("Packages.Name=%s", $this->dbh->quote($pqdata)); - } - return $this->process_query('info', $where_condition); - } - - /** - * Returns the info on multiple packages. - * @param $pqdata A comma-separated list of IDs or names of the packages. - * @return mixed Returns an array of results containing the package data - **/ - private function multiinfo($pqdata) { - global $MAX_RPC_RESULTS; - $args = $this->parse_multiinfo_args($pqdata); - $ids = $args['ids']; - $names = $args['names']; - - if (!$ids && !$names) { - return $this->json_error('Invalid query arguments'); - } - - $where_condition = ""; - if ($ids) { - $ids_value = implode(',', $args['ids']); - $where_condition .= "ID IN ({$ids_value}) "; - } - if ($ids && $names) { - $where_condition .= "OR "; - } - if ($names) { - // individual names were quoted in parse_multiinfo_args() - $names_value = implode(',', $args['names']); - $where_condition .= "Packages.Name IN ({$names_value}) "; - } - - $where_condition .= "LIMIT {$MAX_RPC_RESULTS}"; - - return $this->process_query('multiinfo', $where_condition); - } - - /** - * Returns all the packages for a specific maintainer. - * @param $maintainer The name of the maintainer. - * @return mixed Returns an array of value data containing the package data - **/ - private function msearch($maintainer) { - global $MAX_RPC_RESULTS; - $maintainer = $this->dbh->quote($maintainer); - - $where_condition = "Users.Username = {$maintainer} "; - $where_condition .= "LIMIT {$MAX_RPC_RESULTS}"; - - return $this->process_query('msearch', $where_condition); - } - - /** - * Get all package names that start with $search. - * @param string $search Search string. - * @return string The JSON formatted response data. - **/ - private function suggest($search) { - $query = 'SELECT Name FROM Packages WHERE Name LIKE ' . - $this->dbh->quote(addcslashes($search, '%_') . '%') . - ' ORDER BY Name ASC LIMIT 20'; - - $result = $this->dbh->query($query); - $result_array = array(); - - if ($result) { - $result_array = $result->fetchAll(PDO::FETCH_COLUMN, 0); - } - - return json_encode($result_array); - } + if ($resultcount === $MAX_RPC_RESULTS) { + return $this->json_error('Too many package results.'); + } + + return $this->json_results($type, $resultcount, $search_data); + } else { + return $this->json_results($type, 0, array()); + } + } + + /* + * Parse the args to the multiinfo function. We may have a string or an + * array, so do the appropriate thing. Within the elements, both * package + * IDs and package names are valid; sort them into the relevant arrays and + * escape/quote the names. + * + * @param $args the arg string or array to parse. + * + * @return mixed An array containing 'ids' and 'names'. + */ + private function parse_multiinfo_args($args) { + if (!is_array($args)) { + $args = array($args); + } + + $id_args = array(); + $name_args = array(); + foreach ($args as $arg) { + if (!$arg) { + continue; + } + if (is_numeric($arg)) { + $id_args[] = intval($arg); + } else { + $name_args[] = $this->dbh->quote($arg); + } + } + + return array('ids' => $id_args, 'names' => $name_args); + } + + /* + * Performs a fulltext mysql search of the package database. + * + * @param $keyword_string A string of keywords to search with. + * + * @return mixed Returns an array of package matches. + */ + private function search($keyword_string) { + global $MAX_RPC_RESULTS; + + if (strlen($keyword_string) < 2) { + return $this->json_error('Query arg too small'); + } + + $keyword_string = $this->dbh->quote("%" . addcslashes($keyword_string, '%_') . "%"); + + $where_condition = "(Packages.Name LIKE $keyword_string OR "; + $where_condition .= "Description LIKE $keyword_string) "; + $where_condition .= "LIMIT $MAX_RPC_RESULTS"; + + return $this->process_query('search', $where_condition); + } + + /* + * Returns the info on a specific package. + * + * @param $pqdata The ID or name of the package. Package Query Data. + * + * @return mixed Returns an array of value data containing the package data + */ + private function info($pqdata) { + if (is_numeric($pqdata)) { + $where_condition = "Packages.ID = $pqdata"; + } else { + $where_condition = "Packages.Name = " . $this->dbh->quote($pqdata); + } + + return $this->process_query('info', $where_condition); + } + + /* + * Returns the info on multiple packages. + * + * @param $pqdata A comma-separated list of IDs or names of the packages. + * + * @return mixed Returns an array of results containing the package data + */ + private function multiinfo($pqdata) { + global $MAX_RPC_RESULTS; + + $args = $this->parse_multiinfo_args($pqdata); + $ids = $args['ids']; + $names = $args['names']; + + if (!$ids && !$names) { + return $this->json_error('Invalid query arguments'); + } + + $where_condition = ""; + if ($ids) { + $ids_value = implode(',', $args['ids']); + $where_condition .= "ID IN ($ids_value) "; + } + if ($ids && $names) { + $where_condition .= "OR "; + } + if ($names) { + /* + * Individual names were quoted in + * parse_multiinfo_args(). + */ + $names_value = implode(',', $args['names']); + $where_condition .= "Packages.Name IN ($names_value) "; + } + $where_condition .= "LIMIT $MAX_RPC_RESULTS"; + + return $this->process_query('multiinfo', $where_condition); + } + + /* + * Returns all the packages for a specific maintainer. + * + * @param $maintainer The name of the maintainer. + * + * @return mixed Returns an array of value data containing the package data + */ + private function msearch($maintainer) { + global $MAX_RPC_RESULTS; + + $maintainer = $this->dbh->quote($maintainer); + + $where_condition = "Users.Username = $maintainer "; + $where_condition .= "LIMIT $MAX_RPC_RESULTS"; + + return $this->process_query('msearch', $where_condition); + } + + /* + * Get all package names that start with $search. + * + * @param string $search Search string. + * + * @return string The JSON formatted response data. + */ + private function suggest($search) { + $query = 'SELECT Name FROM Packages WHERE Name LIKE ' . + $this->dbh->quote(addcslashes($search, '%_') . '%') . + ' ORDER BY Name ASC LIMIT 20'; + + $result = $this->dbh->query($query); + $result_array = array(); + + if ($result) { + $result_array = $result->fetchAll(PDO::FETCH_COLUMN, 0); + } + + return json_encode($result_array); + } } |