Exemplo n.º 1
0
 public function testExistsByLibraryAndKey()
 {
     $this->assertFalse(Zotero_Items::existsByLibraryAndKey(self::$config['userLibraryID'], "AAAAAAAA"));
     $item = new Zotero_Item();
     $item->libraryID = self::$config['userLibraryID'];
     $item->itemTypeID = Zotero_ItemTypes::getID("book");
     $item->save();
     $key = $item->key;
     $this->assertTrue(Zotero_Items::existsByLibraryAndKey(self::$config['userLibraryID'], $key));
     Zotero_Items::delete(self::$config['userLibraryID'], $key);
     $this->assertFalse(Zotero_Items::existsByLibraryAndKey(self::$config['userLibraryID'], $key));
 }
Exemplo n.º 2
0
 public function items()
 {
     if (($this->method == 'POST' || $this->method == 'PUT') && !$this->body) {
         $this->e400("{$this->method} data not provided");
     }
     $itemIDs = array();
     $responseItems = array();
     $responseKeys = array();
     $totalResults = null;
     //
     // Single item
     //
     if (($this->objectID || $this->objectKey) && !$this->subset) {
         if ($this->fileMode) {
             if ($this->fileView) {
                 $this->allowMethods(array('GET', 'HEAD', 'POST'));
             } else {
                 $this->allowMethods(array('GET', 'PUT', 'POST', 'HEAD', 'PATCH'));
             }
         } else {
             $this->allowMethods(array('GET', 'PUT', 'DELETE'));
         }
         // Check for general library access
         if (!$this->permissions->canAccess($this->objectLibraryID)) {
             //var_dump($this->objectLibraryID);
             //var_dump($this->permissions);
             $this->e403();
         }
         if ($this->objectKey) {
             $item = Zotero_Items::getByLibraryAndKey($this->objectLibraryID, $this->objectKey);
         } else {
             try {
                 $item = Zotero_Items::get($this->objectLibraryID, $this->objectID);
             } catch (Exception $e) {
                 if ($e->getCode() == Z_ERROR_OBJECT_LIBRARY_MISMATCH) {
                     $item = false;
                 } else {
                     throw $e;
                 }
             }
         }
         if (!$item) {
             // Possibly temporary workaround to block unnecessary full syncs
             if ($this->fileMode && $this->method == 'POST') {
                 // If > 2 requests for missing file, trigger a full sync via 404
                 $cacheKey = "apiMissingFile_" . $this->objectLibraryID . "_" . ($this->objectKey ? $this->objectKey : $this->objectID);
                 $set = Z_Core::$MC->get($cacheKey);
                 if (!$set) {
                     Z_Core::$MC->set($cacheKey, 1, 86400);
                 } else {
                     if ($set < 2) {
                         Z_Core::$MC->increment($cacheKey);
                     } else {
                         Z_Core::$MC->delete($cacheKey);
                         $this->e404("A file sync error occurred. Please sync again.");
                     }
                 }
                 $this->e500("A file sync error occurred. Please sync again.");
             }
             // If we have an id, make sure this isn't really an all-numeric key
             if ($this->objectID && strlen($this->objectID) == 8 && preg_match('/[0-9]{8}/', $this->objectID)) {
                 $item = Zotero_Items::getByLibraryAndKey($this->objectLibraryID, $this->objectID);
                 if ($item) {
                     $this->objectKey = $this->objectID;
                     unset($this->objectID);
                 }
             }
             if (!$item) {
                 $this->e404("Item does not exist");
             }
         }
         if ($item->isNote() && !$this->permissions->canAccess($this->objectLibraryID, 'notes')) {
             $this->e403();
         }
         // Make sure URL libraryID matches item libraryID
         if ($this->objectLibraryID != $item->libraryID) {
             $this->e404("Item does not exist");
         }
         // File access mode
         if ($this->fileMode) {
             $this->_handleFileRequest($item);
         }
         // If id, redirect to key URL
         if ($this->objectID) {
             $this->allowMethods(array('GET'));
             $qs = !empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '';
             header("Location: " . Zotero_API::getItemURI($item) . $qs);
             exit;
         }
         if ($this->scopeObject) {
             switch ($this->scopeObject) {
                 // Remove item from collection
                 case 'collections':
                     $this->allowMethods(array('DELETE'));
                     if (!$this->permissions->canWrite($this->objectLibraryID)) {
                         $this->e403("Write access denied");
                     }
                     $collection = Zotero_Collections::getByLibraryAndKey($this->objectLibraryID, $this->scopeObjectKey);
                     if (!$collection) {
                         $this->e404("Collection not found");
                     }
                     if (!$collection->hasItem($item->id)) {
                         $this->e404("Item not found in collection");
                     }
                     Zotero_DB::beginTransaction();
                     $timestamp = Zotero_Libraries::updateTimestamps($this->objectLibraryID);
                     Zotero_DB::registerTransactionTimestamp($timestamp);
                     $collection->removeItem($item->id);
                     Zotero_DB::commit();
                     $this->e204();
                 default:
                     $this->e400();
             }
         }
         if ($this->method == 'PUT' || $this->method == 'DELETE') {
             if (!$this->permissions->canWrite($this->objectLibraryID)) {
                 $this->e403("Write access denied");
             }
             if (!Z_CONFIG::$TESTING_SITE || empty($_GET['skipetag'])) {
                 if (empty($_SERVER['HTTP_IF_MATCH'])) {
                     $this->e400("If-Match header not provided");
                 }
                 if (!preg_match('/^"?([a-f0-9]{32})"?$/', $_SERVER['HTTP_IF_MATCH'], $matches)) {
                     $this->e400("Invalid ETag in If-Match header");
                 }
                 if ($item->etag != $matches[1]) {
                     $this->e412("ETag does not match current version of item");
                 }
             }
             // Update existing item
             if ($this->method == 'PUT') {
                 $obj = $this->jsonDecode($this->body);
                 Zotero_Items::updateFromJSON($item, $obj, false, null, $this->userID);
                 $this->queryParams['format'] = 'atom';
                 $this->queryParams['content'] = array('json');
                 if ($cacheKey = $this->getWriteTokenCacheKey()) {
                     Z_Core::$MC->set($cacheKey, true, $this->writeTokenCacheTime);
                 }
             } else {
                 Zotero_Items::delete($this->objectLibraryID, $this->objectKey, true);
                 try {
                     Zotero_Processors::notifyProcessors('index');
                 } catch (Exception $e) {
                     Z_Core::logError($e);
                 }
                 $this->e204();
             }
         }
         // Display item
         switch ($this->queryParams['format']) {
             case 'atom':
                 $this->responseXML = Zotero_Items::convertItemToAtom($item, $this->queryParams, $this->apiVersion, $this->permissions);
                 break;
             case 'bib':
                 echo Zotero_Cite::getBibliographyFromCitationServer(array($item), $this->queryParams['style'], $this->queryParams['css']);
                 exit;
             case 'csljson':
                 $json = Zotero_Cite::getJSONFromItems(array($item), true);
                 if ($this->queryParams['pprint']) {
                     header("Content-Type: text/plain");
                     $json = Zotero_Utilities::json_encode_pretty($json);
                 } else {
                     header("Content-Type: application/vnd.citationstyles.csl+json");
                     $json = json_encode($json);
                 }
                 echo $json;
                 exit;
             default:
                 $export = Zotero_Translate::doExport(array($item), $this->queryParams['format']);
                 if ($this->queryParams['pprint']) {
                     header("Content-Type: text/plain");
                 } else {
                     header("Content-Type: " . $export['mimeType']);
                 }
                 echo $export['body'];
                 exit;
         }
     } else {
         $this->allowMethods(array('GET', 'POST'));
         if (!$this->permissions->canAccess($this->objectLibraryID)) {
             $this->e403();
         }
         $includeTrashed = false;
         $formatAsKeys = $this->queryParams['format'] == 'keys';
         if ($this->scopeObject) {
             $this->allowMethods(array('GET', 'POST'));
             // If id, redirect to key URL
             if ($this->scopeObjectID) {
                 $this->allowMethods(array('GET'));
                 if (!in_array($this->scopeObject, array("collections", "tags"))) {
                     $this->e400();
                 }
                 $className = 'Zotero_' . ucwords($this->scopeObject);
                 $obj = call_user_func(array($className, 'get'), $this->objectLibraryID, $this->scopeObjectID);
                 if (!$obj) {
                     $this->e404("Scope " . substr($this->scopeObject, 0, -1) . " not found");
                 }
                 $base = call_user_func(array('Zotero_API', 'get' . substr(ucwords($this->scopeObject), 0, -1) . 'URI'), $obj);
                 $qs = !empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '';
                 header("Location: " . $base . "/items" . $qs);
                 exit;
             }
             switch ($this->scopeObject) {
                 case 'collections':
                     $collection = Zotero_Collections::getByLibraryAndKey($this->objectLibraryID, $this->scopeObjectKey);
                     if (!$collection) {
                         $this->e404("Collection not found");
                     }
                     // Add items to collection
                     if ($this->method == 'POST') {
                         if (!$this->permissions->canWrite($this->objectLibraryID)) {
                             $this->e403("Write access denied");
                         }
                         Zotero_DB::beginTransaction();
                         $timestamp = Zotero_Libraries::updateTimestamps($this->objectLibraryID);
                         Zotero_DB::registerTransactionTimestamp($timestamp);
                         $itemKeys = explode(' ', $this->body);
                         $itemIDs = array();
                         foreach ($itemKeys as $key) {
                             try {
                                 $item = Zotero_Items::getByLibraryAndKey($this->objectLibraryID, $key);
                             } catch (Exception $e) {
                                 if ($e->getCode() == Z_ERROR_OBJECT_LIBRARY_MISMATCH) {
                                     $item = false;
                                 } else {
                                     throw $e;
                                 }
                             }
                             if (!$item) {
                                 throw new Exception("Item '{$key}' not found in library", Z_ERROR_INVALID_INPUT);
                             }
                             if ($item->getSource()) {
                                 throw new Exception("Child items cannot be added to collections directly", Z_ERROR_INVALID_INPUT);
                             }
                             $itemIDs[] = $item->id;
                         }
                         $collection->addItems($itemIDs);
                         Zotero_DB::commit();
                         $this->e204();
                     }
                     $title = "Items in Collection ‘" . $collection->name . "’";
                     $itemIDs = $collection->getChildItems();
                     break;
                 case 'tags':
                     $this->allowMethods(array('GET'));
                     $tagIDs = Zotero_Tags::getIDs($this->objectLibraryID, $this->scopeObjectName);
                     if (!$tagIDs) {
                         $this->e404("Tag not found");
                     }
                     $itemIDs = array();
                     $title = '';
                     foreach ($tagIDs as $tagID) {
                         $tag = new Zotero_Tag();
                         $tag->libraryID = $this->objectLibraryID;
                         $tag->id = $tagID;
                         // Use a real tag name, in case case differs
                         if (!$title) {
                             $title = "Items of Tag ‘" . $tag->name . "’";
                         }
                         $itemIDs = array_merge($itemIDs, $tag->getLinkedItems(true));
                     }
                     $itemIDs = array_unique($itemIDs);
                     break;
                 default:
                     throw new Exception("Invalid items scope object '{$this->scopeObject}'");
             }
         } else {
             // Top-level items
             if ($this->subset == 'top') {
                 $this->allowMethods(array('GET'));
                 $title = "Top-Level Items";
                 $results = Zotero_Items::search($this->objectLibraryID, true, $this->queryParams, false, $formatAsKeys);
             } else {
                 if ($this->subset == 'trash') {
                     $this->allowMethods(array('GET'));
                     $title = "Deleted Items";
                     $itemIDs = Zotero_Items::getDeleted($this->objectLibraryID, true);
                     $includeTrashed = true;
                 } else {
                     if ($this->subset == 'children') {
                         // If we have an id, make sure this isn't really an all-numeric key
                         if ($this->objectID && strlen($this->objectID) == 8 && preg_match('/[0-9]{8}/', $this->objectID)) {
                             $item = Zotero_Items::getByLibraryAndKey($this->objectLibraryID, $this->objectID);
                             if ($item) {
                                 $this->objectKey = $this->objectID;
                                 unset($this->objectID);
                             }
                         }
                         // If id, redirect to key URL
                         if ($this->objectID) {
                             $this->allowMethods(array('GET'));
                             $item = Zotero_Items::get($this->objectLibraryID, $this->objectID);
                             if (!$item) {
                                 $this->e404("Item not found");
                             }
                             $qs = !empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '';
                             header("Location: " . Zotero_API::getItemURI($item) . '/children' . $qs);
                             exit;
                         }
                         $item = Zotero_Items::getByLibraryAndKey($this->objectLibraryID, $this->objectKey);
                         if (!$item) {
                             $this->e404("Item not found");
                         }
                         // Create new child items
                         if ($this->method == 'POST') {
                             if (!$this->permissions->canWrite($this->objectLibraryID)) {
                                 $this->e403("Write access denied");
                             }
                             $obj = $this->jsonDecode($this->body);
                             $keys = Zotero_Items::addFromJSON($obj, $this->objectLibraryID, $item, $this->userID);
                             if ($cacheKey = $this->getWriteTokenCacheKey()) {
                                 Z_Core::$MC->set($cacheKey, true, $this->writeTokenCacheTime);
                             }
                             $uri = Zotero_API::getItemURI($item) . "/children";
                             $queryString = "itemKey=" . urlencode(implode(",", $keys)) . "&content=json";
                             if ($this->apiKey) {
                                 $queryString .= "&key=" . $this->apiKey;
                             }
                             $uri .= "?" . $queryString;
                             $this->responseCode = 201;
                             $this->queryParams = Zotero_API::parseQueryParams($queryString, $this->action, false);
                         }
                         // Display items
                         $title = "Child Items of ‘" . $item->getDisplayTitle() . "’";
                         $notes = $item->getNotes();
                         $attachments = $item->getAttachments();
                         $itemIDs = array_merge($notes, $attachments);
                     } else {
                         // Create new items
                         if ($this->method == 'POST') {
                             if (!$this->permissions->canWrite($this->objectLibraryID)) {
                                 $this->e403("Write access denied");
                             }
                             $obj = $this->jsonDecode($this->body);
                             if (isset($obj->url)) {
                                 $response = Zotero_Items::addFromURL($obj, $this->objectLibraryID, $this->userID, $this->getTranslationToken());
                                 if ($response instanceof stdClass) {
                                     header("Content-Type: application/json");
                                     echo json_encode($response->select);
                                     $this->e300();
                                 } else {
                                     if (is_int($response)) {
                                         switch ($response) {
                                             case 501:
                                                 $this->e501("No translators found for URL");
                                                 break;
                                             default:
                                                 $this->e500("Error translating URL");
                                         }
                                     } else {
                                         $keys = $response;
                                     }
                                 }
                             } else {
                                 $keys = Zotero_Items::addFromJSON($obj, $this->objectLibraryID, null, $this->userID);
                             }
                             if (!$keys) {
                                 throw new Exception("No items added");
                             }
                             if ($cacheKey = $this->getWriteTokenCacheKey()) {
                                 Z_Core::$MC->set($cacheKey, true, $this->writeTokenCacheTime);
                             }
                             $uri = Zotero_API::getItemsURI($this->objectLibraryID);
                             $queryString = "itemKey=" . urlencode(implode(",", $keys)) . "&content=json";
                             if ($this->apiKey) {
                                 $queryString .= "&key=" . $this->apiKey;
                             }
                             $uri .= "?" . $queryString;
                             $this->responseCode = 201;
                             $this->queryParams = Zotero_API::parseQueryParams($queryString, $this->action, false);
                         }
                         $title = "Items";
                         $results = Zotero_Items::search($this->objectLibraryID, false, $this->queryParams, false, $formatAsKeys);
                     }
                 }
             }
             if (!empty($results)) {
                 if ($formatAsKeys) {
                     $responseKeys = $results['keys'];
                 } else {
                     $responseItems = $results['items'];
                 }
                 $totalResults = $results['total'];
             }
         }
         if ($this->queryParams['format'] == 'bib') {
             if (($itemIDs ? sizeOf($itemIDs) : $results['total']) > Zotero_API::$maxBibliographyItems) {
                 $this->e413("Cannot generate bibliography with more than " . Zotero_API::$maxBibliographyItems . " items");
             }
         }
         if ($itemIDs) {
             $this->queryParams['itemIDs'] = $itemIDs;
             $results = Zotero_Items::search($this->objectLibraryID, false, $this->queryParams, $includeTrashed, $formatAsKeys);
             if ($formatAsKeys) {
                 $responseKeys = $results['keys'];
             } else {
                 $responseItems = $results['items'];
             }
             $totalResults = $results['total'];
         } else {
             if (!isset($results)) {
                 if ($formatAsKeys) {
                     $responseKeys = array();
                 } else {
                     $responseItems = array();
                 }
                 $totalResults = 0;
             }
         }
         // Remove notes if not user and not public
         for ($i = 0; $i < sizeOf($responseItems); $i++) {
             if ($responseItems[$i]->isNote() && !$this->permissions->canAccess($responseItems[$i]->libraryID, 'notes')) {
                 array_splice($responseItems, $i, 1);
                 $totalResults--;
                 $i--;
             }
         }
         switch ($this->queryParams['format']) {
             case 'atom':
                 $this->responseXML = Zotero_Atom::createAtomFeed($this->getFeedNamePrefix($this->objectLibraryID) . $title, $this->uri, $responseItems, $totalResults, $this->queryParams, $this->apiVersion, $this->permissions);
                 break;
             case 'bib':
                 echo Zotero_Cite::getBibliographyFromCitationServer($responseItems, $this->queryParams['style'], $this->queryParams['css']);
                 exit;
             case 'csljson':
                 $json = Zotero_Cite::getJSONFromItems($responseItems, true);
                 if ($this->queryParams['pprint']) {
                     header("Content-Type: text/plain");
                     $json = Zotero_Utilities::json_encode_pretty($json);
                 } else {
                     header("Content-Type: application/vnd.citationstyles.csl+json");
                     $json = json_encode($json);
                 }
                 echo $json;
                 exit;
             case 'keys':
                 if (!$formatAsKeys) {
                     $responseKeys = array();
                     foreach ($responseItems as $item) {
                         $responseKeys[] = $item->key;
                     }
                 }
                 header("Content-Type: text/plain");
                 echo implode("\n", $responseKeys) . "\n";
                 exit;
             default:
                 $export = Zotero_Translate::doExport($responseItems, $this->queryParams['format']);
                 if ($this->queryParams['pprint']) {
                     header("Content-Type: text/plain");
                 } else {
                     header("Content-Type: " . $export['mimeType']);
                 }
                 echo $export['body'];
                 exit;
         }
     }
     $this->end();
 }
