public function testChangeItemTypeByLibraryAndKey() { $item = new Zotero_Item(2); $item->libraryID = self::$config['userLibraryID']; $item->save(); $key = $item->key; $this->assertEquals(2, $item->itemTypeID); $item = Zotero_Items::getByLibraryAndKey($item->libraryID, $item->key); $item->itemTypeID = 3; $item->save(); $this->assertEquals(3, $item->itemTypeID); }
public function deleted() { if ($this->apiVersion < 2) { $this->e404(); } $this->allowMethods(array('GET')); if (!$this->permissions->canAccess($this->objectLibraryID)) { $this->e403(); } $this->libraryVersion = Zotero_Libraries::getUpdatedVersion($this->objectLibraryID); // TEMP: sync transition if ($this->queryParams['sincetime'] !== null) { $deleted = array("collections" => Zotero_Collections::getDeleteLogKeys($this->objectLibraryID, $this->queryParams['sincetime'], true), "items" => Zotero_Items::getDeleteLogKeys($this->objectLibraryID, $this->queryParams['sincetime'], true), "searches" => Zotero_Searches::getDeleteLogKeys($this->objectLibraryID, $this->queryParams['sincetime'], true), "tags" => Zotero_Tags::getDeleteLogKeys($this->objectLibraryID, $this->queryParams['sincetime'], true), "settings" => Zotero_Settings::getDeleteLogKeys($this->objectLibraryID, $this->queryParams['sincetime'], true)); echo Zotero_Utilities::formatJSON($deleted); $this->end(); } if ($this->queryParams['since'] === null) { $this->e400("'since' parameter must be provided"); } $deleted = array("collections" => Zotero_Collections::getDeleteLogKeys($this->objectLibraryID, $this->queryParams['since']), "items" => Zotero_Items::getDeleteLogKeys($this->objectLibraryID, $this->queryParams['since']), "searches" => Zotero_Searches::getDeleteLogKeys($this->objectLibraryID, $this->queryParams['since']), "tags" => Zotero_Tags::getDeleteLogKeys($this->objectLibraryID, $this->queryParams['since']), "settings" => Zotero_Settings::getDeleteLogKeys($this->objectLibraryID, $this->queryParams['since'])); echo Zotero_Utilities::formatJSON($deleted); $this->end(); }
/** * Add an item to the cached child items list if loaded */ public function registerChildItem($itemID) { if ($this->loaded['childItems']) { $item = Zotero_Items::get($this->libraryID, $itemID); if ($item) { $this->_hasChildItems = true; $this->childItems[] = $item; } } }
public function getLinkedItems() { if (!$this->id) { return array(); } $items = array(); $sql = "SELECT itemID FROM itemCreators WHERE creatorID=?"; $itemIDs = Zotero_DB::columnQuery($sql, $this->id, Zotero_Shards::getByLibraryID($this->libraryID)); if (!$itemIDs) { return $items; } foreach ($itemIDs as $itemID) { $items[] = Zotero_Items::get($this->libraryID, $itemID); } return $items; }
/** * @param SimpleXMLElement $xml Data necessary for delete as SimpleXML element * @return void */ public static function deleteFromXML(SimpleXMLElement $xml, $userID) { $parents = array(); foreach ($xml->children() as $obj) { $libraryID = (int) $obj['libraryID']; $key = (string) $obj['key']; if ($userID && !Zotero_Libraries::userCanEdit($libraryID, $userID)) { throw new Exception("Cannot edit " . self::$objectType . " in library {$libraryID}", Z_ERROR_LIBRARY_ACCESS_DENIED); } if ($obj->getName() == 'item') { $item = Zotero_Items::getByLibraryAndKey($libraryID, $key); if (!$item) { continue; } if (!$item->getSource()) { $parents[] = array('libraryID' => $libraryID, 'key' => $key); continue; } } self::delete($libraryID, $key); } foreach ($parents as $obj) { self::delete($obj['libraryID'], $obj['key']); } }
private function loadLinkedItems() { Z_Core::debug("Loading linked items for tag {$this->id}"); if (!$this->id && !$this->key) { $this->linkedItemsLoaded = true; return; } if (!$this->loaded) { $this->load(); } if (!$this->id) { $this->linkedItemsLoaded = true; return; } $sql = "SELECT itemID FROM itemTags WHERE tagID=?"; $stmt = Zotero_DB::getStatement($sql, true, Zotero_Shards::getByLibraryID($this->libraryID)); $ids = Zotero_DB::columnQueryFromStatement($stmt, $this->id); $this->linkedItems = array(); if ($ids) { $this->linkedItems = Zotero_Items::get($this->libraryID, $ids); } $this->linkedItemsLoaded = true; }
public static function indexFromXML(DOMElement $xml, $userID) { if ($xml->textContent === "") { error_log("Skipping empty full-text content for item " . $xml->getAttribute('libraryID') . "/" . $xml->getAttribute('key')); return; } $item = Zotero_Items::getByLibraryAndKey($xml->getAttribute('libraryID'), $xml->getAttribute('key')); if (!$item) { error_log("Item " . $xml->getAttribute('libraryID') . "/" . $xml->getAttribute('key') . " not found during full-text indexing"); return; } if (!Zotero_Libraries::userCanEdit($item->libraryID, $userID)) { error_log("Skipping full-text content from user {$userID} for uneditable item " . $xml->getAttribute('libraryID') . "/" . $xml->getAttribute('key')); return; } $stats = array(); foreach (self::$metadata as $prop) { $val = $xml->getAttribute($prop); $stats[$prop] = $val; } self::indexItem($item, $xml->textContent, $stats); }
public function itemContent() { $this->allowMethods(array('GET', 'PUT')); // Check for general library access if (!$this->permissions->canAccess($this->objectLibraryID)) { $this->e403(); } if (!$this->singleObject) { $this->e404(); } if ($this->isWriteMethod()) { // Check for library write access if (!$this->permissions->canWrite($this->objectLibraryID)) { $this->e403("Write access denied"); } Zotero_Libraries::updateVersionAndTimestamp($this->objectLibraryID); } $item = Zotero_Items::getByLibraryAndKey($this->objectLibraryID, $this->objectKey); if (!$item) { $this->e404(); } // If no access to the note, don't show that it exists if ($item->isNote() && !$this->permissions->canAccess($this->objectLibraryID, 'notes')) { $this->e404(); } if (!$item->isAttachment() || Zotero_Attachments::linkModeNumberToName($item->attachmentLinkMode) == 'LINKED_URL') { $this->e404(); } if ($this->isWriteMethod()) { $this->libraryVersion = Zotero_Libraries::getUpdatedVersion($this->objectLibraryID); if ($this->method == 'PUT') { $this->requireContentType("application/json"); $json = json_decode($this->body, true); if (!$json) { $this->e400("PUT data is not valid JSON"); } $stats = []; foreach (Zotero_FullText::$metadata as $prop) { if (isset($json[$prop])) { $stats[$prop] = $json[$prop]; } } Zotero_FullText::indexItem($item, $json['content'], $stats); $this->e204(); } else { $this->e405(); } } $data = Zotero_FullText::getItemData($item->libraryID, $item->key); if (!$data) { $this->e404(); } $this->libraryVersion = $data['version']; $json = ["content" => $data['content']]; foreach (Zotero_FullText::$metadata as $prop) { if (!empty($data[$prop])) { $json[$prop] = $data[$prop]; } } echo Zotero_Utilities::formatJSON($json); $this->end(); }
public function addItem($itemID) { if (!Zotero_Items::get($this->libraryID, $itemID)) { throw new Exception("Item does not exist"); } if ($this->hasItem($itemID)) { Z_Core::debug("Item {$itemID} is already a child of collection {$this->id}"); return; } $this->setChildItems(array_merge($this->getChildItems(), array($itemID))); }
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(); }
public static function createAtomFeed($action, $title, $url, $entries, $totalResults = null, $queryParams = [], Zotero_Permissions $permissions = null, $fixedValues = array()) { if ($queryParams) { $nonDefaultParams = Zotero_API::getNonDefaultParams($action, $queryParams); } else { $nonDefaultParams = []; } $feed = '<?xml version="1.0" encoding="UTF-8"?>' . '<feed xmlns="' . Zotero_Atom::$nsAtom . '" ' . 'xmlns:zapi="' . Zotero_Atom::$nsZoteroAPI . '"/>'; $xml = new SimpleXMLElement($feed); $xml->title = $title; $path = parse_url($url, PHP_URL_PATH); // Generate canonical URI $zoteroURI = Zotero_URI::getBaseURI() . substr($path, 1) . Zotero_API::buildQueryString($queryParams['v'], $action, $nonDefaultParams, ['v']); $baseURI = Zotero_API::getBaseURI() . substr($path, 1); // API version isn't included in URLs (as with the API key) // // It could alternatively be made a private parameter so that it didn't appear // in the Link header either, but for now it's still there. $excludeParams = ['v']; $links = Zotero_API::buildLinks($action, $path, $totalResults, $queryParams, $nonDefaultParams, $excludeParams); $xml->id = $zoteroURI; $link = $xml->addChild("link"); $link['rel'] = "self"; $link['type'] = "application/atom+xml"; $link['href'] = $links['self']; $link = $xml->addChild("link"); $link['rel'] = "first"; $link['type'] = "application/atom+xml"; $link['href'] = $links['first']; if (isset($links['next'])) { $link = $xml->addChild("link"); $link['rel'] = "next"; $link['type'] = "application/atom+xml"; $link['href'] = $links['next']; } $link = $xml->addChild("link"); $link['rel'] = "last"; $link['type'] = "application/atom+xml"; $link['href'] = $links['last']; // Generate alternate URI $link = $xml->addChild("link"); $link['rel'] = "alternate"; $link['type'] = "text/html"; $link['href'] = $links['alternate']; if ($queryParams['v'] < 3) { $xml->addChild("zapi:totalResults", is_numeric($totalResults) ? $totalResults : sizeOf($entries), self::$nsZoteroAPI); } if ($queryParams['v'] < 2) { $xml->addChild("zapi:apiVersion", 1, self::$nsZoteroAPI); } $latestUpdated = ''; // Check memcached for bib data $sharedData = array(); if ($entries && $entries[0] instanceof Zotero_Item) { if (in_array('citation', $queryParams['content'])) { $sharedData["citation"] = Zotero_Cite::multiGetFromMemcached("citation", $entries, $queryParams); } if (in_array('bib', $queryParams['content'])) { $sharedData["bib"] = Zotero_Cite::multiGetFromMemcached("bib", $entries, $queryParams); } } $xmlEntries = array(); foreach ($entries as $entry) { if ($entry->dateModified > $latestUpdated) { $latestUpdated = $entry->dateModified; } if ($entry instanceof SimpleXMLElement) { $xmlEntries[] = $entry; } else { if ($entry instanceof Zotero_Collection) { $entry = Zotero_Collections::convertCollectionToAtom($entry, $queryParams); $xmlEntries[] = $entry; } else { if ($entry instanceof Zotero_Item) { $entry = Zotero_Items::convertItemToAtom($entry, $queryParams, $permissions, $sharedData); $xmlEntries[] = $entry; } else { if ($entry instanceof Zotero_Search) { $entry = $entry->toAtom($queryParams); $xmlEntries[] = $entry; } else { if ($entry instanceof Zotero_Tag) { $xmlEntries[] = $entry->toAtom($queryParams, isset($fixedValues[$entry->id]) ? $fixedValues[$entry->id] : null); } else { if ($entry instanceof Zotero_Group) { $entry = $entry->toAtom($queryParams); $xmlEntries[] = $entry; } } } } } } } if ($latestUpdated) { $xml->updated = Zotero_Date::sqlToISO8601($latestUpdated); } else { $xml->updated = str_replace("+00:00", "Z", date('c')); } // Import object XML nodes into document $doc = dom_import_simplexml($xml); foreach ($xmlEntries as $xmlEntry) { $subNode = dom_import_simplexml($xmlEntry); $importedNode = $doc->ownerDocument->importNode($subNode, true); $doc->appendChild($importedNode); } return $xml; }
public function testGetLongDataValueFromXML() { $longStr = str_pad("", 65534, "-") . "\nFoobar"; $xml = <<<EOD \t\t\t<data> \t\t\t\t<items> \t\t\t\t\t<item libraryID="1" key="BBBBBBBB" itemType="attachment" dateAdded="2010-01-08 10:31:09" dateModified="2010-01-08 10:31:17" sourceItem="VN9DPHBB" linkMode="0" mimeType="application/pdf" storageModTime="1262946676" storageHash="41125f70cc25117b0da961bd7108938 9"> \t\t\t\t\t\t<field name="title">Test_Filename.pdf</field> \t\t\t\t\t</item> \t\t\t\t\t<item libraryID="1" key="AAAAAAAA" itemType="journalArticle" dateAdded="2010-01-08 10:29:36" dateModified="2010-01-08 10:29:36"> \t\t\t\t\t\t<field name="title">Foo</field> \t\t\t\t\t\t<field name="abstractNote">{$longStr}</field> \t\t\t\t\t\t<creator libraryID="1" key="AAAAAAAA" creatorType="author" index="0"> \t\t\t\t\t\t\t<creator libraryID="1" key="AAAAAAAA" dateAdded="2010-01-08 10:29:36" dateModified="2010-01-08 10:29:36"> \t\t\t\t\t\t\t\t<firstName>Irrelevant</firstName> \t\t\t\t\t\t\t\t<lastName>Creator</lastName> \t\t\t\t\t\t\t</creator> \t\t\t\t\t\t</creator> \t\t\t\t\t</item> \t\t\t\t</items> \t\t\t</data> EOD; $doc = new DOMDocument(); $doc->loadXML($xml); $node = Zotero_Items::getLongDataValueFromXML($doc); $this->assertEquals("abstractNote", $node->getAttribute('name')); $this->assertEquals($longStr, $node->nodeValue); }
public static function createAtomFeed($title, $url, $entries, $totalResults = null, $queryParams = null, $apiVersion = null, $permissions = null, $fixedValues = array()) { if ($queryParams) { $nonDefaultParams = Zotero_API::getNonDefaultQueryParams($queryParams); // Convert 'content' array to sorted comma-separated string if (isset($nonDefaultParams['content'])) { $nonDefaultParams['content'] = implode(',', $nonDefaultParams['content']); } } else { $nonDefaultParams = array(); } $feed = '<feed xmlns="' . Zotero_Atom::$nsAtom . '" ' . 'xmlns:zapi="' . Zotero_Atom::$nsZoteroAPI . '"'; if ($queryParams && $queryParams['content'][0] == 'full') { $feed .= ' xmlns:zxfer="' . Zotero_Atom::$nsZoteroTransfer . '"'; } $feed .= '/>'; $xml = new SimpleXMLElement($feed); $xml->title = $title; $path = parse_url($url, PHP_URL_PATH); // Generate canonical URI $zoteroURI = Zotero_URI::getBaseURI() . substr($path, 1); if ($nonDefaultParams) { $zoteroURI .= "?" . http_build_query($nonDefaultParams); } $atomURI = Zotero_Atom::getBaseURI() . substr($path, 1); // // Generate URIs for 'self', 'first', 'next' and 'last' links // // 'self' $atomSelfURI = $atomURI; if ($nonDefaultParams) { $atomSelfURI .= "?" . http_build_query($nonDefaultParams); } // 'first' $atomFirstURI = $atomURI; if ($nonDefaultParams) { $p = $nonDefaultParams; unset($p['start']); if ($first = http_build_query($p)) { $atomFirstURI .= "?" . $first; } } // 'last' if (!$queryParams['start'] && $queryParams['limit'] >= $totalResults) { $atomLastURI = $atomSelfURI; } else { // 'start' past results if ($queryParams['start'] >= $totalResults) { $lastStart = $totalResults - $queryParams['limit']; } else { $lastStart = $totalResults - $totalResults % $queryParams['limit']; if ($lastStart == $totalResults) { $lastStart = $totalResults - $queryParams['limit']; } } $p = $nonDefaultParams; if ($lastStart > 0) { $p['start'] = $lastStart; } else { unset($p['start']); } $atomLastURI = $atomURI; if ($last = http_build_query($p)) { $atomLastURI .= "?" . $last; } // 'next' $nextStart = $queryParams['start'] + $queryParams['limit']; if ($nextStart < $totalResults) { $p = $nonDefaultParams; $p['start'] = $nextStart; $atomNextURI = $atomURI . "?" . http_build_query($p); } } $xml->id = $zoteroURI; $link = $xml->addChild("link"); $link['rel'] = "self"; $link['type'] = "application/atom+xml"; $link['href'] = $atomSelfURI; $link = $xml->addChild("link"); $link['rel'] = "first"; $link['type'] = "application/atom+xml"; $link['href'] = $atomFirstURI; if (isset($atomNextURI)) { $link = $xml->addChild("link"); $link['rel'] = "next"; $link['type'] = "application/atom+xml"; $link['href'] = $atomNextURI; } $link = $xml->addChild("link"); $link['rel'] = "last"; $link['type'] = "application/atom+xml"; $link['href'] = $atomLastURI; // Generate alternate URI $alternateURI = Zotero_URI::getBaseURI() . substr($path, 1); if ($nonDefaultParams) { $p = $nonDefaultParams; if (isset($p['content'])) { unset($p['content']); } if ($p) { $alternateURI .= "?" . http_build_query($p); } } $link = $xml->addChild("link"); $link['rel'] = "alternate"; $link['type'] = "text/html"; $link['href'] = $alternateURI; $xml->addChild("zapi:totalResults", is_numeric($totalResults) ? $totalResults : sizeOf($entries), self::$nsZoteroAPI); $xml->addChild("zapi:apiVersion", $apiVersion, self::$nsZoteroAPI); $latestUpdated = ''; // Get bib data using parallel requests $sharedData = array(); if ($entries && $entries[0] instanceof Zotero_Item) { if (in_array('citation', $queryParams['content'])) { $sharedData["citation"] = Zotero_Cite::multiGetFromCiteServer("citation", $entries, $queryParams['style']); } if (in_array('bib', $queryParams['content'])) { $sharedData["bib"] = Zotero_Cite::multiGetFromCiteServer("bib", $entries, $queryParams['style']); } } $xmlEntries = array(); foreach ($entries as $entry) { if ($entry->dateModified > $latestUpdated) { $latestUpdated = $entry->dateModified; } if ($entry instanceof SimpleXMLElement) { $xmlEntries[] = $entry; } else { if ($entry instanceof Zotero_Collection) { $entry = Zotero_Collections::convertCollectionToAtom($entry, $queryParams['content'], $apiVersion); $xmlEntries[] = $entry; } else { if ($entry instanceof Zotero_Creator) { $entry = Zotero_Creators::convertCreatorToAtom($entry, $queryParams['content'], $apiVersion); $xmlEntries[] = $entry; } else { if ($entry instanceof Zotero_Item) { $entry = Zotero_Items::convertItemToAtom($entry, $queryParams, $apiVersion, $permissions, $sharedData); $xmlEntries[] = $entry; } else { if ($entry instanceof Zotero_Search) { $entry = Zotero_Searches::convertSearchToAtom($entry, $queryParams['content'], $apiVersion); $xmlEntries[] = $entry; } else { if ($entry instanceof Zotero_Tag) { $xmlEntries[] = $entry->toAtom($queryParams['content'], $apiVersion, isset($fixedValues[$entry->id]) ? $fixedValues[$entry->id] : null); } else { if ($entry instanceof Zotero_Group) { $entry = $entry->toAtom($queryParams['content'], $apiVersion); $xmlEntries[] = $entry; } } } } } } } } if ($latestUpdated) { $xml->updated = Zotero_Date::sqlToISO8601($latestUpdated); } else { $xml->updated = str_replace("+00:00", "Z", date('c')); } // Import object XML nodes into document $doc = dom_import_simplexml($xml); foreach ($xmlEntries as $xmlEntry) { $subNode = dom_import_simplexml($xmlEntry); $importedNode = $doc->ownerDocument->importNode($subNode, true); $doc->appendChild($importedNode); } return $xml; }
/** * @param SimpleXMLElement $xml Data necessary for delete as SimpleXML element * @return void */ public static function deleteFromXML(SimpleXMLElement $xml) { $parents = array(); foreach ($xml->children() as $obj) { $libraryID = (int) $obj['libraryID']; $key = (string) $obj['key']; if ($obj->getName() == 'item') { $item = Zotero_Items::getByLibraryAndKey($libraryID, $key); if (!$item) { continue; } if (!$item->getSource()) { $parents[] = array('libraryID' => $libraryID, 'key' => $key); continue; } } static::delete($libraryID, $key); } foreach ($parents as $obj) { static::delete($obj['libraryID'], $obj['key']); } }
public function setLinkedItems($newKeys) { if (!$this->linkedItemsLoaded) { $this->loadLinkedItems(); } if (!is_array($newKeys)) { throw new Exception('$newKeys must be an array'); } $oldKeys = $this->getLinkedItems(true); if (!$newKeys && !$oldKeys) { Z_Core::debug("No linked items added", 4); return false; } $addKeys = array_diff($newKeys, $oldKeys); $removeKeys = array_diff($oldKeys, $newKeys); // Make sure all new keys exist foreach ($addKeys as $key) { if (!Zotero_Items::existsByLibraryAndKey($this->libraryID, $key)) { // Return a specific error for a wrong-library tag issue // that I can't reproduce throw new Exception("Linked item {$key} of tag " . "{$this->libraryID}/{$this->key} not found", Z_ERROR_TAG_LINKED_ITEM_NOT_FOUND); } } if ($addKeys || $removeKeys) { $this->prepFieldChange('linkedItems'); } else { Z_Core::debug('Linked items not changed', 4); return false; } $this->linkedItems = $newKeys; return true; }
private static function processUploadInternal($userID, SimpleXMLElement $xml, $syncQueueID = null, $syncProcessID = null) { $userLibraryID = Zotero_Users::getLibraryIDFromUserID($userID); $affectedLibraries = self::parseAffectedLibraries($xml->asXML()); // Relations-only uploads don't have affected libraries if (!$affectedLibraries) { $affectedLibraries = array(Zotero_Users::getLibraryIDFromUserID($userID)); } $processID = self::addUploadProcess($userID, $affectedLibraries, $syncQueueID, $syncProcessID); set_time_limit(5400); $profile = false; if ($profile) { $shardID = Zotero_Shards::getByUserID($userID); Zotero_DB::profileStart($shardID); } try { Zotero_DB::beginTransaction(); // Mark libraries as updated foreach ($affectedLibraries as $libraryID) { Zotero_Libraries::updateVersion($libraryID); } $timestamp = Zotero_Libraries::updateTimestamps($affectedLibraries); Zotero_DB::registerTransactionTimestamp($timestamp); // Make sure no other upload sessions use this same timestamp // for any of these libraries, since we return >= 1 as the next // last sync time if (!Zotero_Libraries::setTimestampLock($affectedLibraries, $timestamp)) { throw new Exception("Library timestamp already used", Z_ERROR_LIBRARY_TIMESTAMP_ALREADY_USED); } $modifiedItems = array(); // Add/update creators if ($xml->creators) { // DOM $keys = array(); $xmlElements = dom_import_simplexml($xml->creators); $xmlElements = $xmlElements->getElementsByTagName('creator'); Zotero_DB::query("SET foreign_key_checks = 0"); try { $addedLibraryIDs = array(); $addedCreatorDataHashes = array(); foreach ($xmlElements as $xmlElement) { $key = $xmlElement->getAttribute('key'); if (isset($keys[$key])) { throw new Exception("Creator {$key} already processed"); } $keys[$key] = true; $creatorObj = Zotero_Creators::convertXMLToCreator($xmlElement); if (Zotero_Utilities::unicodeTrim($creatorObj->firstName) === '' && Zotero_Utilities::unicodeTrim($creatorObj->lastName) === '') { continue; } $addedLibraryIDs[] = $creatorObj->libraryID; $changed = $creatorObj->save($userID); // If the creator changed, we need to update all linked items if ($changed) { $modifiedItems = array_merge($modifiedItems, $creatorObj->getLinkedItems()); } } } catch (Exception $e) { Zotero_DB::query("SET foreign_key_checks = 1"); throw $e; } Zotero_DB::query("SET foreign_key_checks = 1"); unset($keys); unset($xml->creators); // // Manual foreign key checks // // libraryID foreach (array_unique($addedLibraryIDs) as $addedLibraryID) { $shardID = Zotero_Shards::getByLibraryID($addedLibraryID); $sql = "SELECT COUNT(*) FROM shardLibraries WHERE libraryID=?"; if (!Zotero_DB::valueQuery($sql, $addedLibraryID, $shardID)) { throw new Exception("libraryID inserted into `creators` not found in `shardLibraries` ({$addedLibraryID}, {$shardID})"); } } } // Add/update items $savedItems = array(); if ($xml->items) { $childItems = array(); // DOM $xmlElements = dom_import_simplexml($xml->items); $xmlElements = $xmlElements->getElementsByTagName('item'); foreach ($xmlElements as $xmlElement) { $libraryID = (int) $xmlElement->getAttribute('libraryID'); $key = $xmlElement->getAttribute('key'); if (isset($savedItems[$libraryID . "/" . $key])) { throw new Exception("Item {$libraryID}/{$key} already processed"); } $itemObj = Zotero_Items::convertXMLToItem($xmlElement); if (!$itemObj->getSourceKey()) { try { $modified = $itemObj->save($userID); if ($modified) { $savedItems[$libraryID . "/" . $key] = true; } } catch (Exception $e) { if (strpos($e->getMessage(), 'libraryIDs_do_not_match') !== false) { throw new Exception($e->getMessage() . " ({$key})"); } throw $e; } } else { $childItems[] = $itemObj; } } unset($xml->items); while ($childItem = array_shift($childItems)) { $libraryID = $childItem->libraryID; $key = $childItem->key; if (isset($savedItems[$libraryID . "/" . $key])) { throw new Exception("Item {$libraryID}/{$key} already processed"); } $modified = $childItem->save($userID); if ($modified) { $savedItems[$libraryID . "/" . $key] = true; } } } // Add/update collections if ($xml->collections) { $collections = array(); $collectionSets = array(); // DOM // Build an array of unsaved collection objects and the keys of child items $keys = array(); $xmlElements = dom_import_simplexml($xml->collections); $xmlElements = $xmlElements->getElementsByTagName('collection'); foreach ($xmlElements as $xmlElement) { $key = $xmlElement->getAttribute('key'); if (isset($keys[$key])) { throw new Exception("Collection {$key} already processed"); } $keys[$key] = true; $collectionObj = Zotero_Collections::convertXMLToCollection($xmlElement); $xmlItems = $xmlElement->getElementsByTagName('items')->item(0); // Fix an error if there's leading or trailing whitespace, // which was possible in 2.0.3 if ($xmlItems) { $xmlItems = trim($xmlItems->nodeValue); } $arr = array('obj' => $collectionObj, 'items' => $xmlItems ? explode(' ', $xmlItems) : array()); $collections[] = $collectionObj; $collectionSets[] = $arr; } unset($keys); unset($xml->collections); self::saveCollections($collections, $userID); unset($collections); // Set child items foreach ($collectionSets as $collection) { // Child items if (isset($collection['items'])) { $ids = array(); foreach ($collection['items'] as $key) { $item = Zotero_Items::getByLibraryAndKey($collection['obj']->libraryID, $key); if (!$item) { throw new Exception("Child item '{$key}' of collection {$collection['obj']->id} not found", Z_ERROR_ITEM_NOT_FOUND); } $ids[] = $item->id; } $collection['obj']->setItems($ids); } } unset($collectionSets); } // Add/update saved searches if ($xml->searches) { $searches = array(); $keys = array(); foreach ($xml->searches->search as $xmlElement) { $key = (string) $xmlElement['key']; if (isset($keys[$key])) { throw new Exception("Search {$key} already processed"); } $keys[$key] = true; $searchObj = Zotero_Searches::convertXMLToSearch($xmlElement); $searchObj->save($userID); } unset($xml->searches); } // Add/update tags if ($xml->tags) { $keys = array(); // DOM $xmlElements = dom_import_simplexml($xml->tags); $xmlElements = $xmlElements->getElementsByTagName('tag'); foreach ($xmlElements as $xmlElement) { // TEMP $tagItems = $xmlElement->getElementsByTagName('items'); if ($tagItems->length && $tagItems->item(0)->nodeValue == "") { error_log("Skipping tag with no linked items"); continue; } $libraryID = (int) $xmlElement->getAttribute('libraryID'); $key = $xmlElement->getAttribute('key'); $lk = $libraryID . "/" . $key; if (isset($keys[$lk])) { throw new Exception("Tag {$lk} already processed"); } $keys[$lk] = true; $itemKeysToUpdate = array(); $tagObj = Zotero_Tags::convertXMLToTag($xmlElement, $itemKeysToUpdate); // We need to update removed items, added items, and, // if the tag itself has changed, existing items $modifiedItems = array_merge($modifiedItems, array_map(function ($key) use($libraryID) { return $libraryID . "/" . $key; }, $itemKeysToUpdate)); $tagObj->save($userID, true); } unset($keys); unset($xml->tags); } // Add/update relations if ($xml->relations) { // DOM $xmlElements = dom_import_simplexml($xml->relations); $xmlElements = $xmlElements->getElementsByTagName('relation'); foreach ($xmlElements as $xmlElement) { $relationObj = Zotero_Relations::convertXMLToRelation($xmlElement, $userLibraryID); if ($relationObj->exists()) { continue; } $relationObj->save($userID); } unset($keys); unset($xml->relations); } // Add/update settings if ($xml->settings) { // DOM $xmlElements = dom_import_simplexml($xml->settings); $xmlElements = $xmlElements->getElementsByTagName('setting'); foreach ($xmlElements as $xmlElement) { $settingObj = Zotero_Settings::convertXMLToSetting($xmlElement); $settingObj->save($userID); } unset($xml->settings); } if ($xml->fulltexts) { // DOM $xmlElements = dom_import_simplexml($xml->fulltexts); $xmlElements = $xmlElements->getElementsByTagName('fulltext'); foreach ($xmlElements as $xmlElement) { Zotero_FullText::indexFromXML($xmlElement, $userID); } unset($xml->fulltexts); } // TODO: loop if ($xml->deleted) { // Delete collections if ($xml->deleted->collections) { Zotero_Collections::deleteFromXML($xml->deleted->collections, $userID); } // Delete items if ($xml->deleted->items) { Zotero_Items::deleteFromXML($xml->deleted->items, $userID); } // Delete creators if ($xml->deleted->creators) { Zotero_Creators::deleteFromXML($xml->deleted->creators, $userID); } // Delete saved searches if ($xml->deleted->searches) { Zotero_Searches::deleteFromXML($xml->deleted->searches, $userID); } // Delete tags if ($xml->deleted->tags) { $xmlElements = dom_import_simplexml($xml->deleted->tags); $xmlElements = $xmlElements->getElementsByTagName('tag'); foreach ($xmlElements as $xmlElement) { $libraryID = (int) $xmlElement->getAttribute('libraryID'); $key = $xmlElement->getAttribute('key'); $tagObj = Zotero_Tags::getByLibraryAndKey($libraryID, $key); if (!$tagObj) { continue; } // We need to update all items on the deleted tag $modifiedItems = array_merge($modifiedItems, array_map(function ($key) use($libraryID) { return $libraryID . "/" . $key; }, $tagObj->getLinkedItems(true))); } Zotero_Tags::deleteFromXML($xml->deleted->tags, $userID); } // Delete relations if ($xml->deleted->relations) { Zotero_Relations::deleteFromXML($xml->deleted->relations, $userID); } // Delete relations if ($xml->deleted->settings) { Zotero_Settings::deleteFromXML($xml->deleted->settings, $userID); } } $toUpdate = array(); foreach ($modifiedItems as $item) { // libraryID/key string if (is_string($item)) { if (isset($savedItems[$item])) { continue; } $savedItems[$item] = true; list($libraryID, $key) = explode("/", $item); $item = Zotero_Items::getByLibraryAndKey($libraryID, $key); if (!$item) { // Item was deleted continue; } } else { $lk = $item->libraryID . "/" . $item->key; if (isset($savedItems[$lk])) { continue; } $savedItems[$lk] = true; } $toUpdate[] = $item; } Zotero_Items::updateVersions($toUpdate, $userID); unset($savedItems); unset($modifiedItems); try { self::removeUploadProcess($processID); } catch (Exception $e) { if (strpos($e->getMessage(), 'MySQL server has gone away') !== false) { // Reconnect error_log("Reconnecting to MySQL master"); Zotero_DB::close(); self::removeUploadProcess($processID); } else { throw $e; } } // Send notifications for changed libraries foreach ($affectedLibraries as $libraryID) { Zotero_Notifier::trigger('modify', 'library', $libraryID); } Zotero_DB::commit(); if ($profile) { $shardID = Zotero_Shards::getByUserID($userID); Zotero_DB::profileEnd($shardID); } // Return timestamp + 1, to keep the next /updated call // (using >= timestamp) from returning this data return $timestamp + 1; } catch (Exception $e) { Zotero_DB::rollback(true); self::removeUploadProcess($processID); throw $e; } }
/** * Converts a DOMElement item to a Zotero_Tag object * * @param DOMElement $xml Tag data as DOMElement * @param int $libraryID Library ID * @return Zotero_Tag Zotero tag object */ public static function convertXMLToTag(DOMElement $xml) { $libraryID = (int) $xml->getAttribute('libraryID'); $tag = self::getByLibraryAndKey($libraryID, $xml->getAttribute('key')); if (!$tag) { $tag = new Zotero_Tag(); $tag->libraryID = $libraryID; $tag->key = $xml->getAttribute('key'); } $tag->name = $xml->getAttribute('name'); $type = (int) $xml->getAttribute('type'); $tag->type = $type ? $type : 0; $tag->dateAdded = $xml->getAttribute('dateAdded'); $tag->dateModified = $xml->getAttribute('dateModified'); $itemKeys = $xml->getElementsByTagName('items'); if ($itemKeys->length) { $itemKeys = explode(' ', $itemKeys->item(0)->nodeValue); $itemIDs = array(); foreach ($itemKeys as $key) { $item = Zotero_Items::getByLibraryAndKey($libraryID, $key); if (!$item) { // Return a specific error for a wrong-library tag issue that I can't reproduce throw new Exception("Linked item {$key} of tag {$libraryID}/{$tag->key} not found", Z_ERROR_TAG_LINKED_ITEM_NOT_FOUND); //throw new Exception("Linked item $key of tag $libraryID/$tag->key not found", Z_ERROR_ITEM_NOT_FOUND); } $itemIDs[] = $item->id; } $tag->setLinkedItems($itemIDs); } else { $tag->setLinkedItems(array()); } return $tag; }
public function tags() { $this->allowMethods(array('GET')); if (!$this->permissions->canAccess($this->objectLibraryID)) { $this->e403(); } $tags = array(); $totalResults = 0; $name = $this->objectName; $fixedValues = array(); // Set of tags matching name if ($name && $this->subset != 'tags') { $tagIDs = Zotero_Tags::getIDs($this->objectLibraryID, $name); if (!$tagIDs) { $this->e404(); } $title = "Tags matching ‘" . $name . "’"; } else { if ($this->scopeObject) { // If id, redirect to key URL if ($this->scopeObjectID) { if (!in_array($this->scopeObject, array("collections", "items"))) { $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 . "/tags" . $qs); exit; } switch ($this->scopeObject) { case 'collections': $collection = Zotero_Collections::getByLibraryAndKey($this->objectLibraryID, $this->scopeObjectKey); if (!$collection) { $this->e404(); } $title = "Tags in Collection ‘" . $collection->name . "’"; $counts = $collection->getTagItemCounts(); $tagIDs = array(); if ($counts) { foreach ($counts as $tagID => $count) { $tagIDs[] = $tagID; $fixedValues[$tagID] = array('numItems' => $count); } } break; case 'items': $item = Zotero_Items::getByLibraryAndKey($this->objectLibraryID, $this->scopeObjectKey); if (!$item) { $this->e404(); } $title = "Tags of '" . $item->getDisplayTitle() . "'"; $tagIDs = $item->getTags(true); break; default: throw new Exception("Invalid tags scope object '{$this->scopeObject}'"); } } else { $title = "Tags"; $results = Zotero_Tags::getAllAdvanced($this->objectLibraryID, $this->queryParams); $tags = $results['objects']; $totalResults = $results['total']; } } if (!empty($tagIDs)) { foreach ($tagIDs as $tagID) { $tags[] = Zotero_Tags::get($this->objectLibraryID, $tagID); } // Fake sorting and limiting $totalResults = sizeOf($tags); $key = $this->queryParams['order']; // 'title' order means 'name' for tags if ($key == 'title') { $key = 'name'; } $dir = $this->queryParams['sort']; $cmp = create_function('$a, $b', '$dir = "' . $dir . '" == "asc" ? 1 : -1; if ($a->' . $key . ' == $b->' . $key . ') { return 0; } else { return ($a->' . $key . ' > $b->' . $key . ') ? $dir : ($dir * -1);}'); usort($tags, $cmp); $tags = array_slice($tags, $this->queryParams['start'], $this->queryParams['limit']); } $this->responseXML = Zotero_Atom::createAtomFeed($this->getFeedNamePrefix($this->objectLibraryID) . $title, $this->uri, $tags, $totalResults, $this->queryParams, $this->apiVersion, $this->permissions, $fixedValues); $this->end(); }
public function updated() { if (empty($_REQUEST['lastsync'])) { $this->error(400, 'NO_LAST_SYNC_TIME', 'Last sync time not provided'); } $lastsync = false; if (is_numeric($_REQUEST['lastsync'])) { $lastsync = (int) $_REQUEST['lastsync']; } else { $this->error(400, 'INVALID_LAST_SYNC_TIME', 'Last sync time is invalid'); } $this->sessionCheck(); if (isset($_SERVER['HTTP_X_ZOTERO_VERSION'])) { require_once '../model/ToolkitVersionComparator.inc.php'; if (ToolkitVersionComparator::compare($_SERVER['HTTP_X_ZOTERO_VERSION'], "2.0.4") < 0) { $futureUsers = Z_Core::$MC->get('futureUsers'); if (!$futureUsers) { $futureUsers = Zotero_DB::columnQuery("SELECT userID FROM futureUsers"); Z_Core::$MC->set('futureUsers', $futureUsers, 1800); } if (in_array($this->userID, $futureUsers)) { Z_Core::logError("Blocking sync for future user " . $this->userID . " with version " . $_SERVER['HTTP_X_ZOTERO_VERSION']); $upgradeMessage = "Due to improvements made to sync functionality, you must upgrade to Zotero 2.0.6 or later (via Firefox's Tools menu -> Add-ons -> Extensions -> Find Updates or from zotero.org) to continue syncing your Zotero library."; $this->error(400, 'UPGRADE_REQUIRED', $upgradeMessage); } } } $doc = new DOMDocument(); $domResponse = dom_import_simplexml($this->responseXML); $domResponse = $doc->importNode($domResponse, true); $doc->appendChild($domResponse); try { $result = Zotero_Sync::getSessionDownloadResult($this->sessionID); } catch (Exception $e) { $this->handleUpdatedError($e); } // XML response if (is_string($result)) { $this->clearWaitTime($this->sessionID); $this->responseXML = new SimpleXMLElement($result); $this->end(); } // Queued if ($result === false) { $queued = $this->responseXML->addChild('locked'); $queued['wait'] = $this->getWaitTime($this->sessionID); $this->end(); } // Not queued if ($result == -1) { // See if we're locked Zotero_DB::beginTransaction(); if (Zotero_Sync::userIsWriteLocked($this->userID) || !empty($_REQUEST['upload']) && Zotero_Sync::userIsReadLocked($this->userID)) { Zotero_DB::commit(); $locked = $this->responseXML->addChild('locked'); $locked['wait'] = $this->getWaitTime($this->sessionID); $this->end(); } Zotero_DB::commit(); $queue = true; if (Z_ENV_TESTING_SITE && !empty($_GET['noqueue'])) { $queue = false; } // TEMP $cacheKeyExtra = (!empty($_POST['ft']) ? json_encode($_POST['ft']) : "") . (!empty($_POST['ftkeys']) ? json_encode($_POST['ftkeys']) : ""); // If we have a cached response, return that try { $startedTimestamp = microtime(true); $cached = Zotero_Sync::getCachedDownload($this->userID, $lastsync, $this->apiVersion, $cacheKeyExtra); // Not locked, so clear wait index $this->clearWaitTime($this->sessionID); if ($cached) { $this->responseXML = simplexml_load_string($cached, "SimpleXMLElement", LIBXML_COMPACT | LIBXML_PARSEHUGE); // TEMP if (!$this->responseXML) { error_log("Invalid cached XML data -- stripping control characters"); // Strip control characters in XML data $cached = preg_replace('/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F]/', '', $cached); $this->responseXML = simplexml_load_string($cached, "SimpleXMLElement", LIBXML_COMPACT | LIBXML_PARSEHUGE); } $duration = round((double) microtime(true) - $startedTimestamp, 2); Zotero_Sync::logDownload($this->userID, round($lastsync), strlen($cached), $this->ipAddress ? $this->ipAddress : 0, 0, $duration, $duration, (int) (!$this->responseXML)); StatsD::increment("sync.process.download.cache.hit"); if (!$this->responseXML) { $msg = "Error parsing cached XML for user " . $this->userID; error_log($msg); $this->handleUpdatedError(new Exception($msg)); } $this->end(); } } catch (Exception $e) { $msg = $e->getMessage(); if (strpos($msg, "Too many connections") !== false) { $msg = "'Too many connections' from MySQL"; } else { $msg = "'{$msg}'"; } Z_Core::logError("Warning: {$msg} getting cached download"); StatsD::increment("sync.process.download.cache.error"); } try { $num = Zotero_Items::countUpdated($this->userID, $lastsync, 5); } catch (Exception $e) { // We can get a MySQL lock timeout here if the upload starts // after the write lock check above but before we get here $this->handleUpdatedError($e); } // If nothing updated, or if just a few objects and processing is enabled, process synchronously if ($num == 0 || $num < 5 && Z_CONFIG::$PROCESSORS_ENABLED) { $queue = false; } $params = []; if (isset($_POST['ft'])) { $params['ft'] = $_POST['ft']; } if (isset($_POST['ftkeys'])) { $queue = true; $params['ftkeys'] = $_POST['ftkeys']; } if ($queue) { Zotero_Sync::queueDownload($this->userID, $this->sessionID, $lastsync, $this->apiVersion, $num, $params); try { Zotero_Processors::notifyProcessors('download'); } catch (Exception $e) { Z_Core::logError($e); } $locked = $this->responseXML->addChild('locked'); $locked['wait'] = 1000; } else { try { Zotero_Sync::processDownload($this->userID, $lastsync, $doc, $params); $this->responseXML = simplexml_import_dom($doc); StatsD::increment("sync.process.download.immediate.success"); } catch (Exception $e) { StatsD::increment("sync.process.download.immediate.error"); $this->handleUpdatedError($e); } } $this->end(); } throw new Exception("Unexpected session result {$result}"); }
protected function handleObjectWrite($objectType, $obj = null) { if (!is_object($obj) && !is_null($obj)) { throw new Exception('$obj must be a data object or null'); } $objectTypePlural = \Zotero\DataObjectUtilities::getObjectTypePlural($objectType); $objectsClassName = "Zotero_" . ucwords($objectTypePlural); $json = !empty($this->body) ? $this->jsonDecode($this->body) : false; $objectVersionValidated = $this->checkSingleObjectWriteVersion($objectType, $obj, $json); $this->libraryVersion = Zotero_Libraries::getUpdatedVersion($this->objectLibraryID); // Update item if ($this->method == 'PUT' || $this->method == 'PATCH') { if ($this->apiVersion < 2) { $this->allowMethods(['PUT']); } if (!$obj) { $className = "Zotero_" . ucwords($objectType); $obj = new $className(); $obj->libraryID = $this->objectLibraryID; $obj->key = $this->objectKey; } if ($objectType == 'item') { $changed = Zotero_Items::updateFromJSON($obj, $json, null, $this->queryParams, $this->userID, $objectVersionValidated ? 0 : 2, $this->method == 'PATCH'); } else { $changed = $objectsClassName::updateFromJSON($obj, $json, $this->queryParams, $this->userID, $objectVersionValidated ? 0 : 2, $this->method == 'PATCH'); } // If not updated, return the original library version if (!$changed) { $this->libraryVersion = Zotero_Libraries::getOriginalVersion($this->objectLibraryID); } if ($cacheKey = $this->getWriteTokenCacheKey()) { Z_Core::$MC->set($cacheKey, true, $this->writeTokenCacheTime); } } else { if ($this->method == 'DELETE') { $objectsClassName::delete($this->objectLibraryID, $this->objectKey); } else { throw new Exception("Unexpected method {$this->method}"); } } if ($this->apiVersion >= 2 || $this->method == 'DELETE') { $this->e204(); } return $obj; }
/** * Converts a Zotero_Item object to a SimpleXMLElement Atom object * * @param object $item Zotero_Item object * @param string $content * @return SimpleXMLElement Item data as SimpleXML element */ public static function convertItemToAtom(Zotero_Item $item, $queryParams, $apiVersion = null, $permissions = null, $sharedData = null) { $content = $queryParams['content']; $contentIsHTML = sizeOf($content) == 1 && $content[0] == 'html'; $contentParamString = urlencode(implode(',', $content)); $style = $queryParams['style']; $entry = '<entry xmlns="' . Zotero_Atom::$nsAtom . '" xmlns:zapi="' . Zotero_Atom::$nsZoteroAPI . '"/>'; $xml = new SimpleXMLElement($entry); $title = $item->getDisplayTitle(true); $title = $title ? $title : '[Untitled]'; $xml->title = $title; $author = $xml->addChild('author'); $createdByUserID = null; switch (Zotero_Libraries::getType($item->libraryID)) { case 'group': $createdByUserID = $item->createdByUserID; break; } if ($createdByUserID) { $author->name = Zotero_Users::getUsername($createdByUserID); $author->uri = Zotero_URI::getUserURI($createdByUserID); } else { $author->name = Zotero_Libraries::getName($item->libraryID); $author->uri = Zotero_URI::getLibraryURI($item->libraryID); } $id = Zotero_URI::getItemURI($item); /*if (!$contentIsHTML) { $id .= "?content=$content"; }*/ $xml->id = $id; $xml->published = Zotero_Date::sqlToISO8601($item->getField('dateAdded')); $xml->updated = Zotero_Date::sqlToISO8601($item->getField('dateModified')); $link = $xml->addChild("link"); $link['rel'] = "self"; $link['type'] = "application/atom+xml"; $href = Zotero_Atom::getItemURI($item); if (!$contentIsHTML) { $href .= "?content={$contentParamString}"; } $link['href'] = $href; $parent = $item->getSource(); if ($parent) { // TODO: handle group items? $parentItem = Zotero_Items::get($item->libraryID, $parent); $link = $xml->addChild("link"); $link['rel'] = "up"; $link['type'] = "application/atom+xml"; $href = Zotero_Atom::getItemURI($parentItem); if (!$contentIsHTML) { $href .= "?content={$contentParamString}"; } $link['href'] = $href; } $link = $xml->addChild('link'); $link['rel'] = 'alternate'; $link['type'] = 'text/html'; $link['href'] = Zotero_URI::getItemURI($item); // If appropriate permissions and the file is stored in ZFS, get file request link if ($permissions && $permissions->canAccess($item->libraryID, 'files')) { $details = Zotero_S3::getDownloadDetails($item); if ($details) { $link = $xml->addChild('link'); $link['rel'] = 'enclosure'; $type = $item->attachmentMIMEType; if ($type) { $link['type'] = $type; } $link['href'] = $details['url']; if (!empty($details['filename'])) { $link['title'] = $details['filename']; } if (!empty($details['size'])) { $link['length'] = $details['size']; } } } $xml->addChild('zapi:key', $item->key, Zotero_Atom::$nsZoteroAPI); $xml->addChild('zapi:itemType', Zotero_ItemTypes::getName($item->itemTypeID), Zotero_Atom::$nsZoteroAPI); if ($item->isRegularItem()) { $val = $item->creatorSummary; if ($val !== '') { $xml->addChild('zapi:creatorSummary', htmlspecialchars($val), Zotero_Atom::$nsZoteroAPI); } $val = substr($item->getField('date', true, true, true), 0, 4); if ($val !== '' && $val !== '0000') { $xml->addChild('zapi:year', $val, Zotero_Atom::$nsZoteroAPI); } } if (!$parent && $item->isRegularItem()) { if ($permissions && !$permissions->canAccess($item->libraryID, 'notes')) { $numChildren = $item->numAttachments(); } else { $numChildren = $item->numChildren(); } $xml->addChild('zapi:numChildren', $numChildren, Zotero_Atom::$nsZoteroAPI); } $xml->addChild('zapi:numTags', $item->numTags(), Zotero_Atom::$nsZoteroAPI); $xml->content = ''; // // DOM XML from here on out // $contentNode = dom_import_simplexml($xml->content); $domDoc = $contentNode->ownerDocument; $multiFormat = sizeOf($content) > 1; // Create a root XML document for multi-format responses if ($multiFormat) { $contentNode->setAttribute('type', 'application/xml'); /*$multicontent = $domDoc->createElementNS( Zotero_Atom::$nsZoteroAPI, 'multicontent' ); $contentNode->appendChild($multicontent);*/ } foreach ($content as $type) { // Set the target to either the main <content> // or a <multicontent> <content> if (!$multiFormat) { $target = $contentNode; } else { $target = $domDoc->createElementNS(Zotero_Atom::$nsZoteroAPI, 'subcontent'); $contentNode->appendChild($target); } $target->setAttributeNS(Zotero_Atom::$nsZoteroAPI, "zapi:type", $type); if ($type == 'html') { if (!$multiFormat) { $target->setAttribute('type', 'xhtml'); } $div = $domDoc->createElement('div'); $div->setAttribute('xmlns', Zotero_Atom::$nsXHTML); $target->appendChild($div); $html = $item->toHTML(true); $subNode = dom_import_simplexml($html); $importedNode = $domDoc->importNode($subNode, true); $div->appendChild($importedNode); } else { if ($type == 'citation') { if (!$multiFormat) { $target->setAttribute('type', 'xhtml'); } if (isset($sharedData[$type][$item->libraryID . "/" . $item->key])) { $html = $sharedData[$type][$item->libraryID . "/" . $item->key]; } else { if ($sharedData !== null) { error_log("Citation not found in sharedData -- retrieving individually"); } $html = Zotero_Cite::getCitationFromCiteServer($item, $style); } $html = new SimpleXMLElement($html); $html['xmlns'] = Zotero_Atom::$nsXHTML; $subNode = dom_import_simplexml($html); $importedNode = $domDoc->importNode($subNode, true); $target->appendChild($importedNode); } else { if ($type == 'bib') { if (!$multiFormat) { $target->setAttribute('type', 'xhtml'); } if (isset($sharedData[$type][$item->libraryID . "/" . $item->key])) { $html = $sharedData[$type][$item->libraryID . "/" . $item->key]; } else { if ($sharedData !== null) { error_log("Bibliography not found in sharedData -- retrieving individually"); } $html = Zotero_Cite::getBibliographyFromCitationServer(array($item), $style); } $html = new SimpleXMLElement($html); $html['xmlns'] = Zotero_Atom::$nsXHTML; $subNode = dom_import_simplexml($html); $importedNode = $domDoc->importNode($subNode, true); $target->appendChild($importedNode); } else { if ($type == 'json') { $target->setAttributeNS(Zotero_Atom::$nsZoteroAPI, "zapi:etag", $item->etag); $textNode = $domDoc->createTextNode($item->toJSON(false, $queryParams['pprint'], true)); $target->appendChild($textNode); } else { if ($type == 'csljson') { $arr = $item->toCSLItem(); $mask = JSON_HEX_TAG | JSON_HEX_AMP; if ($queryParams['pprint']) { $json = Zotero_Utilities::json_encode_pretty($arr, $mask); } else { $json = json_encode($arr, $mask); } // Until JSON_UNESCAPED_SLASHES is available $json = str_replace('\\/', '/', $json); $textNode = $domDoc->createTextNode($json); $target->appendChild($textNode); } else { if ($type == 'full') { if (!$multiFormat) { $target->setAttribute('type', 'xhtml'); } $fullXML = Zotero_Items::convertItemToXML($item, array(), $apiVersion); $fullXML->addAttribute("xmlns", Zotero_Atom::$nsZoteroTransfer); $subNode = dom_import_simplexml($fullXML); $importedNode = $domDoc->importNode($subNode, true); $target->appendChild($importedNode); } else { if (in_array($type, Zotero_Translate::$exportFormats)) { $export = Zotero_Translate::doExport(array($item), $type); $target->setAttribute('type', $export['mimeType']); // Insert XML into document if (preg_match('/\\+xml$/', $export['mimeType'])) { // Strip prolog $body = preg_replace('/^<\\?xml.+\\n/', "", $export['body']); $subNode = $domDoc->createDocumentFragment(); $subNode->appendXML($body); $target->appendChild($subNode); } else { $textNode = $domDoc->createTextNode($export['body']); $target->appendChild($textNode); } } } } } } } } } return $xml; }
public function toSolrDocument() { $doc = new SolrInputDocument(); $uri = Zotero_Solr::getItemURI($this->libraryID, $this->key); $doc->addField("uri", $uri); // Primary fields foreach (Zotero_Items::$primaryFields as $field) { switch ($field) { case 'itemID': case 'numAttachments': case 'numNotes': continue 2; case 'itemTypeID': $xmlField = 'itemType'; $xmlValue = Zotero_ItemTypes::getName($this->{$field}); break; case 'dateAdded': case 'dateModified': case 'serverDateModified': $xmlField = $field; $xmlValue = Zotero_Date::sqlToISO8601($this->{$field}); break; default: $xmlField = $field; $xmlValue = $this->{$field}; } $doc->addField($xmlField, $xmlValue); } // Title for sorting $title = $this->getDisplayTitle(true); $title = $title ? $title : ''; // Strip HTML from note titles if ($this->isNote()) { // Clean and strip HTML, giving us an HTML-encoded plaintext string $title = strip_tags($GLOBALS['HTMLPurifier']->purify($title)); // Unencode plaintext string $title = html_entity_decode($title); } // Strip some characters $sortTitle = preg_replace("/^[\\[\\'\"]*(.*)[\\]\\'\"]*\$/", "\$1", $title); if ($sortTitle) { $doc->addField('titleSort', $sortTitle); } // Item data $fieldIDs = $this->getUsedFields(); foreach ($fieldIDs as $fieldID) { $val = $this->getField($fieldID); if ($val == '') { continue; } $fieldName = Zotero_ItemFields::getName($fieldID); switch ($fieldName) { // As is case 'title': $val = $title; break; // Date fields // Date fields case 'date': // Add user part as text $doc->addField($fieldName . "_t", Zotero_Date::multipartToStr($val)); // Add as proper date, if there is one $sqlDate = Zotero_Date::multipartToSQL($val); if (!$sqlDate || $sqlDate == '0000-00-00') { continue 2; } $fieldName .= "_tdt"; $val = Zotero_Date::sqlToISO8601($sqlDate); break; case 'accessDate': if (!Zotero_Date::isSQLDateTime($val)) { continue 2; } $fieldName .= "_tdt"; $val = Zotero_Date::sqlToISO8601($val); break; default: $fieldName .= "_t"; } $doc->addField($fieldName, $val); } // Deleted item flag if ($this->getDeleted()) { $doc->addField('deleted', true); } if ($this->isNote() || $this->isAttachment()) { $sourceItemID = $this->getSource(); if ($sourceItemID) { $sourceItem = Zotero_Items::get($this->libraryID, $sourceItemID); if (!$sourceItem) { throw new Exception("Source item {$sourceItemID} not found"); } $doc->addField('sourceItem', $sourceItem->key); } } // Group modification info $createdByUserID = null; $lastModifiedByUserID = null; switch (Zotero_Libraries::getType($this->libraryID)) { case 'group': $createdByUserID = $this->createdByUserID; $lastModifiedByUserID = $this->lastModifiedByUserID; break; } if ($createdByUserID) { $doc->addField('createdByUserID', $createdByUserID); } if ($lastModifiedByUserID) { $doc->addField('lastModifiedByUserID', $lastModifiedByUserID); } // Note if ($this->isNote()) { $doc->addField('note', $this->getNote()); } if ($this->isAttachment()) { $doc->addField('linkMode', $this->attachmentLinkMode); $doc->addField('mimeType', $this->attachmentMIMEType); if ($this->attachmentCharset) { $doc->addField('charset', $this->attachmentCharset); } // TODO: get from a constant if ($this->attachmentLinkMode != 3) { $doc->addField('path', $this->attachmentPath); } $note = $this->getNote(); if ($note) { $doc->addField('note', $note); } } // Creators $creators = $this->getCreators(); if ($creators) { foreach ($creators as $index => $creator) { $c = $creator['ref']; $doc->addField('creatorKey', $c->key); if ($c->fieldMode == 0) { $doc->addField('creatorFirstName', $c->firstName); } $doc->addField('creatorLastName', $c->lastName); $doc->addField('creatorType', Zotero_CreatorTypes::getName($creator['creatorTypeID'])); $doc->addField('creatorIndex', $index); } } // Tags $tags = $this->getTags(); if ($tags) { foreach ($tags as $tag) { $doc->addField('tagKey', $tag->key); $doc->addField('tag', $tag->name); $doc->addField('tagType', $tag->type); } } // Related items /*$related = $this->relatedItems; if ($related) { $related = Zotero_Items::get($this->libraryID, $related); $keys = array(); foreach ($related as $item) { $doc->addField('relatedItem', $item->key); } }*/ return $doc; }
private static function processUploadInternal($userID, SimpleXMLElement $xml, $syncQueueID = null, $syncProcessID = null) { $userLibraryID = Zotero_Users::getLibraryIDFromUserID($userID); $affectedLibraries = self::parseAffectedLibraries($xml->asXML()); // Relations-only uploads don't have affected libraries if (!$affectedLibraries) { $affectedLibraries = array(Zotero_Users::getLibraryIDFromUserID($userID)); } $processID = self::addUploadProcess($userID, $affectedLibraries, $syncQueueID, $syncProcessID); set_time_limit(5400); $profile = false; if ($profile) { $shardID = Zotero_Shards::getByUserID($userID); Zotero_DB::profileStart($shardID); } try { Z_Core::$MC->begin(); Zotero_DB::beginTransaction(); // Mark libraries as updated $timestamp = Zotero_Libraries::updateTimestamps($affectedLibraries); Zotero_DB::registerTransactionTimestamp($timestamp); // Make sure no other upload sessions use this same timestamp // for any of these libraries, since we return >= 1 as the next // last sync time if (!Zotero_Libraries::setTimestampLock($affectedLibraries, $timestamp)) { throw new Exception("Library timestamp already used", Z_ERROR_LIBRARY_TIMESTAMP_ALREADY_USED); } // Add/update creators if ($xml->creators) { // DOM $keys = array(); $xmlElements = dom_import_simplexml($xml->creators); $xmlElements = $xmlElements->getElementsByTagName('creator'); Zotero_DB::query("SET foreign_key_checks = 0"); try { $addedLibraryIDs = array(); $addedCreatorDataHashes = array(); foreach ($xmlElements as $xmlElement) { $key = $xmlElement->getAttribute('key'); if (isset($keys[$key])) { throw new Exception("Creator {$key} already processed"); } $keys[$key] = true; $creatorObj = Zotero_Creators::convertXMLToCreator($xmlElement); $addedLibraryIDs[] = $creatorObj->libraryID; $creatorObj->save(); } } catch (Exception $e) { Zotero_DB::query("SET foreign_key_checks = 1"); throw $e; } Zotero_DB::query("SET foreign_key_checks = 1"); unset($keys); unset($xml->creators); // // Manual foreign key checks // // libraryID foreach ($addedLibraryIDs as $addedLibraryID) { $shardID = Zotero_Shards::getByLibraryID($addedLibraryID); $sql = "SELECT COUNT(*) FROM shardLibraries WHERE libraryID=?"; if (!Zotero_DB::valueQuery($sql, $addedLibraryID, $shardID)) { throw new Exception("libraryID inserted into `creators` not found in `shardLibraries` ({$addedLibraryID}, {$shardID})"); } } } // Add/update items if ($xml->items) { $childItems = array(); $relatedItemsStore = array(); // DOM $keys = array(); $xmlElements = dom_import_simplexml($xml->items); $xmlElements = $xmlElements->getElementsByTagName('item'); foreach ($xmlElements as $xmlElement) { $key = $xmlElement->getAttribute('key'); if (isset($keys[$key])) { throw new Exception("Item {$key} already processed"); } $keys[$key] = true; $missing = Zotero_Items::removeMissingRelatedItems($xmlElement); $itemObj = Zotero_Items::convertXMLToItem($xmlElement); if ($missing) { $relatedItemsStore[$itemObj->libraryID . '_' . $itemObj->key] = $missing; } if (!$itemObj->getSourceKey()) { try { $itemObj->save($userID); } catch (Exception $e) { if (strpos($e->getMessage(), 'libraryIDs_do_not_match') !== false) { throw new Exception($e->getMessage() . " (" . $itemObj->key . ")"); } throw $e; } } else { $childItems[] = $itemObj; } } unset($keys); unset($xml->items); while ($childItem = array_shift($childItems)) { $childItem->save($userID); } // Add back related items (which now exist) foreach ($relatedItemsStore as $itemLibraryKey => $relset) { $lk = explode('_', $itemLibraryKey); $libraryID = $lk[0]; $key = $lk[1]; $item = Zotero_Items::getByLibraryAndKey($libraryID, $key); foreach ($relset as $relKey) { $relItem = Zotero_Items::getByLibraryAndKey($libraryID, $relKey); $item->addRelatedItem($relItem->id); } $item->save(); } unset($relatedItemsStore); } // Add/update collections if ($xml->collections) { $collections = array(); $collectionSets = array(); // DOM // Build an array of unsaved collection objects and the keys of child items $keys = array(); $xmlElements = dom_import_simplexml($xml->collections); $xmlElements = $xmlElements->getElementsByTagName('collection'); foreach ($xmlElements as $xmlElement) { $key = $xmlElement->getAttribute('key'); if (isset($keys[$key])) { throw new Exception("Collection {$key} already processed"); } $keys[$key] = true; $collectionObj = Zotero_Collections::convertXMLToCollection($xmlElement); $xmlItems = $xmlElement->getElementsByTagName('items')->item(0); // Fix an error if there's leading or trailing whitespace, // which was possible in 2.0.3 if ($xmlItems) { $xmlItems = trim($xmlItems->nodeValue); } $arr = array('obj' => $collectionObj, 'items' => $xmlItems ? explode(' ', $xmlItems) : array()); $collections[] = $collectionObj; $collectionSets[] = $arr; } unset($keys); unset($xml->collections); self::saveCollections($collections); unset($collections); // Set child items foreach ($collectionSets as $collection) { // Child items if (isset($collection['items'])) { $ids = array(); foreach ($collection['items'] as $key) { $item = Zotero_Items::getByLibraryAndKey($collection['obj']->libraryID, $key); if (!$item) { throw new Exception("Child item '{$key}' of collection {$collection['obj']->id} not found", Z_ERROR_ITEM_NOT_FOUND); } $ids[] = $item->id; } $collection['obj']->setChildItems($ids); } } unset($collectionSets); } // Add/update saved searches if ($xml->searches) { $searches = array(); $keys = array(); foreach ($xml->searches->search as $xmlElement) { $key = (string) $xmlElement['key']; if (isset($keys[$key])) { throw new Exception("Search {$key} already processed"); } $keys[$key] = true; $searchObj = Zotero_Searches::convertXMLToSearch($xmlElement); $searchObj->save(); } unset($xml->searches); } // Add/update tags if ($xml->tags) { $keys = array(); // DOM $xmlElements = dom_import_simplexml($xml->tags); $xmlElements = $xmlElements->getElementsByTagName('tag'); foreach ($xmlElements as $xmlElement) { $key = $xmlElement->getAttribute('key'); if (isset($keys[$key])) { throw new Exception("Tag {$key} already processed"); } $keys[$key] = true; $tagObj = Zotero_Tags::convertXMLToTag($xmlElement); $tagObj->save(true); } unset($keys); unset($xml->tags); } // Add/update relations if ($xml->relations) { // DOM $xmlElements = dom_import_simplexml($xml->relations); $xmlElements = $xmlElements->getElementsByTagName('relation'); foreach ($xmlElements as $xmlElement) { $relationObj = Zotero_Relations::convertXMLToRelation($xmlElement, $userLibraryID); if ($relationObj->exists()) { continue; } $relationObj->save(); } unset($keys); unset($xml->relations); } // TODO: loop if ($xml->deleted) { // Delete collections if ($xml->deleted->collections) { Zotero_Collections::deleteFromXML($xml->deleted->collections); } // Delete items if ($xml->deleted->items) { Zotero_Items::deleteFromXML($xml->deleted->items); } // Delete creators if ($xml->deleted->creators) { Zotero_Creators::deleteFromXML($xml->deleted->creators); } // Delete saved searches if ($xml->deleted->searches) { Zotero_Searches::deleteFromXML($xml->deleted->searches); } // Delete tags if ($xml->deleted->tags) { Zotero_Tags::deleteFromXML($xml->deleted->tags); } // Delete tags if ($xml->deleted->relations) { Zotero_Relations::deleteFromXML($xml->deleted->relations); } } self::removeUploadProcess($processID); Zotero_DB::commit(); Z_Core::$MC->commit(); if ($profile) { $shardID = Zotero_Shards::getByUserID($userID); Zotero_DB::profileEnd($shardID); } // Return timestamp + 1, to keep the next /updated call // (using >= timestamp) from returning this data return $timestamp + 1; } catch (Exception $e) { Z_Core::$MC->rollback(); Zotero_DB::rollback(true); self::removeUploadProcess($processID); throw $e; } }
public function toResponseJSON($requestParams=[], Zotero_Permissions $permissions, $sharedData=null) { $t = microtime(true); if (!$this->loaded['primaryData']) { $this->loadPrimaryData(); } if (!$this->loaded['itemData']) { $this->loadItemData(); } // Uncached stuff or parts of the cache key $version = $this->version; $parent = $this->getSource(); $isRegularItem = !$parent && $this->isRegularItem(); $downloadDetails = $permissions->canAccess($this->libraryID, 'files') ? Zotero_Storage::getDownloadDetails($this) : false; if ($isRegularItem) { $numChildren = $permissions->canAccess($this->libraryID, 'notes') ? $this->numChildren() : $this->numAttachments(); } $libraryType = Zotero_Libraries::getType($this->libraryID); // Any query parameters that have an effect on the output // need to be added here $allowedParams = [ 'include', 'style', 'css', 'linkwrap' ]; $cachedParams = Z_Array::filterKeys($requestParams, $allowedParams); $cacheVersion = 1; $cacheKey = "jsonEntry_" . $this->libraryID . "/" . $this->id . "_" . md5( $version . json_encode($cachedParams) . ($downloadDetails ? 'hasFile' : '') // For groups, include the group WWW URL, which can change . ($libraryType == 'group' ? Zotero_URI::getItemURI($this, true) : '') ) . "_" . $requestParams['v'] // For code-based changes . "_" . $cacheVersion // For data-based changes . (isset(Z_CONFIG::$CACHE_VERSION_RESPONSE_JSON_ITEM) ? "_" . Z_CONFIG::$CACHE_VERSION_RESPONSE_JSON_ITEM : "") // If there's bib content, include the bib cache version . ((in_array('bib', $requestParams['include']) && isset(Z_CONFIG::$CACHE_VERSION_BIB)) ? "_" . Z_CONFIG::$CACHE_VERSION_BIB : ""); $cached = Z_Core::$MC->get($cacheKey); if (false && $cached) { // Make sure numChildren reflects the current permissions if ($isRegularItem) { $json = json_decode($cached); $json['numChildren'] = $numChildren; $cached = json_encode($json); } //StatsD::timing("api.items.itemToResponseJSON.cached", (microtime(true) - $t) * 1000); //StatsD::increment("memcached.items.itemToResponseJSON.hit"); // Skip the cache every 10 times for now, to ensure cache sanity if (!Z_Core::probability(10)) { return $cached; } } $json = [ 'key' => $this->key, 'version' => $version, 'library' => Zotero_Libraries::toJSON($this->libraryID) ]; $json['links'] = [ 'self' => [ 'href' => Zotero_API::getItemURI($this), 'type' => 'application/json' ], 'alternate' => [ 'href' => Zotero_URI::getItemURI($this, true), 'type' => 'text/html' ] ]; if ($parent) { $parentItem = Zotero_Items::get($this->libraryID, $parent); $json['links']['up'] = [ 'href' => Zotero_API::getItemURI($parentItem), 'type' => 'application/json' ]; } // If appropriate permissions and the file is stored in ZFS, get file request link if ($downloadDetails) { $details = $downloadDetails; $type = $this->attachmentMIMEType; if ($type) { $json['links']['enclosure'] = [ 'type' => $type ]; } $json['links']['enclosure']['href'] = $details['url']; if (!empty($details['filename'])) { $json['links']['enclosure']['title'] = $details['filename']; } if (isset($details['size'])) { $json['links']['enclosure']['length'] = $details['size']; } } // 'meta' $json['meta'] = new stdClass; if (Zotero_Libraries::getType($this->libraryID) == 'group') { $createdByUserID = $this->createdByUserID; $lastModifiedByUserID = $this->lastModifiedByUserID; if ($createdByUserID) { $json['meta']->createdByUser = Zotero_Users::toJSON($createdByUserID); } if ($lastModifiedByUserID && $lastModifiedByUserID != $createdByUserID) { $json['meta']->lastModifiedByUser = Zotero_Users::toJSON($lastModifiedByUserID); } } if ($isRegularItem) { $val = $this->getCreatorSummary(); if ($val !== '') { $json['meta']->creatorSummary = $val; } $val = $this->getField('date', true, true, true); if ($val !== '') { $sqlDate = Zotero_Date::multipartToSQL($val); if (substr($sqlDate, 0, 4) !== '0000') { $json['meta']->parsedDate = Zotero_Date::sqlToISO8601($sqlDate); } } $json['meta']->numChildren = $numChildren; } // 'include' $include = $requestParams['include']; foreach ($include as $type) { if ($type == 'html') { $json[$type] = trim($this->toHTML()); } else if ($type == 'citation') { if (isset($sharedData[$type][$this->libraryID . "/" . $this->key])) { $html = $sharedData[$type][$this->libraryID . "/" . $this->key]; } else { if ($sharedData !== null) { //error_log("Citation not found in sharedData -- retrieving individually"); } $html = Zotero_Cite::getCitationFromCiteServer($this, $requestParams); } $json[$type] = $html; } else if ($type == 'bib') { if (isset($sharedData[$type][$this->libraryID . "/" . $this->key])) { $html = $sharedData[$type][$this->libraryID . "/" . $this->key]; } else { if ($sharedData !== null) { //error_log("Bibliography not found in sharedData -- retrieving individually"); } $html = Zotero_Cite::getBibliographyFromCitationServer([$this], $requestParams); // Strip prolog $html = preg_replace('/^<\?xml.+\n/', "", $html); $html = trim($html); } $json[$type] = $html; } else if ($type == 'data') { $json[$type] = $this->toJSON(true, $requestParams, true); } else if ($type == 'csljson') { $json[$type] = $this->toCSLItem(); } else if (in_array($type, Zotero_Translate::$exportFormats)) { $export = Zotero_Translate::doExport([$this], $type); $json[$type] = $export['body']; unset($export); } } // TEMP if ($cached) { $uncached = Zotero_Utilities::formatJSON($json); if ($cached != $uncached) { error_log("Cached JSON item entry does not match"); error_log(" Cached: " . $cached); error_log("Uncached: " . $uncached); //Z_Core::$MC->set($cacheKey, $uncached, 3600); // 1 hour for now } } else { /*Z_Core::$MC->set($cacheKey, $xmlstr, 3600); // 1 hour for now StatsD::timing("api.items.itemToAtom.uncached", (microtime(true) - $t) * 1000); StatsD::increment("memcached.items.itemToAtom.miss");*/ } return $json; }
public function itemToAtom($itemID) { if (!is_int($itemID)) { throw new Exception("itemID must be an integer (was " . gettype($itemID) . ")"); } if (!$this->loaded) { $this->load(); } //$groupUserData = $this->getUserData($itemID); $item = Zotero_Items::get($this->libraryID, $itemID); if (!$item) { throw new Exception("Item {$itemID} doesn't exist"); } $xml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?>' . '<entry xmlns="' . Zotero_Atom::$nsAtom . '" ' . 'xmlns:zapi="' . Zotero_Atom::$nsZoteroAPI . '" ' . 'xmlns:xfer="' . Zotero_Atom::$nsZoteroTransfer . '"/>'); $title = $item->getDisplayTitle(true); $title = $title ? $title : '[Untitled]'; // Strip HTML from note titles if ($item->isNote()) { // Clean and strip HTML, giving us an HTML-encoded plaintext string $title = strip_tags(Zotero_Notes::sanitize($title)); // Unencode plaintext string $title = html_entity_decode($title); } $xml->title = $title; $author = $xml->addChild('author'); $author->name = Zotero_Libraries::getName($item->libraryID); $author->uri = Zotero_URI::getLibraryURI($item->libraryID); $xml->id = Zotero_URI::getItemURI($item); $xml->published = Zotero_Date::sqlToISO8601($item->dateAdded); $xml->updated = Zotero_Date::sqlToISO8601($item->dateModified); $link = $xml->addChild("link"); $link['rel'] = "self"; $link['type'] = "application/atom+xml"; $link['href'] = Zotero_API::getItemURI($item); $link = $xml->addChild('link'); $link['rel'] = 'alternate'; $link['type'] = 'text/html'; $link['href'] = Zotero_URI::getItemURI($item, true); $xml->content['type'] = 'application/xml'; $itemXML = new SimpleXMLElement('<item xmlns="' . Zotero_Atom::$nsZoteroTransfer . '"/>'); // This method of adding the element seems to be necessary to get the // namespace prefix to show up $fNode = dom_import_simplexml($xml->content); $subNode = dom_import_simplexml($itemXML); $importedNode = $fNode->ownerDocument->importNode($subNode, true); $fNode->appendChild($importedNode); $xml->content->item['id'] = $itemID; return $xml; }
/** * Import an item by URL using the translation server * * Initial request: * * { * "url": "http://..." * } * * Item selection for multi-item results: * * { * "url": "http://...", * "token": "<token>" * "items": { * "0": "Item 1 Title", * "3": "Item 2 Title" * } * } * * Returns an array of keys of added items (like updateMultipleFromJSON) or an object * with a 'select' property containing an array of titles for multi-item results */ public static function addFromURL($json, $requestParams, $libraryID, $userID, Zotero_Permissions $permissions, $translationToken) { if (!$translationToken) { throw new Exception("Translation token not provided"); } self::validateJSONURL($json, $requestParams); $cacheKey = 'addFromURLKeyMappings_' . md5($json->url . $translationToken); // Replace numeric keys with URLs for selected items if (isset($json->items) && $requestParams['v'] >= 2) { $keyMappings = Z_Core::$MC->get($cacheKey); $newItems = []; foreach ($json->items as $number => $title) { if (!isset($keyMappings[$number])) { throw new Exception("Index '{$number}' not found for URL and token", Z_ERROR_INVALID_INPUT); } $url = $keyMappings[$number]; $newItems[$url] = $title; } $json->items = $newItems; } $response = Zotero_Translate::doWeb($json->url, $translationToken, isset($json->items) ? $json->items : null); if (!$response || is_int($response)) { return $response; } if (isset($response->items)) { if ($requestParams['v'] >= 2) { $response = $response->items; for ($i = 0, $len = sizeOf($response); $i < $len; $i++) { // Assign key here so that we can add notes if necessary do { $itemKey = Zotero_ID::getKey(); } while (Zotero_Items::existsByLibraryAndKey($libraryID, $itemKey)); $response[$i]->key = $itemKey; // Pull out notes and stick in separate items if (isset($response[$i]->notes)) { foreach ($response[$i]->notes as $note) { $newNote = (object) ["itemType" => "note", "note" => $note->note, "parentItem" => $itemKey]; $response[] = $newNote; } unset($response[$i]->notes); } // TODO: link attachments, or not possible from translation-server? } } try { self::validateMultiObjectJSON($response, $requestParams); } catch (Exception $e) { error_log($e); error_log(json_encode($response)); throw new Exception("Invalid JSON from doWeb()"); } } else { if (isset($response->select)) { // Replace URLs with numeric keys for found items if ($requestParams['v'] >= 2) { $keyMappings = []; $newItems = new stdClass(); $number = 0; foreach ($response->select as $url => $title) { $keyMappings[$number] = $url; $newItems->{$number} = $title; $number++; } Z_Core::$MC->set($cacheKey, $keyMappings, 600); $response->select = $newItems; } return $response; } else { throw new Exception("Invalid return value from doWeb()"); } } return self::updateMultipleFromJSON($response, $requestParams, $libraryID, $userID, $permissions, false, null); }
public function tags() { $this->allowMethods(['HEAD', 'GET', 'DELETE']); 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 $this->checkLibraryIfUnmodifiedSinceVersion(true); Zotero_Libraries::updateVersionAndTimestamp($this->objectLibraryID); } $tagIDs = array(); $results = array(); $name = $this->objectName; $fixedValues = array(); $this->libraryVersion = Zotero_Libraries::getUpdatedVersion($this->objectLibraryID); // Set of tags matching name if ($name && $this->subset != 'tags') { $this->allowMethods(array('GET')); $tagIDs = Zotero_Tags::getIDs($this->objectLibraryID, $name); if (!$tagIDs) { $this->e404(); } $title = "Tags matching ‘" . $name . "’"; } else { $this->allowMethods(array('GET', 'DELETE')); if ($this->scopeObject) { $this->allowMethods(array('GET')); switch ($this->scopeObject) { case 'collections': $collection = Zotero_Collections::getByLibraryAndKey($this->objectLibraryID, $this->scopeObjectKey); if (!$collection) { $this->e404(); } $title = "Tags in Collection ‘" . $collection->name . "’"; $counts = $collection->getTagItemCounts(); $tagIDs = array(); if ($counts) { foreach ($counts as $tagID => $count) { $tagIDs[] = $tagID; $fixedValues[$tagID] = array('numItems' => $count); } } break; case 'items': $item = Zotero_Items::getByLibraryAndKey($this->objectLibraryID, $this->scopeObjectKey); if (!$item) { $this->e404(); } $title = "Tags of '" . $item->getDisplayTitle() . "'"; $tagIDs = $item->getTags(true); break; default: throw new Exception("Invalid tags scope object '{$this->scopeObject}'"); } } else { if ($this->method == 'DELETE') { // Filter for specific tags with "?tag=foo || bar" $tagNames = !empty($this->queryParams['tag']) ? explode(' || ', $this->queryParams['tag']) : array(); Zotero_DB::beginTransaction(); foreach ($tagNames as $tagName) { $tagIDs = Zotero_Tags::getIDs($this->objectLibraryID, $tagName); foreach ($tagIDs as $tagID) { $tag = Zotero_Tags::get($this->objectLibraryID, $tagID, true); Zotero_Tags::delete($this->objectLibraryID, $tag->key); } } Zotero_DB::commit(); $this->e204(); } else { $title = "Tags"; $results = Zotero_Tags::search($this->objectLibraryID, $this->queryParams); } } } if ($tagIDs) { $this->queryParams['tagIDs'] = $tagIDs; $results = Zotero_Tags::search($this->objectLibraryID, $this->queryParams); } $this->generateMultiResponse($results, $title, $fixedValues); $this->end(); }