/** * Parse query string into parameters, validating and filling in defaults */ public static function parseQueryParams($queryString, $action, $singleObject, $apiVersion = false, $atomAccepted = false) { // Handle multiple identical parameters in the CGI-standard way instead of // PHP's foo[]=bar way $queryParams = Zotero_URL::proper_parse_str($queryString); $finalParams = []; // // Handle some special cases // // If client accepts Atom, serve it if an explicit format isn't requested if ($atomAccepted && empty($queryParams['format'])) { $queryParams['format'] = 'atom'; } // Set API version based on header if ($apiVersion) { if (!empty($queryParams['v']) && $apiVersion != $queryParams['v']) { throw new Exception("Zotero-API-Version header does not match 'v' query parameter", Z_ERROR_INVALID_INPUT); } $queryParams['v'] = $apiVersion; } else { if (isset($queryParams['version']) && $queryParams['version'] == 1 && !isset($queryParams['v'])) { $queryParams['v'] = 1; unset($queryParams['version']); } } // If format=json, override version to 3 if (!isset($queryParams['v']) && isset($queryParams['format']) && $queryParams['format'] == 'json') { $queryParams['v'] = 3; } $apiVersion = isset($queryParams['v']) ? $queryParams['v'] : self::$defaultParams['v']; // If 'content', override 'format' to 'atom' if (!isset($queryParams['format']) && isset($queryParams['content'])) { $queryParams['format'] = 'atom'; } // Handle deprecated (in v3) 'order' parameter if (isset($queryParams['order'])) { // If 'order' is a direction, move it to 'direction' if (in_array($queryParams['order'], ['asc', 'desc'])) { $finalParams['direction'] = $queryParams['direction'] = $queryParams['order']; } else { // If 'sort' already has a direction, move that to 'direction' first if (isset($queryParams['sort']) && in_array($queryParams['sort'], ['asc', 'desc'])) { $finalParams['direction'] = $queryParams['direction'] = $queryParams['sort']; } $queryParams['sort'] = $queryParams['order']; } unset($queryParams['order']); } // Handle deprecated (in v3) 'newer' and 'newertime' parameters if (isset($queryParams['newer'])) { if (!isset($queryParams['since'])) { $queryParams['since'] = $queryParams['newer']; } unset($queryParams['newer']); } if (isset($queryParams['newertime'])) { if (!isset($queryParams['sincetime'])) { $queryParams['sincetime'] = $queryParams['newertime']; } unset($queryParams['newertime']); } foreach (self::resolveDefaultParams($action, self::$defaultParams, $queryParams) as $key => $value) { // Don't overwrite field if already set (either above or derived from another field) if (!empty($finalParams[$key])) { continue; } // Fill defaults $finalParams[$key] = $value; // Ignore private parameters in the URL if (in_array($key, self::getPrivateParams($key)) && isset($queryParams[$key])) { continue; } // If no parameter passed, use default if (!isset($queryParams[$key])) { continue; } // Use query parameter as value $value = $queryParams[$key]; if (isset($finalParams['format'])) { $format = $finalParams['format']; // Some formats need special parameter handling if ($format == 'bib') { switch ($key) { // Invalid parameters case 'order': case 'sort': case 'start': case 'limit': case 'direction': throw new Exception("'{$key}' is not valid for format=bib", Z_ERROR_INVALID_INPUT); } } else { if ($apiVersion < 3 && in_array($format, array('keys', 'versions'))) { switch ($key) { // Invalid parameters case 'start': throw new Exception("'{$key}' is not valid for format={$format}", Z_ERROR_INVALID_INPUT); } } } } switch ($key) { case 'v': if (!in_array($value, self::$validAPIVersions)) { throw new Exception("Invalid API version '{$value}'", Z_ERROR_INVALID_INPUT); } break; case 'format': if (!self::isValidFormatForAction($action, $value, $singleObject)) { throw new Exception("Invalid 'format' value '{$value}'", Z_ERROR_INVALID_INPUT); } break; case 'since': case 'sincetime': if (!is_numeric($value)) { throw new Exception("Invalid value for '{$key}' parameter", Z_ERROR_INVALID_INPUT); } break; case 'start': $value = (int) $value; break; case 'limit': // Maximum limit depends on 'format' $limitMax = self::getLimitMax($format); // Since the export formats and csljson don't give a clear indication of limiting or // rel="next" links in API v1/2 (before the Link header), require an explicit limit for // everything other than single items and itemKey queries if ($apiVersion < 3 && (in_array($format, Zotero_Translate::$exportFormats) || $format == 'csljson') && !$singleObject && empty($queryParams['itemKey'])) { if (empty($value)) { throw new Exception("'limit' is required for format={$format}", Z_ERROR_INVALID_INPUT); } else { if ($value > $limitMax) { throw new Exception("'limit' cannot be greater than {$limitMax} for format={$format}", Z_ERROR_INVALID_INPUT); } } } // If there's a maximum, enforce it if ($limitMax && (int) $value > $limitMax) { $value = $limitMax; } else { if ((int) $value == 0) { continue 2; } } $value = (int) $value; break; case 'include': case 'content': if ($key == 'content' && $format != 'atom') { throw new Exception("'content' is valid only for format=atom", Z_ERROR_INVALID_INPUT); } else { if ($key == 'include' && $format != 'json') { throw new Exception("'include' is valid only for format=json", Z_ERROR_INVALID_INPUT); } } $value = array_values(array_unique(explode(',', $value))); sort($value); foreach ($value as $includeType) { switch ($includeType) { case 'none': if (sizeOf($value) > 1) { throw new Exception("{$key}={$includeType} is not valid in multi-format responses", Z_ERROR_INVALID_INPUT); } break; case 'html': case 'citation': case 'bib': case 'csljson': break; case 'json': if ($format != 'atom') { throw new Exception("{$key}={$includeType} is valid only for format=atom", Z_ERROR_INVALID_INPUT); } break; case 'data': if ($format != 'json') { throw new Exception("{$key}={$includeType} is valid only for format=json", Z_ERROR_INVALID_INPUT); } break; default: if (in_array($includeType, Zotero_Translate::$exportFormats)) { break; } throw new Exception("Invalid '{$key}' value '{$includeType}'", Z_ERROR_INVALID_INPUT); } } break; case 'sort': // If direction, move to 'direction' and use default 'sort' value if (in_array($value, array('asc', 'desc'))) { $finalParams['direction'] = $queryParams['direction'] = $value; continue 2; } // Whether to sort empty values first $finalParams['emptyFirst'] = self::getSortEmptyFirst($value); switch ($value) { // Valid fields to sort by // // Allow all fields available in client case 'title': case 'creator': case 'itemType': case 'date': case 'publisher': case 'publicationTitle': case 'journalAbbreviation': case 'language': case 'accessDate': case 'libraryCatalog': case 'callNumber': case 'rights': case 'dateAdded': case 'dateModified': //case 'numChildren': //case 'numChildren': case 'addedBy': case 'numItems': case 'serverDateModified': case 'collectionKeyList': case 'itemKeyList': case 'searchKeyList': switch ($value) { // numItems is valid only for tags requests case 'numItems': if ($action != 'tags') { throw new Exception("Invalid 'order' value '{$value}'", Z_ERROR_INVALID_INPUT); } break; case 'collectionKeyList': if ($action != 'collections') { throw new Exception("order=collectionKeyList is not valid for this request"); } if (!isset($queryParams['collectionKey'])) { throw new Exception("order=collectionKeyList requires the collectionKey parameter"); } break; case 'itemKeyList': if ($action != 'items') { throw new Exception("order=itemKeyList is not valid for this request"); } if (!isset($queryParams['itemKey'])) { throw new Exception("order=itemKeyList requires the itemKey parameter"); } break; case 'searchKeyList': if ($action != 'searches') { throw new Exception("order=searchKeyList is not valid for this request"); } if (!isset($queryParams['searchKey'])) { throw new Exception("order=searchKeyList requires the searchKey parameter"); } break; } if (!isset($queryParams['direction'])) { $finalParams['direction'] = self::getDefaultDirection($value); } break; default: throw new Exception("Invalid 'sort' value '{$value}'", Z_ERROR_INVALID_INPUT); } break; case 'direction': if (!in_array($value, array('asc', 'desc'))) { throw new Exception("Invalid '{$key}' value '{$value}'", Z_ERROR_INVALID_INPUT); } break; case 'qmode': if (!in_array($value, array('titleCreatorYear', 'everything'))) { throw new Exception("Invalid '{$key}' value '{$value}'", Z_ERROR_INVALID_INPUT); } break; case 'collectionKey': case 'itemKey': case 'searchKey': // Allow leading/trailing commas $objectKeys = trim($value, ","); $objectKeys = explode(",", $objectKeys); // Make sure all keys are plausible foreach ($objectKeys as $objectKey) { if (!Zotero_ID::isValidKey($objectKey)) { throw new Exception("Invalid '{$key}' value '{$value}'", Z_ERROR_INVALID_INPUT); } } $value = $objectKeys; // Force limit if explicit object keys are used $finalParams['limit'] = self::MAX_OBJECT_KEYS; break; case 'includeTrashed': case 'uncached': $value = !!$value; break; } $finalParams[$key] = $value; } return $finalParams; }
/** * Parse query string into parameters, validating and filling in defaults */ public static function parseQueryParams($queryString, $action, $singleObject) { // Handle multiple identical parameters in the CGI-standard way instead of // PHP's foo[]=bar way $getParams = Zotero_URL::proper_parse_str($queryString); $queryParams = array(); foreach (self::getDefaultQueryParams() as $key => $val) { // Don't overwrite field if already derived from another field if (!empty($queryParams[$key])) { continue; } if ($key == 'limit') { $val = self::getDefaultLimit(isset($getParams['format']) ? $getParams['format'] : ""); } // Fill defaults $queryParams[$key] = $val; // If no parameter passed, used default if (!isset($getParams[$key])) { continue; } // Some formats need special parameter handling if (isset($getParams['format'])) { if ($getParams['format'] == 'bib') { switch ($key) { // Invalid parameters case 'order': case 'sort': case 'start': case 'limit': throw new Exception("'{$key}' is not valid for format=bib", Z_ERROR_INVALID_INPUT); } } else { if ($getParams['format'] == 'keys') { switch ($key) { // Invalid parameters case 'start': throw new Exception("'{$key}' is not valid for format=bib", Z_ERROR_INVALID_INPUT); } } } } switch ($key) { case 'format': $format = $getParams[$key]; $isExportFormat = in_array($format, Zotero_Translate::$exportFormats); // All actions other than items must be Atom if ($action != 'items') { if ($format != 'atom') { throw new Exception("Invalid 'format' value '{$format}'", Z_ERROR_INVALID_INPUT); } } else { if ($isExportFormat || $format == 'csljson') { if ($singleObject || !empty($getParams['itemKey'])) { break; } $limitMax = self::getLimitMax($format); if (empty($getParams['limit'])) { throw new Exception("'limit' is required for format={$format}", Z_ERROR_INVALID_INPUT); } else { if ($getParams['limit'] > $limitMax) { throw new Exception("'limit' cannot be greater than {$limitMax} for format={$format}", Z_ERROR_INVALID_INPUT); } } } else { switch ($format) { case 'atom': case 'bib': break; default: if ($format == 'keys' && !$singleObject) { break; } throw new Exception("Invalid 'format' value '{$format}' for request", Z_ERROR_INVALID_INPUT); } } } break; case 'start': $queryParams[$key] = (int) $getParams[$key]; continue 2; case 'limit': // Maximum limit depends on 'format' $limitMax = self::getLimitMax(isset($getParams['format']) ? $getParams['format'] : ""); // If there's a maximum, enforce it if ($limitMax && (int) $getParams[$key] > $limitMax) { $getParams[$key] = $limitMax; } else { if ((int) $getParams[$key] == 0) { continue 2; } } $queryParams[$key] = (int) $getParams[$key]; continue 2; case 'content': if (isset($getParams['format']) && $getParams['format'] != 'atom') { throw new Exception("'content' is valid only for format=atom", Z_ERROR_INVALID_INPUT); } $getParams[$key] = array_values(array_unique(explode(',', $getParams[$key]))); sort($getParams[$key]); foreach ($getParams[$key] as $value) { switch ($value) { case 'none': case 'full': if (sizeOf($getParams[$key]) > 1) { throw new Exception("content={$value} is not valid in " . "multi-format responses", Z_ERROR_INVALID_INPUT); } break; case 'html': case 'citation': case 'bib': case 'json': case 'csljson': break; default: if (in_array($value, Zotero_Translate::$exportFormats)) { break; } throw new Exception("Invalid 'content' value '{$value}'", Z_ERROR_INVALID_INPUT); } } break; case 'order': // Whether to sort empty values first $queryParams['emptyFirst'] = Zotero_API::getSortEmptyFirst($getParams[$key]); switch ($getParams[$key]) { // Valid fields to sort by // // Allow all fields available in client case 'title': case 'creator': case 'itemType': case 'date': case 'publisher': case 'publicationTitle': case 'journalAbbreviation': case 'language': case 'accessDate': case 'libraryCatalog': case 'callNumber': case 'rights': case 'dateAdded': case 'dateModified': //case 'numChildren': //case 'numChildren': case 'addedBy': case 'numItems': case 'serverDateModified': // numItems is valid only for tags requests switch ($getParams[$key]) { case 'numItems': if ($action != 'tags') { throw new Exception("Invalid 'order' value '" . $getParams[$key] . "'", Z_ERROR_INVALID_INPUT); } break; } if (!isset($getParams['sort'])) { $queryParams['sort'] = self::getDefaultSort($getParams[$key]); } else { if (!in_array($getParams['sort'], array('asc', 'desc'))) { throw new Exception("Invalid 'sort' value '" . $getParams['sort'] . "'", Z_ERROR_INVALID_INPUT); } else { $queryParams['sort'] = $getParams['sort']; } } break; default: throw new Exception("Invalid 'order' value '" . $getParams[$key] . "'", Z_ERROR_INVALID_INPUT); } break; case 'sort': if (!in_array($getParams['sort'], array('asc', 'desc'))) { throw new Exception("Invalid 'sort' value '" . $getParams[$key] . "'", Z_ERROR_INVALID_INPUT); } break; } $queryParams[$key] = $getParams[$key]; } return $queryParams; }