Exemplo n.º 3
0
 public function items()
 {
     // Check for general library access
     if (!$this->permissions->canAccess($this->objectLibraryID)) {
         $this->e403();
     }
     if ($this->isWriteMethod()) {
         // Check for library write access
         if (!$this->permissions->canWrite($this->objectLibraryID)) {
             $this->e403("Write access denied");
         }
         // Make sure library hasn't been modified
         if (!$this->singleObject) {
             $libraryTimestampChecked = $this->checkLibraryIfUnmodifiedSinceVersion();
         }
         // We don't update the library version in file mode, because currently
         // to avoid conflicts in the client the timestamp can't change
         // when the client updates file metadata
         if (!$this->fileMode) {
             Zotero_Libraries::updateVersionAndTimestamp($this->objectLibraryID);
         }
     }
     $itemIDs = array();
     $itemKeys = array();
     $results = array();
     $title = "";
     //
     // Single item
     //
     if ($this->singleObject) {
         if ($this->fileMode) {
             if ($this->fileView) {
                 $this->allowMethods(array('HEAD', 'GET', 'POST'));
             } else {
                 $this->allowMethods(array('HEAD', 'GET', 'PUT', 'POST', 'PATCH'));
             }
         } else {
             $this->allowMethods(array('HEAD', 'GET', 'PUT', 'PATCH', 'DELETE'));
         }
         if (!$this->objectLibraryID || !Zotero_ID::isValidKey($this->objectKey)) {
             $this->e404();
         }
         $item = Zotero_Items::getByLibraryAndKey($this->objectLibraryID, $this->objectKey);
         if ($item) {
             // If no access to the note, don't show that it exists
             if ($item->isNote() && !$this->permissions->canAccess($this->objectLibraryID, 'notes')) {
                 $this->e404();
             }
             // Make sure URL libraryID matches item libraryID
             if ($this->objectLibraryID != $item->libraryID) {
                 $this->e404("Item does not exist");
             }
             // File access mode
             if ($this->fileMode) {
                 $this->_handleFileRequest($item);
             }
             if ($this->scopeObject) {
                 switch ($this->scopeObject) {
                     // Remove item from collection
                     case 'collections':
                         $this->allowMethods(array('DELETE'));
                         $collection = Zotero_Collections::getByLibraryAndKey($this->objectLibraryID, $this->scopeObjectKey);
                         if (!$collection) {
                             $this->e404("Collection not found");
                         }
                         if (!$collection->hasItem($item->id)) {
                             $this->e404("Item not found in collection");
                         }
                         $collection->removeItem($item->id);
                         $this->e204();
                     default:
                         $this->e400();
                 }
             }
         } else {
             // Possibly temporary workaround to block unnecessary full syncs
             if ($this->fileMode && $this->httpAuth && $this->method == 'POST') {
                 // If > 2 requests for missing file, trigger a full sync via 404
                 $cacheKey = "apiMissingFile_" . $this->objectLibraryID . "_" . $this->objectKey;
                 $set = Z_Core::$MC->get($cacheKey);
                 if (!$set) {
                     Z_Core::$MC->set($cacheKey, 1, 86400);
                 } else {
                     if ($set < 2) {
                         Z_Core::$MC->increment($cacheKey);
                     } else {
                         Z_Core::$MC->delete($cacheKey);
                         $this->e404("A file sync error occurred. Please sync again.");
                     }
                 }
                 $this->e500("A file sync error occurred. Please sync again.");
             }
         }
         if ($this->isWriteMethod()) {
             $item = $this->handleObjectWrite('item', $item ? $item : null);
             if ($this->apiVersion < 2 && ($this->method == 'PUT' || $this->method == 'PATCH')) {
                 $this->queryParams['format'] = 'atom';
                 $this->queryParams['content'] = ['json'];
             }
         }
         if (!$item) {
             $this->e404("Item does not exist");
         }
         $this->libraryVersion = $item->version;
         if ($this->method == 'HEAD') {
             $this->end();
         }
         // Display item
         switch ($this->queryParams['format']) {
             case 'atom':
                 $this->responseXML = Zotero_Items::convertItemToAtom($item, $this->queryParams, $this->permissions);
                 break;
             case 'bib':
                 echo Zotero_Cite::getBibliographyFromCitationServer(array($item), $this->queryParams);
                 break;
             case 'csljson':
                 $json = Zotero_Cite::getJSONFromItems(array($item), true);
                 echo Zotero_Utilities::formatJSON($json);
                 break;
             case 'json':
                 $json = $item->toResponseJSON($this->queryParams, $this->permissions);
                 echo Zotero_Utilities::formatJSON($json);
                 break;
             default:
                 $export = Zotero_Translate::doExport(array($item), $this->queryParams['format']);
                 $this->queryParams['format'] = null;
                 header("Content-Type: " . $export['mimeType']);
                 echo $export['body'];
                 break;
         }
     } else {
         $this->allowMethods(array('HEAD', 'GET', 'POST', 'DELETE'));
         $this->libraryVersion = Zotero_Libraries::getUpdatedVersion($this->objectLibraryID);
         $includeTrashed = $this->queryParams['includeTrashed'];
         if ($this->scopeObject) {
             $this->allowMethods(array('GET', 'POST'));
             switch ($this->scopeObject) {
                 case 'collections':
                     // TEMP
                     if (Zotero_ID::isValidKey($this->scopeObjectKey)) {
                         $collection = Zotero_Collections::getByLibraryAndKey($this->objectLibraryID, $this->scopeObjectKey);
                     } else {
                         $collection = false;
                     }
                     if (!$collection) {
                         // If old collectionID, redirect
                         if ($this->method == 'GET' && Zotero_Utilities::isPosInt($this->scopeObjectKey)) {
                             $collection = Zotero_Collections::get($this->objectLibraryID, $this->scopeObjectKey);
                             if ($collection) {
                                 $qs = !empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '';
                                 $base = Zotero_API::getCollectionURI($collection);
                                 $this->redirect($base . "/items" . $qs, 301);
                             }
                         }
                         $this->e404("Collection not found");
                     }
                     // Add items to collection
                     if ($this->method == 'POST') {
                         $itemKeys = explode(' ', $this->body);
                         $itemIDs = array();
                         foreach ($itemKeys as $key) {
                             try {
                                 $item = Zotero_Items::getByLibraryAndKey($this->objectLibraryID, $key);
                             } catch (Exception $e) {
                                 if ($e->getCode() == Z_ERROR_OBJECT_LIBRARY_MISMATCH) {
                                     $item = false;
                                 } else {
                                     throw $e;
                                 }
                             }
                             if (!$item) {
                                 throw new Exception("Item '{$key}' not found in library", Z_ERROR_INVALID_INPUT);
                             }
                             if ($item->getSource()) {
                                 throw new Exception("Child items cannot be added to collections directly", Z_ERROR_INVALID_INPUT);
                             }
                             $itemIDs[] = $item->id;
                         }
                         $collection->addItems($itemIDs);
                         $this->e204();
                     }
                     if ($this->subset == 'top' || $this->apiVersion < 2) {
                         $title = "Top-Level Items in Collection ‘" . $collection->name . "’";
                         $itemIDs = $collection->getItems();
                     } else {
                         $title = "Items in Collection ‘" . $collection->name . "’";
                         $itemIDs = $collection->getItems(true);
                     }
                     break;
                 case 'tags':
                     if ($this->apiVersion >= 2) {
                         $this->e404();
                     }
                     $this->allowMethods(array('GET'));
                     $tagIDs = Zotero_Tags::getIDs($this->objectLibraryID, $this->scopeObjectName);
                     if (!$tagIDs) {
                         $this->e404("Tag not found");
                     }
                     foreach ($tagIDs as $tagID) {
                         $tag = new Zotero_Tag();
                         $tag->libraryID = $this->objectLibraryID;
                         $tag->id = $tagID;
                         // Use a real tag name, in case case differs
                         if (!$title) {
                             $title = "Items of Tag ‘" . $tag->name . "’";
                         }
                         $itemKeys = array_merge($itemKeys, $tag->getLinkedItems(true));
                     }
                     $itemKeys = array_unique($itemKeys);
                     break;
                 default:
                     $this->e404();
             }
         } else {
             // Top-level items
             if ($this->subset == 'top') {
                 $this->allowMethods(array('GET'));
                 $title = "Top-Level Items";
                 $results = Zotero_Items::search($this->objectLibraryID, true, $this->queryParams, $includeTrashed, $this->permissions);
             } else {
                 if ($this->subset == 'trash') {
                     $this->allowMethods(array('GET'));
                     $title = "Deleted Items";
                     $this->queryParams['trashedItemsOnly'] = true;
                     $includeTrashed = true;
                     $results = Zotero_Items::search($this->objectLibraryID, false, $this->queryParams, $includeTrashed, $this->permissions);
                 } else {
                     if ($this->subset == 'children') {
                         $item = Zotero_Items::getByLibraryAndKey($this->objectLibraryID, $this->objectKey);
                         if (!$item) {
                             $this->e404("Item not found");
                         }
                         if ($item->isAttachment()) {
                             $this->e400("/children cannot be called on attachment items");
                         }
                         if ($item->isNote()) {
                             $this->e400("/children cannot be called on note items");
                         }
                         if ($item->getSource()) {
                             $this->e400("/children cannot be called on child items");
                         }
                         // Create new child items
                         if ($this->method == 'POST') {
                             if ($this->apiVersion >= 2) {
                                 $this->allowMethods(array('GET'));
                             }
                             Zotero_DB::beginTransaction();
                             $obj = $this->jsonDecode($this->body);
                             $results = Zotero_Items::updateMultipleFromJSON($obj, $this->queryParams, $this->objectLibraryID, $this->userID, $this->permissions, $libraryTimestampChecked ? 0 : 1, $item);
                             Zotero_DB::commit();
                             if ($cacheKey = $this->getWriteTokenCacheKey()) {
                                 Z_Core::$MC->set($cacheKey, true, $this->writeTokenCacheTime);
                             }
                             $uri = Zotero_API::getItemsURI($this->objectLibraryID);
                             $keys = array_merge(get_object_vars($results['success']), get_object_vars($results['unchanged']));
                             $queryString = "itemKey=" . urlencode(implode(",", $keys)) . "&format=atom&content=json&order=itemKeyList&sort=asc";
                             if ($this->apiKey) {
                                 $queryString .= "&key=" . $this->apiKey;
                             }
                             $uri .= "?" . $queryString;
                             $this->queryParams = Zotero_API::parseQueryParams($queryString, $this->action, false);
                             $this->responseCode = 201;
                             $title = "Items";
                             $results = Zotero_Items::search($this->objectLibraryID, false, $this->queryParams, $includeTrashed, $this->permissions);
                         } else {
                             $title = "Child Items of ‘" . $item->getDisplayTitle() . "’";
                             $notes = $item->getNotes();
                             $attachments = $item->getAttachments();
                             $itemIDs = array_merge($notes, $attachments);
                         }
                     } else {
                         // Create new items
                         if ($this->method == 'POST') {
                             $this->queryParams['format'] = 'writereport';
                             $obj = $this->jsonDecode($this->body);
                             // Server-side translation
                             if (isset($obj->url)) {
                                 if ($this->apiVersion == 1) {
                                     Zotero_DB::beginTransaction();
                                 }
                                 $token = $this->getTranslationToken($obj);
                                 $results = Zotero_Items::addFromURL($obj, $this->queryParams, $this->objectLibraryID, $this->userID, $this->permissions, $token);
                                 if ($this->apiVersion == 1) {
                                     Zotero_DB::commit();
                                 }
                                 // Multiple choices
                                 if ($results instanceof stdClass) {
                                     $this->queryParams['format'] = null;
                                     header("Content-Type: application/json");
                                     if ($this->queryParams['v'] >= 2) {
                                         echo Zotero_Utilities::formatJSON(['url' => $obj->url, 'token' => $token, 'items' => $results->select]);
                                     } else {
                                         echo Zotero_Utilities::formatJSON($results->select);
                                     }
                                     $this->e300();
                                 } else {
                                     if (is_int($results)) {
                                         switch ($results) {
                                             case 501:
                                                 $this->e501("No translators found for URL");
                                                 break;
                                             default:
                                                 $this->e500("Error translating URL");
                                         }
                                     } else {
                                         if ($this->apiVersion == 1) {
                                             $uri = Zotero_API::getItemsURI($this->objectLibraryID);
                                             $keys = array_merge(get_object_vars($results['success']), get_object_vars($results['unchanged']));
                                             $queryString = "itemKey=" . urlencode(implode(",", $keys)) . "&format=atom&content=json&order=itemKeyList&sort=asc";
                                             if ($this->apiKey) {
                                                 $queryString .= "&key=" . $this->apiKey;
                                             }
                                             $uri .= "?" . $queryString;
                                             $this->queryParams = Zotero_API::parseQueryParams($queryString, $this->action, false);
                                             $this->responseCode = 201;
                                             $title = "Items";
                                             $results = Zotero_Items::search($this->objectLibraryID, false, $this->queryParams, $includeTrashed, $this->permissions);
                                         }
                                     }
                                 }
                                 // Otherwise return write status report
                             } else {
                                 if ($this->apiVersion < 2) {
                                     Zotero_DB::beginTransaction();
                                 }
                                 $results = Zotero_Items::updateMultipleFromJSON($obj, $this->queryParams, $this->objectLibraryID, $this->userID, $this->permissions, $libraryTimestampChecked ? 0 : 1, null);
                                 if ($this->apiVersion < 2) {
                                     Zotero_DB::commit();
                                     $uri = Zotero_API::getItemsURI($this->objectLibraryID);
                                     $keys = array_merge(get_object_vars($results['success']), get_object_vars($results['unchanged']));
                                     $queryString = "itemKey=" . urlencode(implode(",", $keys)) . "&format=atom&content=json&order=itemKeyList&sort=asc";
                                     if ($this->apiKey) {
                                         $queryString .= "&key=" . $this->apiKey;
                                     }
                                     $uri .= "?" . $queryString;
                                     $this->queryParams = Zotero_API::parseQueryParams($queryString, $this->action, false);
                                     $this->responseCode = 201;
                                     $title = "Items";
                                     $results = Zotero_Items::search($this->objectLibraryID, false, $this->queryParams, $includeTrashed, $this->permissions);
                                 }
                             }
                             if ($cacheKey = $this->getWriteTokenCacheKey()) {
                                 Z_Core::$MC->set($cacheKey, true, $this->writeTokenCacheTime);
                             }
                         } else {
                             if ($this->method == 'DELETE') {
                                 Zotero_DB::beginTransaction();
                                 foreach ($this->queryParams['itemKey'] as $itemKey) {
                                     Zotero_Items::delete($this->objectLibraryID, $itemKey);
                                 }
                                 Zotero_DB::commit();
                                 $this->e204();
                             } else {
                                 $title = "Items";
                                 $results = Zotero_Items::search($this->objectLibraryID, false, $this->queryParams, $includeTrashed, $this->permissions);
                             }
                         }
                     }
                 }
             }
         }
         if ($itemIDs || $itemKeys) {
             if ($itemIDs) {
                 $this->queryParams['itemIDs'] = $itemIDs;
             }
             if ($itemKeys) {
                 $this->queryParams['itemKey'] = $itemKeys;
             }
             $results = Zotero_Items::search($this->objectLibraryID, false, $this->queryParams, $includeTrashed, $this->permissions);
         }
         if ($this->queryParams['format'] == 'bib') {
             $maxBibItems = Zotero_API::MAX_BIBLIOGRAPHY_ITEMS;
             if ($results['total'] > $maxBibItems) {
                 $this->e413("Cannot generate bibliography with more than {$maxBibItems} items");
             }
         }
         $this->generateMultiResponse($results, $title);
     }
     $this->end();
 }