public static function indexItem(Zotero_Item $item, $content, $stats = array()) { if (!$item->isAttachment()) { throw new Exception("Full-text content can only be added for attachments", Z_ERROR_INVALID_INPUT); } Zotero_DB::beginTransaction(); $libraryID = $item->libraryID; $key = $item->key; $version = Zotero_Libraries::getUpdatedVersion($item->libraryID); $timestamp = Zotero_DB::transactionInProgress() ? Zotero_DB::getTransactionTimestamp() : date("Y-m-d H:i:s"); // Add to MySQL for syncing, since Elasticsearch doesn't refresh immediately $sql = "REPLACE INTO itemFulltext ("; $fields = ["itemID", "version", "timestamp"]; $params = [$item->id, $version, $timestamp]; $sql .= implode(", ", $fields) . ") VALUES (" . implode(', ', array_fill(0, sizeOf($params), '?')) . ")"; Zotero_DB::query($sql, $params, Zotero_Shards::getByLibraryID($libraryID)); // Add to Elasticsearch self::indexItemInElasticsearch($libraryID, $key, $version, $timestamp, $content, $stats); Zotero_DB::commit(); }
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(); }
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 static function delete($libraryID, $key) { $table = self::$table; $type = self::$objectType; $types = self::$objectTypePlural; if (!$key) { throw new Exception("Invalid key {$key}"); } // Get object (and trigger caching) $obj = self::getByLibraryAndKey($libraryID, $key); if (!$obj) { return; } self::editCheck($obj); Z_Core::debug("Deleting {$type} {$libraryID}/{$key}", 4); $shardID = Zotero_Shards::getByLibraryID($libraryID); Zotero_DB::beginTransaction(); // Delete child items if ($type == 'item') { if ($obj->isRegularItem()) { $children = array_merge($obj->getNotes(), $obj->getAttachments()); if ($children) { $children = Zotero_Items::get($libraryID, $children); foreach ($children as $child) { self::delete($child->libraryID, $child->key); } } } // Remove relations (except for merge tracker) $uri = Zotero_URI::getItemURI($obj); Zotero_Relations::eraseByURI($libraryID, $uri, array(Zotero_Relations::$deletedItemPredicate)); } else { if ($type == 'tag') { $tagName = $obj->name; } } if ($type == 'item' && $obj->isAttachment()) { Zotero_FullText::deleteItemContent($obj); } $sql = "DELETE FROM {$table} WHERE libraryID=? AND `key`=?"; $deleted = Zotero_DB::query($sql, array($libraryID, $key), $shardID); self::unload($obj->id); if ($deleted) { $sql = "INSERT INTO syncDeleteLogKeys\n\t\t\t\t\t\t(libraryID, objectType, `key`, timestamp, version)\n\t\t\t\t\t\tVALUES (?, '{$type}', ?, ?, ?)\n\t\t\t\t\t\tON DUPLICATE KEY UPDATE timestamp=?, version=?"; $timestamp = Zotero_DB::getTransactionTimestamp(); $version = Zotero_Libraries::getUpdatedVersion($libraryID); $params = array($libraryID, $key, $timestamp, $version, $timestamp, $version); Zotero_DB::query($sql, $params, $shardID); if ($type == 'tag') { $sql = "INSERT INTO syncDeleteLogKeys\n\t\t\t\t\t\t\t(libraryID, objectType, `key`, timestamp, version)\n\t\t\t\t\t\t\tVALUES (?, 'tagName', ?, ?, ?)\n\t\t\t\t\t\t\tON DUPLICATE KEY UPDATE timestamp=?, version=?"; $params = array($libraryID, $tagName, $timestamp, $version, $timestamp, $version); Zotero_DB::query($sql, $params, $shardID); } } Zotero_DB::commit(); }
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; }
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 save($userID = false) { if (!$this->libraryID) { throw new Exception("Library ID must be set before saving"); } Zotero_Searches::editCheck($this, $userID); if (!$this->changed) { Z_Core::debug("Search {$this->id} has not changed"); return false; } if (!isset($this->name) || $this->name === '') { throw new Exception("Name not provided for saved search"); } $shardID = Zotero_Shards::getByLibraryID($this->libraryID); Zotero_DB::beginTransaction(); $isNew = !$this->id || !$this->exists(); try { $searchID = $this->id ? $this->id : Zotero_ID::get('savedSearches'); Z_Core::debug("Saving search {$this->id}"); if (!$isNew) { $sql = "DELETE FROM savedSearchConditions WHERE searchID=?"; Zotero_DB::query($sql, $searchID, $shardID); } $key = $this->key ? $this->key : Zotero_ID::getKey(); $fields = "searchName=?, libraryID=?, `key`=?, dateAdded=?, dateModified=?,\n\t\t\t\t\t\tserverDateModified=?, version=?"; $timestamp = Zotero_DB::getTransactionTimestamp(); $params = array($this->name, $this->libraryID, $key, $this->dateAdded ? $this->dateAdded : $timestamp, $this->dateModified ? $this->dateModified : $timestamp, $timestamp, Zotero_Libraries::getUpdatedVersion($this->libraryID)); $shardID = Zotero_Shards::getByLibraryID($this->libraryID); if ($isNew) { $sql = "INSERT INTO savedSearches SET searchID=?, {$fields}"; $stmt = Zotero_DB::getStatement($sql, true, $shardID); Zotero_DB::queryFromStatement($stmt, array_merge(array($searchID), $params)); // Remove from delete log if it's there $sql = "DELETE FROM syncDeleteLogKeys WHERE libraryID=? AND objectType='search' AND `key`=?"; Zotero_DB::query($sql, array($this->libraryID, $key), $shardID); } else { $sql = "UPDATE savedSearches SET {$fields} WHERE searchID=?"; $stmt = Zotero_DB::getStatement($sql, true, $shardID); Zotero_DB::queryFromStatement($stmt, array_merge($params, array($searchID))); } foreach ($this->conditions as $searchConditionID => $condition) { $sql = "INSERT INTO savedSearchConditions (searchID,\n\t\t\t\t\t\tsearchConditionID, `condition`, mode, operator,\n\t\t\t\t\t\tvalue, required) VALUES (?,?,?,?,?,?,?)"; $sqlParams = array($searchID, $searchConditionID + 1, $condition['condition'], $condition['mode'] ? $condition['mode'] : '', $condition['operator'] ? $condition['operator'] : '', $condition['value'] ? $condition['value'] : '', !empty($condition['required']) ? 1 : 0); try { Zotero_DB::query($sql, $sqlParams, $shardID); } catch (Exception $e) { $msg = $e->getMessage(); if (strpos($msg, "Data too long for column 'value'") !== false) { throw new Exception("=Value '" . mb_substr($condition['value'], 0, 75) . "…' too long in saved search '" . $this->name . "'"); } throw $e; } } Zotero_DB::commit(); } catch (Exception $e) { Zotero_DB::rollback(); throw $e; } if (!$this->id) { $this->id = $searchID; } if (!$this->key) { $this->key = $key; } return $this->id; }
/** * Save the setting to the DB */ public function save($userID = false) { if (!$this->libraryID) { throw new Exception("libraryID not set"); } if (!isset($this->name) || $this->name === '') { throw new Exception("Setting name not provided"); } try { Zotero_Settings::editCheck($this, $userID); } catch (Exception $e) { error_log("WARNING: " . $e); return false; } if (!$this->changed) { Z_Core::debug("Setting {$this->libraryID}/{$this->name} has not changed"); return false; } $shardID = Zotero_Shards::getByLibraryID($this->libraryID); Zotero_DB::beginTransaction(); $isNew = !$this->exists(); try { Z_Core::debug("Saving setting {$this->libraryID}/{$this->name}"); $params = array(json_encode($this->value), Zotero_Libraries::getUpdatedVersion($this->libraryID), Zotero_DB::getTransactionTimestamp()); $params = array_merge(array($this->libraryID, $this->name), $params, $params); $shardID = Zotero_Shards::getByLibraryID($this->libraryID); $sql = "INSERT INTO settings (libraryID, name, value, version, lastUpdated) " . "VALUES (?, ?, ?, ?, ?) " . "ON DUPLICATE KEY UPDATE value=?, version=?, lastUpdated=?"; Zotero_DB::query($sql, $params, $shardID); // Remove from delete log if it's there $sql = "DELETE FROM syncDeleteLogKeys WHERE libraryID=? AND objectType='setting' AND `key`=?"; Zotero_DB::query($sql, array($this->libraryID, $this->name), $shardID); Zotero_DB::commit(); } catch (Exception $e) { Zotero_DB::rollback(); throw $e; } return true; }
public function collections() { // 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(); } Zotero_Libraries::updateVersionAndTimestamp($this->objectLibraryID); } $collectionIDs = array(); $collectionKeys = array(); $results = array(); // Single collection if ($this->singleObject) { $this->allowMethods(['HEAD', 'GET', 'PUT', 'PATCH', 'DELETE']); if (!Zotero_ID::isValidKey($this->objectKey)) { $this->e404(); } $collection = Zotero_Collections::getByLibraryAndKey($this->objectLibraryID, $this->objectKey); if ($this->isWriteMethod()) { $collection = $this->handleObjectWrite('collection', $collection ? $collection : null); $this->queryParams['content'] = ['json']; } if (!$collection) { $this->e404("Collection not found"); } $this->libraryVersion = $collection->version; if ($this->method == 'HEAD') { $this->end(); } switch ($this->queryParams['format']) { case 'atom': $this->responseXML = Zotero_Collections::convertCollectionToAtom($collection, $this->queryParams); break; case 'json': $json = $collection->toResponseJSON($this->queryParams, $this->permissions); echo Zotero_Utilities::formatJSON($json); break; default: throw new Exception("Unexpected format '" . $this->queryParams['format'] . "'"); } } else { $this->allowMethods(['HEAD', 'GET', 'POST', 'DELETE']); $this->libraryVersion = Zotero_Libraries::getUpdatedVersion($this->objectLibraryID); if ($this->scopeObject) { $this->allowMethods(array('GET')); switch ($this->scopeObject) { case 'collections': $collection = Zotero_Collections::getByLibraryAndKey($this->objectLibraryID, $this->scopeObjectKey); if (!$collection) { $this->e404("Collection not found"); } $title = "Child Collections of ‘{$collection->name}'’"; $collectionIDs = $collection->getChildCollections(); break; default: throw new Exception("Invalid collections scope object '{$this->scopeObject}'"); } } else { // Top-level items if ($this->subset == 'top') { $this->allowMethods(array('GET')); $title = "Top-Level Collections"; $results = Zotero_Collections::search($this->objectLibraryID, true, $this->queryParams); } else { // Create a collection if ($this->method == 'POST') { $this->queryParams['format'] = 'writereport'; $obj = $this->jsonDecode($this->body); $results = Zotero_Collections::updateMultipleFromJSON($obj, $this->queryParams, $this->objectLibraryID, $this->userID, $this->permissions, $libraryTimestampChecked ? 0 : 1, null); if ($cacheKey = $this->getWriteTokenCacheKey()) { Z_Core::$MC->set($cacheKey, true, $this->writeTokenCacheTime); } if ($this->apiVersion < 2) { $uri = Zotero_API::getCollectionsURI($this->objectLibraryID); $keys = array_merge(get_object_vars($results['success']), get_object_vars($results['unchanged'])); $queryString = "collectionKey=" . urlencode(implode(",", $keys)) . "&format=atom&content=json&order=collectionKeyList&sort=asc"; if ($this->apiKey) { $queryString .= "&key=" . $this->apiKey; } $uri .= "?" . $queryString; $this->queryParams = Zotero_API::parseQueryParams($queryString, $this->action, true, $this->apiVersion); $title = "Collections"; $results = Zotero_Collections::search($this->objectLibraryID, false, $this->queryParams); } } else { if ($this->method == 'DELETE') { Zotero_DB::beginTransaction(); foreach ($this->queryParams['collectionKey'] as $collectionKey) { Zotero_Collections::delete($this->objectLibraryID, $collectionKey); } Zotero_DB::commit(); $this->e204(); } else { $title = "Collections"; $results = Zotero_Collections::search($this->objectLibraryID, false, $this->queryParams); } } } } if ($collectionIDs) { $this->queryParams['collectionIDs'] = $collectionIDs; $results = Zotero_Collections::search($this->objectLibraryID, false, $this->queryParams); } $options = ['action' => $this->action, 'uri' => $this->uri, 'results' => $results, 'requestParams' => $this->queryParams, 'permissions' => $this->permissions, 'head' => $this->method == 'HEAD']; switch ($this->queryParams['format']) { case 'atom': $this->responseXML = Zotero_API::multiResponse(array_merge($options, ['title' => $this->getFeedNamePrefix($this->objectLibraryID) . $title])); break; case 'json': case 'keys': case 'versions': case 'writereport': Zotero_API::multiResponse($options); break; default: throw new Exception("Unexpected format '" . $this->queryParams['format'] . "'"); } } $this->end(); }
public function searches() { if ($this->apiVersion < 2) { $this->e404(); } // 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(); } Zotero_Libraries::updateVersionAndTimestamp($this->objectLibraryID); } $results = array(); // Single search if ($this->singleObject) { $this->allowMethods(['HEAD', 'GET', 'PUT', 'PATCH', 'DELETE']); $search = Zotero_Searches::getByLibraryAndKey($this->objectLibraryID, $this->objectKey); if ($this->isWriteMethod()) { $search = $this->handleObjectWrite('search', $search ? $search : null); $this->e204(); } if (!$search) { $this->e404("Search not found"); } $this->libraryVersion = $search->version; if ($this->method == 'HEAD') { $this->end(); } // Display search switch ($this->queryParams['format']) { case 'atom': $this->responseXML = $search->toAtom($this->queryParams); break; case 'json': $json = $search->toResponseJSON($this->queryParams, $this->permissions); echo Zotero_Utilities::formatJSON($json); break; default: throw new Exception("Unexpected format '" . $this->queryParams['format'] . "'"); } } else { $this->allowMethods(['HEAD', 'GET', 'POST', 'DELETE']); $this->libraryVersion = Zotero_Libraries::getUpdatedVersion($this->objectLibraryID); // Create a search if ($this->method == 'POST') { $this->queryParams['format'] = 'writereport'; $obj = $this->jsonDecode($this->body); $results = Zotero_Searches::updateMultipleFromJSON($obj, $this->objectLibraryID, $this->queryParams, $this->userID, $libraryTimestampChecked ? 0 : 1, null); if ($cacheKey = $this->getWriteTokenCacheKey()) { Z_Core::$MC->set($cacheKey, true, $this->writeTokenCacheTime); } } else { if ($this->method == 'DELETE') { Zotero_DB::beginTransaction(); foreach ($this->queryParams['searchKey'] as $searchKey) { Zotero_Searches::delete($this->objectLibraryID, $searchKey); } Zotero_DB::commit(); $this->e204(); } else { $title = "Searches"; $results = Zotero_Searches::search($this->objectLibraryID, $this->queryParams); } } $options = ['action' => $this->action, 'uri' => $this->uri, 'results' => $results, 'requestParams' => $this->queryParams, 'permissions' => $this->permissions, 'head' => $this->method == 'HEAD']; switch ($this->queryParams['format']) { case 'atom': $this->responseXML = Zotero_API::multiResponse(array_merge($options, ['title' => $this->getFeedNamePrefix($this->objectLibraryID) . $title])); break; case 'json': case 'keys': case 'versions': case 'writereport': Zotero_API::multiResponse($options); break; default: throw new Exception("Unexpected format '" . $this->queryParams['format'] . "'"); } } $this->end(); }
public function settings() { if ($this->apiVersion < 2) { $this->e404(); } // Check for general library access if (!$this->permissions->canAccess($this->objectLibraryID)) { $this->e403(); } if ($this->isWriteMethod()) { Zotero_DB::beginTransaction(); // 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(); } Zotero_Libraries::updateVersionAndTimestamp($this->objectLibraryID); } // Single setting if ($this->singleObject) { $this->allowMethods(array('GET', 'PUT', 'DELETE')); $setting = Zotero_Settings::getByLibraryAndKey($this->objectLibraryID, $this->objectKey); if (!$setting) { if ($this->method == 'PUT') { $setting = new Zotero_Setting(); $setting->libraryID = $this->objectLibraryID; $setting->name = $this->objectKey; } else { $this->e404("Setting not found"); } } if ($this->isWriteMethod()) { if ($this->method == 'PUT') { $json = $this->jsonDecode($this->body); $objectVersionValidated = $this->checkSingleObjectWriteVersion('setting', $setting, $json); } $this->libraryVersion = Zotero_Libraries::getUpdatedVersion($this->objectLibraryID); // Update setting if ($this->method == 'PUT') { $changed = Zotero_Settings::updateFromJSON($setting, $json, $this->queryParams, $this->userID, $objectVersionValidated ? 0 : 2); // If not updated, return the original library version if (!$changed) { $this->libraryVersion = Zotero_Libraries::getOriginalVersion($this->objectLibraryID); Zotero_DB::rollback(); $this->e204(); } } else { if ($this->method == 'DELETE') { Zotero_Settings::delete($this->objectLibraryID, $this->objectKey); } else { throw new Exception("Unexpected method {$this->method}"); } } Zotero_DB::commit(); $this->e204(); } else { $this->libraryVersion = $setting->version; $json = $setting->toJSON(true, $this->queryParams); echo Zotero_Utilities::formatJSON($json); } } else { $this->allowMethods(array('GET', 'POST')); $this->libraryVersion = Zotero_Libraries::getUpdatedVersion($this->objectLibraryID); // Create a setting if ($this->method == 'POST') { $obj = $this->jsonDecode($this->body); $changed = Zotero_Settings::updateMultipleFromJSON($obj, $this->queryParams, $this->objectLibraryID, $this->userID, $this->permissions, $libraryTimestampChecked ? 0 : 1, null); // If not updated, return the original library version if (!$changed) { $this->libraryVersion = Zotero_Libraries::getOriginalVersion($this->objectLibraryID); } Zotero_DB::commit(); $this->e204(); } else { $settings = Zotero_Settings::search($this->objectLibraryID, $this->queryParams); $json = new stdClass(); foreach ($settings as $setting) { $json->{$setting->name} = $setting->toJSON(true, $this->queryParams); } echo Zotero_Utilities::formatJSON($json); } } $this->end(); }
public function save($userID = false, $full = false) { if (!$this->libraryID) { trigger_error("Library ID must be set before saving", E_USER_ERROR); } Zotero_Tags::editCheck($this, $userID); if (!$this->hasChanged()) { Z_Core::debug("Tag {$this->id} has not changed"); return false; } $shardID = Zotero_Shards::getByLibraryID($this->libraryID); Zotero_DB::beginTransaction(); try { $tagID = $this->id ? $this->id : Zotero_ID::get('tags'); $isNew = !$this->id; Z_Core::debug("Saving tag {$tagID}"); $key = $this->key ? $this->key : Zotero_ID::getKey(); $timestamp = Zotero_DB::getTransactionTimestamp(); $dateAdded = $this->dateAdded ? $this->dateAdded : $timestamp; $dateModified = $this->dateModified ? $this->dateModified : $timestamp; $version = $this->changed['name'] || $this->changed['type'] ? Zotero_Libraries::getUpdatedVersion($this->libraryID) : $this->version; $fields = "name=?, `type`=?, dateAdded=?, dateModified=?,\n\t\t\t\tlibraryID=?, `key`=?, serverDateModified=?, version=?"; $params = array($this->name, $this->type ? $this->type : 0, $dateAdded, $dateModified, $this->libraryID, $key, $timestamp, $version); try { if ($isNew) { $sql = "INSERT INTO tags SET tagID=?, {$fields}"; $stmt = Zotero_DB::getStatement($sql, true, $shardID); Zotero_DB::queryFromStatement($stmt, array_merge(array($tagID), $params)); // Remove from delete log if it's there $sql = "DELETE FROM syncDeleteLogKeys WHERE libraryID=?\n\t\t\t\t\t AND objectType='tag' AND `key`=?"; Zotero_DB::query($sql, array($this->libraryID, $key), $shardID); $sql = "DELETE FROM syncDeleteLogKeys WHERE libraryID=?\n\t\t\t\t\t AND objectType='tagName' AND `key`=?"; Zotero_DB::query($sql, array($this->libraryID, $this->name), $shardID); } else { $sql = "UPDATE tags SET {$fields} WHERE tagID=?"; $stmt = Zotero_DB::getStatement($sql, true, Zotero_Shards::getByLibraryID($this->libraryID)); Zotero_DB::queryFromStatement($stmt, array_merge($params, array($tagID))); } } catch (Exception $e) { // If an incoming tag is the same as an existing tag, but with a different key, // then delete the old tag and add its linked items to the new tag if (preg_match("/Duplicate entry .+ for key 'uniqueTags'/", $e->getMessage())) { // GET existing tag $existing = Zotero_Tags::getIDs($this->libraryID, $this->name); if (!$existing) { throw new Exception("Existing tag not found"); } foreach ($existing as $id) { $tag = Zotero_Tags::get($this->libraryID, $id, true); if ($tag->__get('type') == $this->type) { $linked = $tag->getLinkedItems(true); Zotero_Tags::delete($this->libraryID, $tag->key); break; } } // Save again if ($isNew) { $sql = "INSERT INTO tags SET tagID=?, {$fields}"; $stmt = Zotero_DB::getStatement($sql, true, $shardID); Zotero_DB::queryFromStatement($stmt, array_merge(array($tagID), $params)); // Remove from delete log if it's there $sql = "DELETE FROM syncDeleteLogKeys WHERE libraryID=?\n\t\t\t\t\t\t AND objectType='tag' AND `key`=?"; Zotero_DB::query($sql, array($this->libraryID, $key), $shardID); $sql = "DELETE FROM syncDeleteLogKeys WHERE libraryID=?\n\t\t\t\t\t\t AND objectType='tagName' AND `key`=?"; Zotero_DB::query($sql, array($this->libraryID, $this->name), $shardID); } else { $sql = "UPDATE tags SET {$fields} WHERE tagID=?"; $stmt = Zotero_DB::getStatement($sql, true, Zotero_Shards::getByLibraryID($this->libraryID)); Zotero_DB::queryFromStatement($stmt, array_merge($params, array($tagID))); } $new = array_unique(array_merge($linked, $this->getLinkedItems(true))); $this->setLinkedItems($new); } else { throw $e; } } // Linked items if ($full || $this->changed['linkedItems']) { $removeKeys = array(); $currentKeys = $this->getLinkedItems(true); if ($full) { $sql = "SELECT `key` FROM itemTags JOIN items " . "USING (itemID) WHERE tagID=?"; $stmt = Zotero_DB::getStatement($sql, true, $shardID); $dbKeys = Zotero_DB::columnQueryFromStatement($stmt, $tagID); if ($dbKeys) { $removeKeys = array_diff($dbKeys, $currentKeys); $newKeys = array_diff($currentKeys, $dbKeys); } else { $newKeys = $currentKeys; } } else { if (!empty($this->previousData['linkedItems'])) { $removeKeys = array_diff($this->previousData['linkedItems'], $currentKeys); $newKeys = array_diff($currentKeys, $this->previousData['linkedItems']); } else { $newKeys = $currentKeys; } } if ($removeKeys) { $sql = "DELETE itemTags FROM itemTags JOIN items USING (itemID) " . "WHERE tagID=? AND items.key IN (" . implode(', ', array_fill(0, sizeOf($removeKeys), '?')) . ")"; Zotero_DB::query($sql, array_merge(array($this->id), $removeKeys), $shardID); } if ($newKeys) { $sql = "INSERT INTO itemTags (tagID, itemID) " . "SELECT ?, itemID FROM items " . "WHERE libraryID=? AND `key` IN (" . implode(', ', array_fill(0, sizeOf($newKeys), '?')) . ")"; Zotero_DB::query($sql, array_merge(array($tagID, $this->libraryID), $newKeys), $shardID); } //Zotero.Notifier.trigger('add', 'collection-item', $this->id . '-' . $itemID); } Zotero_DB::commit(); Zotero_Tags::cachePrimaryData(array('id' => $tagID, 'libraryID' => $this->libraryID, 'key' => $key, 'name' => $this->name, 'type' => $this->type ? $this->type : 0, 'dateAdded' => $dateAdded, 'dateModified' => $dateModified, 'version' => $version)); } catch (Exception $e) { Zotero_DB::rollback(); throw $e; } // If successful, set values in object if (!$this->id) { $this->id = $tagID; } if (!$this->key) { $this->key = $key; } $this->init(); if ($isNew) { Zotero_Tags::cache($this); } return $this->id; }
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 updateVersions($items, $userID = false) { $libraryShards = array(); $libraryIsGroup = array(); $shardItemIDs = array(); $shardGroupItemIDs = array(); $libraryItems = array(); foreach ($items as $item) { $libraryID = $item->libraryID; $itemID = $item->id; // Index items by shard if (isset($libraryShards[$libraryID])) { $shardID = $libraryShards[$libraryID]; $shardItemIDs[$shardID][] = $itemID; } else { $shardID = Zotero_Shards::getByLibraryID($libraryID); $libraryShards[$libraryID] = $shardID; $shardItemIDs[$shardID] = array($itemID); } // Separate out group items by shard if (!isset($libraryIsGroup[$libraryID])) { $libraryIsGroup[$libraryID] = Zotero_Libraries::getType($libraryID) == 'group'; } if ($libraryIsGroup[$libraryID]) { if (isset($shardGroupItemIDs[$shardID])) { $shardGroupItemIDs[$shardID][] = $itemID; } else { $shardGroupItemIDs[$shardID] = array($itemID); } } // Index items by library if (!isset($libraryItems[$libraryID])) { $libraryItems[$libraryID] = array(); } $libraryItems[$libraryID][] = $item; } Zotero_DB::beginTransaction(); foreach ($shardItemIDs as $shardID => $itemIDs) { // Group item data if ($userID && isset($shardGroupItemIDs[$shardID])) { $sql = "UPDATE groupItems SET lastModifiedByUserID=? " . "WHERE itemID IN (" . implode(',', array_fill(0, sizeOf($shardGroupItemIDs[$shardID]), '?')) . ")"; Zotero_DB::query($sql, array_merge(array($userID), $shardGroupItemIDs[$shardID]), $shardID); } } foreach ($libraryItems as $libraryID => $items) { $itemIDs = array(); foreach ($items as $item) { $itemIDs[] = $item->id; } $version = Zotero_Libraries::getUpdatedVersion($libraryID); $sql = "UPDATE items SET version=? WHERE itemID IN " . "(" . implode(',', array_fill(0, sizeOf($itemIDs), '?')) . ")"; Zotero_DB::query($sql, array_merge(array($version), $itemIDs), $shardID); } Zotero_DB::commit(); foreach ($libraryItems as $libraryID => $items) { foreach ($items as $item) { $item->reload(); } $libraryKeys = array_map(function ($item) use($libraryID) { return $libraryID . "/" . $item->key; }, $items); Zotero_Notifier::trigger('modify', 'item', $libraryKeys); } }
public function setItems($itemIDs) { $shardID = Zotero_Shards::getByLibraryID($this->libraryID); Zotero_DB::beginTransaction(); $this->loadChildItems(); $current = $this->childItems; $removed = array_diff($current, $itemIDs); $new = array_diff($itemIDs, $current); if ($removed) { $arr = $removed; $sql = "DELETE FROM collectionItems WHERE collectionID=? AND itemID IN ("; while ($chunk = array_splice($arr, 0, 500)) { array_unshift($chunk, $this->id); Zotero_DB::query($sql . implode(', ', array_fill(0, sizeOf($chunk) - 1, '?')) . ")", $chunk, $shardID); } } if ($new) { $arr = $new; $sql = "INSERT INTO collectionItems (collectionID, itemID) VALUES "; while ($chunk = array_splice($arr, 0, 250)) { Zotero_DB::query($sql . implode(',', array_fill(0, sizeOf($chunk), '(?,?)')), call_user_func_array('array_merge', array_map(function ($itemID) { return [$this->id, $itemID]; }, $chunk)), $shardID); } } $this->childItems = array_values(array_unique($itemIDs)); // // TODO: remove UPDATE statements below once classic syncing is removed // // Update timestamp of collection $sql = "UPDATE collections SET serverDateModified=? WHERE collectionID=?"; $ts = Zotero_DB::getTransactionTimestamp(); Zotero_DB::query($sql, array($ts, $this->id), $shardID); // Update version of new and removed items if ($new || $removed) { $sql = "UPDATE items SET version=? WHERE itemID IN (" . implode(', ', array_fill(0, sizeOf($new) + sizeOf($removed), '?')) . ")"; Zotero_DB::query($sql, array_merge(array(Zotero_Libraries::getUpdatedVersion($this->libraryID)), $new, $removed), $shardID); } Zotero_DB::commit(); }
/** * @param object $object Zotero object (Zotero_Item, Zotero_Collection, Zotero_Search, Zotero_Setting) * @param object $json JSON object to check * @param array $requestParams * @param int $requireVersion If 0, don't require; if 1, require if there's * an object key property in the JSON; if 2, * always require */ public static function checkJSONObjectVersion($object, $json, $requestParams, $requireVersion) { $objectType = Zotero_Utilities::getObjectTypeFromObject($object); if (!in_array($objectType, array('item', 'collection', 'search', 'setting'))) { throw new Exception("Invalid object type"); } $oldKeyProp = $objectType . "Key"; $oldVersionProp = $objectType == 'setting' ? 'version' : $objectType . "Version"; $newKeyProp = 'key'; $newVersionProp = 'version'; if ($requestParams['v'] >= 3) { $keyProp = $newKeyProp; $versionProp = $newVersionProp; // Disallow old properties if (isset($json->{$oldKeyProp})) { throw new Exception("'{$oldKeyProp}' property is now '" . $newKeyProp . "'", Z_ERROR_INVALID_INPUT); } else { if (isset($json->{$oldVersionProp}) && $oldVersionProp != $newVersionProp) { throw new Exception("'{$oldVersionProp}' property is now '" . $newVersionProp . "'", Z_ERROR_INVALID_INPUT); } } } else { $keyProp = $oldKeyProp; $versionProp = $oldVersionProp; // Disallow new properties if (isset($json->{$newKeyProp})) { throw new Exception("Invalid property '{$newKeyProp}'", Z_ERROR_INVALID_INPUT); } else { if (isset($json->{$newVersionProp}) && $oldVersionProp != $newVersionProp) { throw new Exception("Invalid property '{$newVersionProp}'", Z_ERROR_INVALID_INPUT); } } } if (isset($json->{$versionProp})) { if ($requestParams['v'] < 2) { throw new Exception("Invalid property '{$versionProp}'", Z_ERROR_INVALID_INPUT); } if (!is_numeric($json->{$versionProp})) { throw new Exception("'{$versionProp}' must be an integer", Z_ERROR_INVALID_INPUT); } if (!isset($json->{$keyProp}) && $objectType != 'setting' && !$object->key) { throw new Exception("'{$versionProp}' is valid only if {$objectType} key is provided", Z_ERROR_INVALID_INPUT); } $originalVersion = Zotero_Libraries::getOriginalVersion($object->libraryID); $updatedVersion = Zotero_Libraries::getUpdatedVersion($object->libraryID); // Make sure the object hasn't been modified since the specified version if ($object->version > $json->{$versionProp}) { // Unless it was modified in this request if ($updatedVersion != $originalVersion && $object->version == $updatedVersion) { return; } throw new HTTPException(ucwords($objectType) . " has been modified since specified version " . "(expected {$json->{$versionProp}}, found {$object->version})", 412); } else { if ($json->{$versionProp} > 0 && !$object->version) { throw new HTTPException(ucwords($objectType) . " doesn't exist (expected version {$json->{$versionProp}}; use 0 instead)", 404); } } } else { if ($requireVersion == 1 && isset($json->{$keyProp})) { if ($objectType == 'setting') { throw new HTTPException("Either If-Unmodified-Since-Version or " . "'{$versionProp}' property must be provided", 428); } else { throw new HTTPException("Either If-Unmodified-Since-Version or " . "'{$versionProp}' property must be provided for " . "'{$keyProp}'-based writes", 428); } } else { if ($requireVersion == 2) { throw new HTTPException("Either If-Unmodified-Since-Version or " . "'{$versionProp}' property must be provided for " . "single-{$objectType} writes", 428); } } } }
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(); }