/** * Converts a Zotero_Tag object to a SimpleXMLElement item * * @param object $item Zotero_Tag object * @return SimpleXMLElement Tag data as SimpleXML element */ public static function convertTagToXML(Zotero_Tag $tag, $syncMode = false) { return $tag->toXML($syncMode); }
public function save($userID=false) { if (!$this->_libraryID) { trigger_error("Library ID must be set before saving", E_USER_ERROR); } Zotero_Items::editCheck($this, $userID); if (!$this->hasChanged()) { Z_Core::debug("Item $this->id has not changed"); return false; } $this->cacheEnabled = false; // Make sure there are no gaps in the creator indexes $creators = $this->getCreators(); $lastPos = -1; foreach ($creators as $pos=>$creator) { if ($pos != $lastPos + 1) { trigger_error("Creator index $pos out of sequence for item $this->id", E_USER_ERROR); } $lastPos++; } $shardID = Zotero_Shards::getByLibraryID($this->_libraryID); $env = []; Zotero_DB::beginTransaction(); try { // // New item, insert and return id // if (!$this->id || (empty($this->changed['version']) && !$this->exists())) { Z_Core::debug('Saving data for new item to database'); $isNew = $env['isNew'] = true; $sqlColumns = array(); $sqlValues = array(); // // Primary fields // $itemID = $this->_id = $this->_id ? $this->_id : Zotero_ID::get('items'); $key = $this->_key = $this->_key ? $this->_key : Zotero_ID::getKey(); $sqlColumns = array( 'itemID', 'itemTypeID', 'libraryID', 'key', 'dateAdded', 'dateModified', 'serverDateModified', 'version' ); $timestamp = Zotero_DB::getTransactionTimestamp(); $dateAdded = $this->_dateAdded ? $this->_dateAdded : $timestamp; $dateModified = $this->_dateModified ? $this->_dateModified : $timestamp; $version = Zotero_Libraries::getUpdatedVersion($this->_libraryID); $sqlValues = array( $itemID, $this->_itemTypeID, $this->_libraryID, $key, $dateAdded, $dateModified, $timestamp, $version ); $sql = 'INSERT INTO items (`' . implode('`, `', $sqlColumns) . '`) VALUES ('; // Insert placeholders for bind parameters for ($i=0; $i<sizeOf($sqlValues); $i++) { $sql .= '?, '; } $sql = substr($sql, 0, -2) . ')'; // Save basic data to items table try { $insertID = Zotero_DB::query($sql, $sqlValues, $shardID); } catch (Exception $e) { if (strpos($e->getMessage(), "Incorrect datetime value") !== false) { preg_match("/Incorrect datetime value: '([^']+)'/", $e->getMessage(), $matches); throw new Exception("=Invalid date value '{$matches[1]}' for item $key", Z_ERROR_INVALID_INPUT); } throw $e; } if (!$this->_id) { if (!$insertID) { throw new Exception("Item id not available after INSERT"); } $itemID = $insertID; $this->_serverDateModified = $timestamp; } // Group item data if (Zotero_Libraries::getType($this->_libraryID) == 'group' && $userID) { $sql = "INSERT INTO groupItems VALUES (?, ?, ?)"; Zotero_DB::query($sql, array($itemID, $userID, $userID), $shardID); } // // ItemData // if (!empty($this->changed['itemData'])) { // Use manual bound parameters to speed things up $origInsertSQL = "INSERT INTO itemData (itemID, fieldID, value) VALUES "; $insertSQL = $origInsertSQL; $insertParams = array(); $insertCounter = 0; $maxInsertGroups = 40; $max = Zotero_Items::$maxDataValueLength; $fieldIDs = array_keys($this->changed['itemData']); foreach ($fieldIDs as $fieldID) { $value = $this->getField($fieldID, true, false, true); if ($value == 'CURRENT_TIMESTAMP' && Zotero_ItemFields::getID('accessDate') == $fieldID) { $value = Zotero_DB::getTransactionTimestamp(); } // Check length if (strlen($value) > $max) { $fieldName = Zotero_ItemFields::getLocalizedString( $this->_itemTypeID, $fieldID ); $msg = "=$fieldName field value " . "'" . mb_substr($value, 0, 50) . "...' too long"; if ($this->_key) { $msg .= " for item '" . $this->_libraryID . "/" . $key . "'"; } throw new Exception($msg, Z_ERROR_FIELD_TOO_LONG); } if ($insertCounter < $maxInsertGroups) { $insertSQL .= "(?,?,?),"; $insertParams = array_merge( $insertParams, array($itemID, $fieldID, $value) ); } if ($insertCounter == $maxInsertGroups - 1) { $insertSQL = substr($insertSQL, 0, -1); $stmt = Zotero_DB::getStatement($insertSQL, true, $shardID); Zotero_DB::queryFromStatement($stmt, $insertParams); $insertSQL = $origInsertSQL; $insertParams = array(); $insertCounter = -1; } $insertCounter++; } if ($insertCounter > 0 && $insertCounter < $maxInsertGroups) { $insertSQL = substr($insertSQL, 0, -1); $stmt = Zotero_DB::getStatement($insertSQL, true, $shardID); Zotero_DB::queryFromStatement($stmt, $insertParams); } } // // Creators // if (!empty($this->changed['creators'])) { $indexes = array_keys($this->changed['creators']); // TODO: group queries $sql = "INSERT INTO itemCreators (itemID, creatorID, creatorTypeID, orderIndex) VALUES "; $placeholders = array(); $sqlValues = array(); $cacheRows = array(); foreach ($indexes as $orderIndex) { Z_Core::debug('Adding creator in position ' . $orderIndex, 4); $creator = $this->getCreator($orderIndex); if (!$creator) { continue; } if ($creator['ref']->hasChanged()) { Z_Core::debug("Auto-saving changed creator {$creator['ref']->id}"); try { $creator['ref']->save(); } catch (Exception $e) { // TODO: Provide the item in question /*if (strpos($e->getCode() == Z_ERROR_CREATOR_TOO_LONG)) { $msg = $e->getMessage(); $msg = str_replace( "with this name and shorten it.", "with this name, or paste '$key' into the quick search bar " . "in the Zotero toolbar, and shorten the name." ); throw new Exception($msg, Z_ERROR_CREATOR_TOO_LONG); }*/ throw $e; } } $placeholders[] = "(?, ?, ?, ?)"; array_push( $sqlValues, $itemID, $creator['ref']->id, $creator['creatorTypeID'], $orderIndex ); $cacheRows[] = array( 'creatorID' => $creator['ref']->id, 'creatorTypeID' => $creator['creatorTypeID'], 'orderIndex' => $orderIndex ); } if ($sqlValues) { $sql = $sql . implode(',', $placeholders); Zotero_DB::query($sql, $sqlValues, $shardID); } } // Deleted item if (!empty($this->changed['deleted'])) { $deleted = $this->getDeleted(); if ($deleted) { $sql = "REPLACE INTO deletedItems (itemID) VALUES (?)"; } else { $sql = "DELETE FROM deletedItems WHERE itemID=?"; } Zotero_DB::query($sql, $itemID, $shardID); } // Note if ($this->isNote() || !empty($this->changed['note'])) { if (!is_string($this->noteText)) { $this->noteText = ''; } // If we don't have a sanitized note, generate one if (is_null($this->noteTextSanitized)) { $noteTextSanitized = Zotero_Notes::sanitize($this->noteText); // But if note is sanitized already, store empty string if ($this->noteText === $noteTextSanitized) { $this->noteTextSanitized = ''; } else { $this->noteTextSanitized = $noteTextSanitized; } } $this->noteTitle = Zotero_Notes::noteToTitle( $this->noteTextSanitized === '' ? $this->noteText : $this->noteTextSanitized ); $sql = "INSERT INTO itemNotes (itemID, sourceItemID, note, noteSanitized, title, hash) VALUES (?,?,?,?,?,?)"; $parent = $this->isNote() ? $this->getSource() : null; $hash = $this->noteText ? md5($this->noteText) : ''; $bindParams = array( $itemID, $parent ? $parent : null, $this->noteText !== null ? $this->noteText : '', $this->noteTextSanitized, $this->noteTitle, $hash ); try { Zotero_DB::query($sql, $bindParams, $shardID); } catch (Exception $e) { if (strpos($e->getMessage(), "Incorrect string value") !== false) { throw new Exception("=Invalid character in note '" . Zotero_Utilities::ellipsize($title, 70) . "'", Z_ERROR_INVALID_INPUT); } throw ($e); } Zotero_Notes::updateNoteCache($this->_libraryID, $itemID, $this->noteText); Zotero_Notes::updateHash($this->_libraryID, $itemID, $hash); } // Attachment if ($this->isAttachment()) { $sql = "INSERT INTO itemAttachments (itemID, sourceItemID, linkMode, mimeType, charsetID, path, storageModTime, storageHash) VALUES (?,?,?,?,?,?,?,?)"; $parent = $this->getSource(); if ($parent) { $parentItem = Zotero_Items::get($this->_libraryID, $parent); if (!$parentItem) { throw new Exception("Parent item $parent not found"); } if ($parentItem->getSource()) { $parentKey = $parentItem->key; throw new Exception("=Parent item $parentKey cannot be a child attachment", Z_ERROR_INVALID_INPUT); } } $linkMode = $this->attachmentLinkMode; $charsetID = Zotero_CharacterSets::getID($this->attachmentCharset); $path = $this->attachmentPath; $storageModTime = $this->attachmentStorageModTime; $storageHash = $this->attachmentStorageHash; $bindParams = array( $itemID, $parent ? $parent : null, $linkMode + 1, $this->attachmentMIMEType, $charsetID ? $charsetID : null, $path ? $path : '', $storageModTime ? $storageModTime : null, $storageHash ? $storageHash : null ); Zotero_DB::query($sql, $bindParams, $shardID); } // Sort fields $sortTitle = Zotero_Items::getSortTitle($this->getDisplayTitle(true)); if (mb_substr($sortTitle, 0, 5) == mb_substr($this->getField('title', false, true), 0, 5)) { $sortTitle = null; } $creatorSummary = $this->isRegularItem() ? mb_strcut($this->getCreatorSummary(true), 0, Zotero_Creators::$creatorSummarySortLength) : ''; $sql = "INSERT INTO itemSortFields (itemID, sortTitle, creatorSummary) VALUES (?, ?, ?)"; Zotero_DB::query($sql, array($itemID, $sortTitle, $creatorSummary), $shardID); // // Source item id // if ($sourceItemID = $this->getSource()) { $newSourceItem = Zotero_Items::get($this->_libraryID, $sourceItemID); if (!$newSourceItem) { throw new Exception("Cannot set source to invalid item"); } switch (Zotero_ItemTypes::getName($this->_itemTypeID)) { case 'note': $newSourceItem->incrementNoteCount(); break; case 'attachment': $newSourceItem->incrementAttachmentCount(); break; } } // Collections if (!empty($this->changed['collections'])) { foreach ($this->collections as $collectionKey) { $collection = Zotero_Collections::getByLibraryAndKey($this->_libraryID, $collectionKey); if (!$collection) { throw new Exception("Collection with key '$collectionKey' not found", Z_ERROR_COLLECTION_NOT_FOUND); } $collection->addItem($itemID); $collection->save(); } } // Tags if (!empty($this->changed['tags'])) { foreach ($this->tags as $tag) { $tagID = Zotero_Tags::getID($this->libraryID, $tag->tag, $tag->type); if ($tagID) { $tagObj = Zotero_Tags::get($this->_libraryID, $tagID); } else { $tagObj = new Zotero_Tag; $tagObj->libraryID = $this->_libraryID; $tagObj->name = $tag->tag; $tagObj->type = (int) $tag->type ? $tag->type : 0; } $tagObj->addItem($this->_key); $tagObj->save(); } } // Related items if (!empty($this->changed['relations'])) { $uri = Zotero_URI::getItemURI($this); $sql = "INSERT IGNORE INTO relations " . "(relationID, libraryID, `key`, subject, predicate, object) " . "VALUES (?, ?, ?, ?, ?, ?)"; $insertStatement = Zotero_DB::getStatement($sql, false, $shardID); foreach ($this->relations as $rel) { $insertStatement->execute( array( Zotero_ID::get('relations'), $this->_libraryID, Zotero_Relations::makeKey($uri, $rel[0], $rel[1]), $uri, $rel[0], $rel[1] ) ); } } // Remove from delete log if it's there $sql = "DELETE FROM syncDeleteLogKeys WHERE libraryID=? AND objectType='item' AND `key`=?"; Zotero_DB::query($sql, array($this->_libraryID, $key), $shardID); } // // Existing item, update // else { Z_Core::debug('Updating database with new item data for item ' . $this->_libraryID . '/' . $this->_key, 4); $isNew = $env['isNew'] = false; // // Primary fields // $sql = "UPDATE items SET "; $sqlValues = array(); $timestamp = Zotero_DB::getTransactionTimestamp(); $version = Zotero_Libraries::getUpdatedVersion($this->_libraryID); $updateFields = array( 'itemTypeID', 'libraryID', 'key', 'dateAdded', 'dateModified' ); if (!empty($this->changed['primaryData'])) { foreach ($updateFields as $updateField) { if (in_array($updateField, $this->changed['primaryData'])) { $sql .= "`$updateField`=?, "; $sqlValues[] = $this->{"_$updateField"}; } } } $sql .= "serverDateModified=?, version=? WHERE itemID=?"; array_push( $sqlValues, $timestamp, $version, $this->_id ); Zotero_DB::query($sql, $sqlValues, $shardID); $this->_serverDateModified = $timestamp; // Group item data if (Zotero_Libraries::getType($this->_libraryID) == 'group' && $userID) { $sql = "INSERT INTO groupItems VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE lastModifiedByUserID=?"; Zotero_DB::query($sql, array($this->_id, null, $userID, $userID), $shardID); } // // ItemData // if (!empty($this->changed['itemData'])) { $del = array(); $origReplaceSQL = "REPLACE INTO itemData (itemID, fieldID, value) VALUES "; $replaceSQL = $origReplaceSQL; $replaceParams = array(); $replaceCounter = 0; $maxReplaceGroups = 40; $max = Zotero_Items::$maxDataValueLength; $fieldIDs = array_keys($this->changed['itemData']); foreach ($fieldIDs as $fieldID) { $value = $this->getField($fieldID, true, false, true); // If field changed and is empty, mark row for deletion if ($value === "") { $del[] = $fieldID; continue; } if ($value == 'CURRENT_TIMESTAMP' && Zotero_ItemFields::getID('accessDate') == $fieldID) { $value = Zotero_DB::getTransactionTimestamp(); } // Check length if (strlen($value) > $max) { $fieldName = Zotero_ItemFields::getLocalizedString( $this->_itemTypeID, $fieldID ); $msg = "=$fieldName field value " . "'" . mb_substr($value, 0, 50) . "...' too long"; if ($this->_key) { $msg .= " for item '" . $this->_libraryID . "/" . $this->_key . "'"; } throw new Exception($msg, Z_ERROR_FIELD_TOO_LONG); } if ($replaceCounter < $maxReplaceGroups) { $replaceSQL .= "(?,?,?),"; $replaceParams = array_merge($replaceParams, array($this->_id, $fieldID, $value) ); } if ($replaceCounter == $maxReplaceGroups - 1) { $replaceSQL = substr($replaceSQL, 0, -1); $stmt = Zotero_DB::getStatement($replaceSQL, true, $shardID); Zotero_DB::queryFromStatement($stmt, $replaceParams); $replaceSQL = $origReplaceSQL; $replaceParams = array(); $replaceCounter = -1; } $replaceCounter++; } if ($replaceCounter > 0 && $replaceCounter < $maxReplaceGroups) { $replaceSQL = substr($replaceSQL, 0, -1); $stmt = Zotero_DB::getStatement($replaceSQL, true, $shardID); Zotero_DB::queryFromStatement($stmt, $replaceParams); } // Update memcached with used fields $fids = array(); foreach ($this->itemData as $fieldID=>$value) { if ($value !== false && $value !== null) { $fids[] = $fieldID; } } // Delete blank fields if ($del) { $sql = 'DELETE from itemData WHERE itemID=? AND fieldID IN ('; $sqlParams = array($this->_id); foreach ($del as $d) { $sql .= '?, '; $sqlParams[] = $d; } $sql = substr($sql, 0, -2) . ')'; Zotero_DB::query($sql, $sqlParams, $shardID); } } // // Creators // if (!empty($this->changed['creators'])) { $indexes = array_keys($this->changed['creators']); $sql = "INSERT INTO itemCreators (itemID, creatorID, creatorTypeID, orderIndex) VALUES "; $placeholders = array(); $sqlValues = array(); $cacheRows = array(); foreach ($indexes as $orderIndex) { Z_Core::debug('Creator in position ' . $orderIndex . ' has changed', 4); $creator = $this->getCreator($orderIndex); $sql2 = 'DELETE FROM itemCreators WHERE itemID=? AND orderIndex=?'; Zotero_DB::query($sql2, array($this->_id, $orderIndex), $shardID); if (!$creator) { continue; } if ($creator['ref']->hasChanged()) { Z_Core::debug("Auto-saving changed creator {$creator['ref']->id}"); $creator['ref']->save(); } $placeholders[] = "(?, ?, ?, ?)"; array_push( $sqlValues, $this->_id, $creator['ref']->id, $creator['creatorTypeID'], $orderIndex ); } if ($sqlValues) { $sql = $sql . implode(',', $placeholders); Zotero_DB::query($sql, $sqlValues, $shardID); } } // Deleted item if (!empty($this->changed['deleted'])) { $deleted = $this->getDeleted(); if ($deleted) { $sql = "REPLACE INTO deletedItems (itemID) VALUES (?)"; } else { $sql = "DELETE FROM deletedItems WHERE itemID=?"; } Zotero_DB::query($sql, $this->_id, $shardID); } // In case this was previously a standalone item, // delete from any collections it may have been in if (!empty($this->changed['source']) && $this->getSource()) { $sql = "DELETE FROM collectionItems WHERE itemID=?"; Zotero_DB::query($sql, $this->_id, $shardID); } // // Note or attachment note // if (!empty($this->changed['note'])) { // If we don't have a sanitized note, generate one if (is_null($this->noteTextSanitized)) { $noteTextSanitized = Zotero_Notes::sanitize($this->noteText); // But if note is sanitized already, store empty string if ($this->noteText == $noteTextSanitized) { $this->noteTextSanitized = ''; } else { $this->noteTextSanitized = $noteTextSanitized; } } $this->noteTitle = Zotero_Notes::noteToTitle( $this->noteTextSanitized === '' ? $this->noteText : $this->noteTextSanitized ); // Only record sourceItemID in itemNotes for notes if ($this->isNote()) { $sourceItemID = $this->getSource(); } $sourceItemID = !empty($sourceItemID) ? $sourceItemID : null; $hash = $this->noteText ? md5($this->noteText) : ''; $sql = "INSERT INTO itemNotes (itemID, sourceItemID, note, noteSanitized, title, hash) VALUES (?,?,?,?,?,?) ON DUPLICATE KEY UPDATE sourceItemID=?, note=?, noteSanitized=?, title=?, hash=?"; $bindParams = array( $this->_id, $sourceItemID, $this->noteText, $this->noteTextSanitized, $this->noteTitle, $hash, $sourceItemID, $this->noteText, $this->noteTextSanitized, $this->noteTitle, $hash ); Zotero_DB::query($sql, $bindParams, $shardID); Zotero_Notes::updateNoteCache($this->_libraryID, $this->_id, $this->noteText); Zotero_Notes::updateHash($this->_libraryID, $this->_id, $hash); // TODO: handle changed source? } // Attachment if (!empty($this->changed['attachmentData'])) { $sql = "INSERT INTO itemAttachments (itemID, sourceItemID, linkMode, mimeType, charsetID, path, storageModTime, storageHash) VALUES (?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE sourceItemID=VALUES(sourceItemID), linkMode=VALUES(linkMode), mimeType=VALUES(mimeType), charsetID=VALUES(charsetID), path=VALUES(path), storageModTime=VALUES(storageModTime), storageHash=VALUES(storageHash)"; $parent = $this->getSource(); if ($parent) { $parentItem = Zotero_Items::get($this->_libraryID, $parent); if (!$parentItem) { throw new Exception("Parent item $parent not found"); } if ($parentItem->getSource()) { $parentKey = $parentItem->key; throw new Exception("=Parent item $parentKey cannot be a child attachment", Z_ERROR_INVALID_INPUT); } } $linkMode = $this->attachmentLinkMode; $charsetID = Zotero_CharacterSets::getID($this->attachmentCharset); $path = $this->attachmentPath; $storageModTime = $this->attachmentStorageModTime; $storageHash = $this->attachmentStorageHash; $bindParams = array( $this->_id, $parent ? $parent : null, $linkMode + 1, $this->attachmentMIMEType, $charsetID ? $charsetID : null, $path ? $path : '', $storageModTime ? $storageModTime : null, $storageHash ? $storageHash : null ); Zotero_DB::query($sql, $bindParams, $shardID); } // Sort fields if (!empty($this->changed['primaryData']['itemTypeID']) || !empty($this->changed['itemData']) || !empty($this->changed['creators'])) { $sql = "UPDATE itemSortFields SET sortTitle=?"; $params = array(); $sortTitle = Zotero_Items::getSortTitle($this->getDisplayTitle(true)); if (mb_substr($sortTitle, 0, 5) == mb_substr($this->getField('title', false, true), 0, 5)) { $sortTitle = null; } $params[] = $sortTitle; if (!empty($this->changed['creators'])) { $creatorSummary = mb_strcut($this->getCreatorSummary(true), 0, Zotero_Creators::$creatorSummarySortLength); $sql .= ", creatorSummary=?"; $params[] = $creatorSummary; } $sql .= " WHERE itemID=?"; $params[] = $this->_id; Zotero_DB::query($sql, $params, $shardID); } // // Source item id // if (!empty($this->changed['source'])) { $type = Zotero_ItemTypes::getName($this->_itemTypeID); $Type = ucwords($type); // Update DB, if not a note or attachment we already changed above if (empty($this->changed['attachmentData']) && (empty($this->changed['note']) || !$this->isNote())) { $sql = "UPDATE item" . $Type . "s SET sourceItemID=? WHERE itemID=?"; $parent = $this->getSource(); $bindParams = array( $parent ? $parent : null, $this->_id ); Zotero_DB::query($sql, $bindParams, $shardID); } } if (false && !empty($this->changed['source'])) { trigger_error("Unimplemented", E_USER_ERROR); $newItem = Zotero_Items::get($this->_libraryID, $sourceItemID); // FK check if ($newItem) { if ($sourceItemID) { } else { trigger_error("Cannot set $type source to invalid item $sourceItemID", E_USER_ERROR); } } $oldSourceItemID = $this->getSource(); if ($oldSourceItemID == $sourceItemID) { Z_Core::debug("$Type source hasn't changed", 4); } else { $oldItem = Zotero_Items::get($this->_libraryID, $oldSourceItemID); if ($oldSourceItemID && $oldItem) { } else { //$oldItemNotifierData = null; Z_Core::debug("Old source item $oldSourceItemID didn't exist in setSource()", 2); } // If this was an independent item, remove from any collections where it // existed previously and add source instead if there is one if (!$oldSourceItemID) { $sql = "SELECT collectionID FROM collectionItems WHERE itemID=?"; $changedCollections = Zotero_DB::query($sql, $itemID, $shardID); if ($changedCollections) { trigger_error("Unimplemented", E_USER_ERROR); if ($sourceItemID) { $sql = "UPDATE OR REPLACE collectionItems " . "SET itemID=? WHERE itemID=?"; Zotero_DB::query($sql, array($sourceItemID, $this->_id), $shardID); } else { $sql = "DELETE FROM collectionItems WHERE itemID=?"; Zotero_DB::query($sql, $this->_id, $shardID); } } } $sql = "UPDATE item{$Type}s SET sourceItemID=? WHERE itemID=?"; $bindParams = array( $sourceItemID ? $sourceItemID : null, $itemID ); Zotero_DB::query($sql, $bindParams, $shardID); //Zotero.Notifier.trigger('modify', 'item', $this->_id, notifierData); // Update the counts of the previous and new sources if ($oldItem) { /* switch ($type) { case 'note': $oldItem->decrementNoteCount(); break; case 'attachment': $oldItem->decrementAttachmentCount(); break; } */ //Zotero.Notifier.trigger('modify', 'item', oldSourceItemID, oldItemNotifierData); } if ($newItem) { /* switch ($type) { case 'note': $newItem->incrementNoteCount(); break; case 'attachment': $newItem->incrementAttachmentCount(); break; } */ //Zotero.Notifier.trigger('modify', 'item', sourceItemID, newItemNotifierData); } } } // Collections if (!empty($this->changed['collections'])) { $oldCollections = $this->previousData['collections']; $newCollections = $this->collections; $toAdd = array_diff($newCollections, $oldCollections); $toRemove = array_diff($oldCollections, $newCollections); foreach ($toAdd as $collectionKey) { $collection = Zotero_Collections::getByLibraryAndKey($this->_libraryID, $collectionKey); if (!$collection) { throw new Exception("Collection with key '$collectionKey' not found", Z_ERROR_COLLECTION_NOT_FOUND); } $collection->addItem($this->_id); $collection->save(); } foreach ($toRemove as $collectionKey) { $collection = Zotero_Collections::getByLibraryAndKey($this->_libraryID, $collectionKey); $collection->removeItem($this->_id); $collection->save(); } } if (!empty($this->changed['tags'])) { $oldTags = $this->previousData['tags']; $newTags = $this->tags; $toAdd = []; $toRemove = []; // Get new tags not in existing for ($i=0, $len=sizeOf($newTags); $i<$len; $i++) { if (!isset($newTags[$i]->type)) { $newTags[$i]->type = 0; } $name = trim($newTags[$i]->tag); $type = $newTags[$i]->type; foreach ($oldTags as $tag) { // Do a case-insensitive comparison, to match the client if (strtolower($tag->name) == strtolower($name) && $tag->type == $type) { continue 2; } } $toAdd[] = $newTags[$i]; } // Get existing tags not in new for ($i=0, $len=sizeOf($oldTags); $i<$len; $i++) { $name = $oldTags[$i]->name; $type = $oldTags[$i]->type; foreach ($newTags as $tag) { if (strtolower($tag->tag) == strtolower($name) && $tag->type == $type) { continue 2; } } $toRemove[] = $oldTags[$i]; } foreach ($toAdd as $tag) { $name = $tag->tag; $type = $tag->type; $tagID = Zotero_Tags::getID($this->_libraryID, $name, $type, true); if (!$tagID) { $tag = new Zotero_Tag; $tag->libraryID = $this->_libraryID; $tag->name = $name; $tag->type = $type; $tagID = $tag->save(); } $tag = Zotero_Tags::get($this->_libraryID, $tagID); $tag->addItem($this->_key); $tag->save(); } foreach ($toRemove as $tag) { $tag->removeItem($this->_key); $tag->save(); } } // Related items if (!empty($this->changed['relations'])) { $removed = []; $new = []; $current = $this->relations; // TEMP // Convert old-style related items into relations $sql = "SELECT `key` FROM itemRelated IR " . "JOIN items I ON (IR.linkedItemID=I.itemID) " . "WHERE IR.itemID=?"; $toMigrate = Zotero_DB::columnQuery($sql, $this->_id, $shardID); if ($toMigrate) { $prefix = Zotero_URI::getLibraryURI($this->_libraryID) . "/items/"; $new = array_map(function ($key) use ($prefix) { return [ Zotero_Relations::$relatedItemPredicate, $prefix . $key ]; }, $toMigrate); $sql = "DELETE FROM itemRelated WHERE itemID=?"; Zotero_DB::query($sql, $this->_id, $shardID); } foreach ($this->previousData['relations'] as $rel) { if (array_search($rel, $current) === false) { $removed[] = $rel; } } foreach ($current as $rel) { if (array_search($rel, $this->previousData['relations']) !== false) { continue; } $new[] = $rel; } $uri = Zotero_URI::getItemURI($this); if ($removed) { $sql = "DELETE FROM relations WHERE libraryID=? AND `key`=?"; $deleteStatement = Zotero_DB::getStatement($sql, false, $shardID); foreach ($removed as $rel) { $params = [ $this->_libraryID, Zotero_Relations::makeKey($uri, $rel[0], $rel[1]) ]; $deleteStatement->execute($params); // TEMP // For owl:sameAs, delete reverse as well, since the client // can save that way if ($rel[0] == Zotero_Relations::$linkedObjectPredicate) { $params = [ $this->_libraryID, Zotero_Relations::makeKey($rel[1], $rel[0], $uri) ]; $deleteStatement->execute($params); } } } if ($new) { $sql = "INSERT IGNORE INTO relations " . "(relationID, libraryID, `key`, subject, predicate, object) " . "VALUES (?, ?, ?, ?, ?, ?)"; $insertStatement = Zotero_DB::getStatement($sql, false, $shardID); foreach ($new as $rel) { $insertStatement->execute( array( Zotero_ID::get('relations'), $this->_libraryID, Zotero_Relations::makeKey($uri, $rel[0], $rel[1]), $uri, $rel[0], $rel[1] ) ); // If adding a related item, the version on that item has to be // updated as well (if it exists). Otherwise, requests for that // item will return cached data without the new relation. if ($rel[0] == Zotero_Relations::$relatedItemPredicate) { $relatedItem = Zotero_URI::getURIItem($rel[1]); if (!$relatedItem) { Z_Core::debug("Related item " . $rel[1] . " does not exist " . "for item " . $this->_libraryKey); continue; } // If item has already changed, assume something else is taking // care of saving it and don't do so now, to avoid endless loops // with circular relations if ($relatedItem->hasChanged()) { continue; } $relatedItem->updateVersion($userID); } } } } } Zotero_DB::commit(); } catch (Exception $e) { Zotero_DB::rollback(); throw ($e); } $this->cacheEnabled = false; $this->finalizeSave($env); if ($isNew) { Zotero_Notifier::trigger('add', 'item', $this->_libraryID . "/" . $this->_key); return $this->_id; } Zotero_Notifier::trigger('modify', 'item', $this->_libraryID . "/" . $this->_key); return true; }
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(); }
/** * $tags is an array of objects with properties 'tag' and 'type' */ public function setTags($newTags) { if (!$this->id) { throw new Exception('itemID not set'); } $numTags = $this->numTags(); if (!$newTags && !$numTags) { return false; } Zotero_DB::beginTransaction(); $existingTags = $this->getTags(); $toAdd = array(); $toRemove = array(); // Get new tags not in existing for ($i = 0, $len = sizeOf($newTags); $i < $len; $i++) { if (!isset($newTags[$i]->type)) { $newTags[$i]->type = 0; } $name = trim($newTags[$i]->tag); // 'tag', not 'name', since that's what JSON uses $type = $newTags[$i]->type; foreach ($existingTags as $tag) { // Do a case-insensitive comparison, to match the client if (strtolower($tag->name) == strtolower($name) && $tag->type == $type) { continue 2; } } $toAdd[] = $newTags[$i]; } // Get existing tags not in new for ($i = 0, $len = sizeOf($existingTags); $i < $len; $i++) { $name = $existingTags[$i]->name; $type = $existingTags[$i]->type; foreach ($newTags as $tag) { if (strtolower($tag->tag) == strtolower($name) && $tag->type == $type) { continue 2; } } $toRemove[] = $existingTags[$i]; } foreach ($toAdd as $tag) { $name = $tag->tag; $type = $tag->type; $tagID = Zotero_Tags::getID($this->libraryID, $name, $type, true); if (!$tagID) { $tag = new Zotero_Tag(); $tag->libraryID = $this->libraryID; $tag->name = $name; $tag->type = $type; $tagID = $tag->save(); } $tag = Zotero_Tags::get($this->libraryID, $tagID); $tag->addItem($this->id); $tag->save(); } foreach ($toRemove as $tag) { $tag->removeItem($this->id); $tag->save(); } Zotero_DB::commit(); return $toAdd || $toRemove; }
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(); }