Ejemplo n.º 1
0
 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();
 }
Ejemplo n.º 2
0
 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();
 }
Ejemplo n.º 3
0
 public function save($userID = false)
 {
     if (!$this->libraryID) {
         trigger_error("Library ID must be set before saving", E_USER_ERROR);
     }
     Zotero_Items::editCheck($this);
     if (!$this->hasChanged()) {
         Z_Core::debug("Item {$this->id} has not changed");
         return 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);
     Zotero_DB::beginTransaction();
     try {
         //
         // New item, insert and return id
         //
         if (!$this->id || !$this->exists()) {
             Z_Core::debug('Saving data for new item to database');
             $isNew = true;
             $sqlColumns = array();
             $sqlValues = array();
             //
             // Primary fields
             //
             $itemID = $this->id ? $this->id : Zotero_ID::get('items');
             $key = $this->key ? $this->key : $this->generateKey();
             $sqlColumns = array('itemID', 'itemTypeID', 'libraryID', 'key', 'dateAdded', 'dateModified', 'serverDateModified');
             $timestamp = Zotero_DB::getTransactionTimestamp();
             $sqlValues = array($itemID, $this->itemTypeID, $this->libraryID, $key, $this->dateAdded ? $this->dateAdded : $timestamp, $this->dateModified ? $this->dateModified : $timestamp, $timestamp);
             //
             // Primary fields
             //
             $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
             $insertID = Zotero_DB::query($sql, $sqlValues, $shardID);
             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, null), $shardID);
             }
             //
             // ItemData
             //
             if ($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);
                         throw new Exception("={$fieldName} field " . "'" . substr($value, 0, 50) . "...' 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 ($this->changed['creators']) {
                 $indexes = array_keys($this->changed['creators']);
                 // TODO: group queries
                 $sql = "INSERT INTO itemCreators\n\t\t\t\t\t\t\t\t(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}");
                         $creator['ref']->save();
                     }
                     $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 ($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() || $this->changed['note']) {
                 $noteIsSanitized = false;
                 // If we don't have a sanitized note, generate one
                 if (is_null($this->noteTextSanitized)) {
                     $noteTextSanitized = Zotero_Notes::sanitize($this->noteText);
                     // But if the same as original, just use reference
                     if ($this->noteText == $noteTextSanitized) {
                         $this->noteTextSanitized =& $this->noteText;
                         $noteIsSanitized = true;
                     } else {
                         $this->noteTextSanitized = $noteTextSanitized;
                     }
                 }
                 // If note is sanitized already, store empty string
                 // If not, store sanitized version
                 $noteTextSanitized = $noteIsSanitized ? '' : $this->noteTextSanitized;
                 $title = Zotero_Notes::noteToTitle($this->noteTextSanitized);
                 $sql = "INSERT INTO itemNotes\n\t\t\t\t\t\t\t(itemID, sourceItemID, note, noteSanitized, title, hash)\n\t\t\t\t\t\t\tVALUES (?,?,?,?,?,?)";
                 $parent = $this->isNote() ? $this->getSource() : null;
                 $hash = $this->noteText ? md5($this->noteText) : '';
                 $bindParams = array($itemID, $parent ? $parent : null, $this->noteText ? $this->noteText : '', $noteTextSanitized, $title, $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\n\t\t\t\t\t\t\t(itemID, sourceItemID, linkMode, mimeType, charsetID, path, storageModTime, storageHash)\n\t\t\t\t\t\t\tVALUES (?,?,?,?,?,?,?,?)";
                 $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()) {
                         trigger_error("Parent item cannot be a child attachment", E_USER_ERROR);
                     }
                 }
                 $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(), 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;
                 }
             }
             // Related items
             if (!empty($this->changed['relatedItems'])) {
                 $removed = array();
                 $newids = array();
                 $currentIDs = $this->relatedItems;
                 if (!$currentIDs) {
                     $currentIDs = array();
                 }
                 foreach ($this->previousData['relatedItems'] as $id) {
                     if (!in_array($id, $currentIDs)) {
                         $removed[] = $id;
                     }
                 }
                 foreach ($currentIDs as $id) {
                     if (in_array($id, $this->previousData['relatedItems'])) {
                         continue;
                     }
                     $newids[] = $id;
                 }
                 if ($removed) {
                     $sql = "DELETE FROM itemRelated WHERE itemID=?\n\t\t\t\t\t\t\t\tAND linkedItemID IN (";
                     $sql .= implode(', ', array_fill(0, sizeOf($removed), '?')) . ")";
                     Zotero_DB::query($sql, array_merge(array($this->id), $removed), $shardID);
                 }
                 if ($newids) {
                     $sql = "INSERT INTO itemRelated (itemID, linkedItemID)\n\t\t\t\t\t\t\t\tVALUES (?,?)";
                     $insertStatement = Zotero_DB::getStatement($sql, false, $shardID);
                     foreach ($newids as $linkedItemID) {
                         $insertStatement->execute(array($itemID, $linkedItemID));
                     }
                 }
             }
             // 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);
         } else {
             Z_Core::debug('Updating database with new item data', 4);
             $isNew = false;
             //
             // Primary fields
             //
             $sql = "UPDATE items SET ";
             $sqlValues = array();
             $timestamp = Zotero_DB::getTransactionTimestamp();
             $updateFields = array('itemTypeID', 'libraryID', 'key', 'dateAdded', 'dateModified');
             foreach ($updateFields as $updateField) {
                 if (in_array($updateField, $this->changed['primaryData'])) {
                     $sql .= "`{$updateField}`=?, ";
                     $sqlValues[] = $this->{$updateField};
                 } else {
                     if ($updateField == 'dateModified') {
                         $sql .= "`{$updateField}`=?, ";
                         $sqlValues[] = $timestamp;
                     }
                 }
             }
             $sql .= "serverDateModified=?, version=IF(version = 65535, 0, version + 1) WHERE itemID=?";
             array_push($sqlValues, $timestamp, $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 (?, ?, ?)\n\t\t\t\t\t\t\t\tON DUPLICATE KEY UPDATE lastModifiedByUserID=?";
                 Zotero_DB::query($sql, array($this->id, null, $userID, $userID), $shardID);
             }
             //
             // ItemData
             //
             if ($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);
                         throw new Exception("={$fieldName} field " . "'" . substr($value, 0, 50) . "...' 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 ($this->changed['creators']) {
                 $indexes = array_keys($this->changed['creators']);
                 $sql = "INSERT INTO itemCreators\n\t\t\t\t\t\t\t\t(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 ($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 ($this->changed['source'] && $this->getSource()) {
                 $sql = "DELETE FROM collectionItems WHERE itemID=?";
                 Zotero_DB::query($sql, $this->id, $shardID);
             }
             //
             // Note or attachment note
             //
             if ($this->changed['note']) {
                 $noteIsSanitized = false;
                 // If we don't have a sanitized note, generate one
                 if (is_null($this->noteTextSanitized)) {
                     $noteTextSanitized = Zotero_Notes::sanitize($this->noteText);
                     // But if the same as original, just use reference
                     if ($this->noteText == $noteTextSanitized) {
                         $this->noteTextSanitized =& $this->noteText;
                         $noteIsSanitized = true;
                     } else {
                         $this->noteTextSanitized = $noteTextSanitized;
                     }
                 }
                 // If note is sanitized already, store empty string
                 // If not, store sanitized version
                 $noteTextSanitized = $noteIsSanitized ? '' : $this->noteTextSanitized;
                 $title = Zotero_Notes::noteToTitle($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\n\t\t\t\t\t\t\t(itemID, sourceItemID, note, noteSanitized, title, hash)\n\t\t\t\t\t\t\tVALUES (?,?,?,?,?,?)\n\t\t\t\t\t\t\tON DUPLICATE KEY UPDATE sourceItemID=?, note=?, noteSanitized=?, title=?, hash=?";
                 $bindParams = array($this->id, $sourceItemID, $this->noteText ? $this->noteText : '', $noteTextSanitized, $title, $hash, $sourceItemID, $this->noteText ? $this->noteText : '', $noteTextSanitized, $title, $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 ($this->changed['attachmentData']) {
                 $sql = "REPLACE INTO itemAttachments\n\t\t\t\t\t\t(itemID, sourceItemID, linkMode, mimeType, charsetID, path, storageModTime, storageHash)\n\t\t\t\t\t\tVALUES (?,?,?,?,?,?,?,?)";
                 $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()) {
                         trigger_error("Parent item cannot be a child attachment", E_USER_ERROR);
                     }
                 }
                 $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']) || $this->changed['itemData'] || $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 ($this->changed['creators']) {
                     $creatorSummary = mb_strcut($this->getCreatorSummary(), 0, Zotero_Creators::$creatorSummarySortLength);
                     $sql .= ", creatorSummary=?";
                     $params[] = $creatorSummary;
                 }
                 $sql .= " WHERE itemID=?";
                 $params[] = $this->id;
                 Zotero_DB::query($sql, $params, $shardID);
             }
             //
             // Source item id
             //
             if ($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 (!$this->changed['attachmentData'] && (!$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 && $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=?\n\t\t\t\t\t\t\t\tWHERE 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);
                     }
                 }
             }
             // Related items
             if (!empty($this->changed['relatedItems'])) {
                 $removed = array();
                 $newids = array();
                 $currentIDs = $this->relatedItems;
                 if (!$currentIDs) {
                     $currentIDs = array();
                 }
                 foreach ($this->previousData['relatedItems'] as $id) {
                     if (!in_array($id, $currentIDs)) {
                         $removed[] = $id;
                     }
                 }
                 foreach ($currentIDs as $id) {
                     if (in_array($id, $this->previousData['relatedItems'])) {
                         continue;
                     }
                     $newids[] = $id;
                 }
                 if ($removed) {
                     $sql = "DELETE FROM itemRelated WHERE itemID=?\n\t\t\t\t\t\t\t\tAND linkedItemID IN (";
                     $q = array_fill(0, sizeOf($removed), '?');
                     $sql .= implode(', ', $q) . ")";
                     Zotero_DB::query($sql, array_merge(array($this->id), $removed), $shardID);
                 }
                 if ($newids) {
                     $sql = "INSERT INTO itemRelated (itemID, linkedItemID)\n\t\t\t\t\t\t\t\tVALUES (?,?)";
                     $insertStatement = Zotero_DB::getStatement($sql, false, $shardID);
                     foreach ($newids as $linkedItemID) {
                         $insertStatement->execute(array($this->id, $linkedItemID));
                     }
                 }
             }
         }
         Zotero_DB::commit();
     } catch (Exception $e) {
         Zotero_DB::rollback();
         throw $e;
     }
     if (!$this->id) {
         $this->id = $itemID;
     }
     if (!$this->key) {
         $this->key = $key;
     }
     if ($isNew) {
         Zotero_Items::cache($this);
         Zotero_Items::cacheLibraryKeyID($this->libraryID, $key, $itemID);
     }
     // TODO: invalidate memcache
     Zotero_Items::reload($this->libraryID, $this->id);
     if ($isNew) {
         //Zotero.Notifier.trigger('add', 'item', $this->getID());
         return $this->id;
     }
     //Zotero.Notifier.trigger('modify', 'item', $this->getID(), { old: $this->_preChangeArray });
     return true;
 }
Ejemplo n.º 4
0
 public function save($full = false)
 {
     if (!$this->libraryID) {
         trigger_error("Library ID must be set before saving", E_USER_ERROR);
     }
     Zotero_Tags::editCheck($this);
     if (!$this->changed) {
         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 : $this->generateKey();
         $timestamp = Zotero_DB::getTransactionTimestamp();
         $dateAdded = $this->dateAdded ? $this->dateAdded : $timestamp;
         $dateModified = $this->dateModified ? $this->dateModified : $timestamp;
         $fields = "name=?, `type`=?, dateAdded=?, dateModified=?,\n\t\t\t\tlibraryID=?, `key`=?, serverDateModified=?";
         $params = array($this->name, $this->type ? $this->type : 0, $dateAdded, $dateModified, $this->libraryID, $key, $timestamp);
         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=? AND objectType='tag' AND `key`=?";
                 Zotero_DB::query($sql, array($this->libraryID, $key), $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=? AND objectType='tag' AND `key`=?";
                     Zotero_DB::query($sql, array($this->libraryID, $key), $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 || !empty($this->changed['linkedItems'])) {
             $removed = array();
             $newids = array();
             $currentIDs = $this->getLinkedItems(true);
             if (!$currentIDs) {
                 $currentIDs = array();
             }
             if ($full) {
                 $sql = "SELECT itemID FROM itemTags WHERE tagID=?";
                 $stmt = Zotero_DB::getStatement($sql, true, $shardID);
                 $dbItemIDs = Zotero_DB::columnQueryFromStatement($stmt, $tagID);
                 if ($dbItemIDs) {
                     $removed = array_diff($dbItemIDs, $currentIDs);
                     $newids = array_diff($currentIDs, $dbItemIDs);
                 } else {
                     $newids = $currentIDs;
                 }
             } else {
                 if ($this->previousData['linkedItems']) {
                     $removed = array_diff($this->previousData['linkedItems'], $currentIDs);
                     $newids = array_diff($currentIDs, $this->previousData['linkedItems']);
                 } else {
                     $newids = $currentIDs;
                 }
             }
             if ($removed) {
                 $sql = "DELETE FROM itemTags WHERE tagID=? AND itemID IN (";
                 $q = array_fill(0, sizeOf($removed), '?');
                 $sql .= implode(', ', $q) . ")";
                 Zotero_DB::query($sql, array_merge(array($this->id), $removed), $shardID);
             }
             if ($newids) {
                 $newids = array_values($newids);
                 $sql = "INSERT INTO itemTags (tagID, itemID) VALUES ";
                 $maxInsertGroups = 50;
                 Zotero_DB::bulkInsert($sql, $newids, $maxInsertGroups, $tagID, $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));
     } 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);
         Zotero_Tags::cacheLibraryKeyID($this->libraryID, $key, $tagID);
     }
     return $this->id;
 }
Ejemplo n.º 5
0
 public static function checkUploadForErrors($syncProcessID)
 {
     Zotero_DB::beginTransaction();
     if (Z_Core::probability(30)) {
         $sql = "UPDATE syncUploadQueue SET started=NULL, errorCheck=0 WHERE started IS NOT NULL AND errorCheck=1 AND\n\t\t\t\t\t\tstarted < (NOW() - INTERVAL 15 MINUTE) AND finished IS NULL";
         Zotero_DB::query($sql);
     }
     // Get a queued process that hasn't been error-checked and is large enough to warrant it
     $sql = "SELECT * FROM syncUploadQueue WHERE started IS NULL AND errorCheck=0\n\t\t\t\tAND dataLength>=" . self::$minErrorCheckSize . " ORDER BY added LIMIT 1 FOR UPDATE";
     $row = Zotero_DB::rowQuery($sql);
     // No pending processes
     if (!$row) {
         Zotero_DB::commit();
         return 0;
     }
     $sql = "UPDATE syncUploadQueue SET started=NOW(), errorCheck=1 WHERE syncUploadQueueID=?";
     Zotero_DB::query($sql, array($row['syncUploadQueueID']));
     // We track error processes as upload processes that just get reset back to
     // started=NULL on completion (but with errorCheck=2)
     self::addUploadProcess($row['userID'], null, $row['syncUploadQueueID'], $syncProcessID);
     Zotero_DB::commit();
     try {
         $doc = new DOMDocument();
         $doc->loadXML($row['xmldata'], LIBXML_COMPACT | LIBXML_PARSEHUGE);
         // Get long tags
         $value = Zotero_Tags::getLongDataValueFromXML($doc);
         if ($value) {
             throw new Exception("Tag '" . $value . "' too long", Z_ERROR_TAG_TOO_LONG);
         }
         // Get long collection names
         $value = Zotero_Collections::getLongDataValueFromXML($doc);
         if ($value) {
             throw new Exception("Collection '" . $value . "' too long", Z_ERROR_COLLECTION_TOO_LONG);
         }
         // Get long creator names
         $node = Zotero_Creators::getLongDataValueFromXML($doc);
         // returns DOMNode rather than value
         if ($node) {
             $name = mb_substr($node->nodeValue, 0, 50);
             throw new Exception("=The name ‘{$name}…’ is too long to sync.\n\n" . "Search for the item with this name and shorten it. " . "Note that the item may be in the trash or in a group library.\n\n" . "If you receive this message repeatedly for items saved from a " . "particular site, you can report this issue in the Zotero Forums.", Z_ERROR_CREATOR_TOO_LONG);
         }
         // Get long item data fields
         $node = Zotero_Items::getLongDataValueFromXML($doc);
         // returns DOMNode rather than value
         if ($node) {
             $libraryID = $node->parentNode->getAttribute('libraryID');
             $key = $node->parentNode->getAttribute('key');
             if ($libraryID) {
                 $key = $libraryID . "/" . $key;
             }
             $fieldName = $node->getAttribute('name');
             $fieldName = Zotero_ItemFields::getLocalizedString(null, $fieldName);
             if ($fieldName) {
                 $start = "{$fieldName} field";
             } else {
                 $start = "Field";
             }
             throw new Exception("={$start} value '" . mb_substr($node->nodeValue, 0, 75) . "...' too long for item '{$key}'", Z_ERROR_FIELD_TOO_LONG);
         }
     } catch (Exception $e) {
         //Z_Core::logError($e);
         Zotero_DB::beginTransaction();
         $sql = "UPDATE syncUploadQueue SET syncProcessID=NULL, finished=?,\n\t\t\t\t\t\terrorCode=?, errorMessage=? WHERE syncUploadQueueID=?";
         Zotero_DB::query($sql, array(Zotero_DB::getTransactionTimestamp(), $e->getCode(), serialize($e), $row['syncUploadQueueID']));
         $sql = "DELETE FROM syncUploadQueueLocks WHERE syncUploadQueueID=?";
         Zotero_DB::query($sql, $row['syncUploadQueueID']);
         self::removeUploadProcess($syncProcessID);
         Zotero_DB::commit();
         return -2;
     }
     Zotero_DB::beginTransaction();
     $sql = "UPDATE syncUploadQueue SET syncProcessID=NULL, started=NULL, errorCheck=2 WHERE syncUploadQueueID=?";
     Zotero_DB::query($sql, $row['syncUploadQueueID']);
     self::removeUploadProcess($syncProcessID);
     Zotero_DB::commit();
     return 1;
 }
Ejemplo n.º 6
0
 public function save($fixGaps = false)
 {
     if (!$this->libraryID) {
         throw new Exception("Library ID must be set before saving");
     }
     Zotero_Searches::editCheck($this);
     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 : $this->generateKey();
         $fields = "searchName=?, libraryID=?, `key`=?, dateAdded=?, dateModified=?,\n\t\t\t\t\t\tserverDateModified=?";
         $timestamp = Zotero_DB::getTransactionTimestamp();
         $params = array($this->name, $this->libraryID, $key, $this->dateAdded ? $this->dateAdded : $timestamp, $this->dateModified ? $this->dateModified : $timestamp, $timestamp);
         $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));
             Zotero_Searches::cacheLibraryKeyID($this->libraryID, $key, $searchID);
             // 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)));
         }
         // Close gaps in savedSearchIDs
         $saveConditions = array();
         $i = 1;
         foreach ($this->conditions as $id => $condition) {
             if (!$fixGaps && $id != $i) {
                 trigger_error('searchConditionIDs not contiguous and |fixGaps| not set in save() of saved search ' . $this->id, E_USER_ERROR);
             }
             $saveConditions[$i] = $condition;
             $i++;
         }
         $this->conditions = $saveConditions;
         // TODO: use proper bound parameters once DB class is updated
         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, $condition['condition'], $condition['mode'] ? $condition['mode'] : '', $condition['operator'] ? $condition['operator'] : '', $condition['value'] ? $condition['value'] : '', $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 successful, set values in object
     if (!$this->id) {
         $this->id = $searchID;
     }
     if (!$this->key) {
         $this->key = $key;
     }
     return $this->id;
 }
Ejemplo n.º 7
0
 public static function delete($libraryID, $key, $updateLibrary = false)
 {
     $table = static::field('table');
     $id = static::field('id');
     $type = static::field('object');
     $types = static::field('objects');
     if (!$key) {
         throw new Exception("Invalid key {$key}");
     }
     // Get object (and trigger caching)
     $obj = static::getByLibraryAndKey($libraryID, $key);
     if (!$obj) {
         return;
     }
     static::editCheck($obj);
     Z_Core::debug("Deleting {$type} {$libraryID}/{$key}", 4);
     $shardID = Zotero_Shards::getByLibraryID($libraryID);
     Zotero_DB::beginTransaction();
     // Needed for API deletes to get propagated via sync
     if ($updateLibrary) {
         $timestamp = Zotero_Libraries::updateTimestamps($obj->libraryID);
         Zotero_DB::registerTransactionTimestamp($timestamp);
     }
     // 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) {
                     static::delete($child->libraryID, $child->key);
                 }
             }
         }
     }
     if ($type == 'relation') {
         // TODO: add key column to relations to speed this up
         $sql = "DELETE FROM {$table} WHERE libraryID=? AND MD5(CONCAT(subject, '_', predicate, '_', object))=?";
         $deleted = Zotero_DB::query($sql, array($libraryID, $key), $shardID);
     } else {
         $sql = "DELETE FROM {$table} WHERE libraryID=? AND `key`=?";
         $deleted = Zotero_DB::query($sql, array($libraryID, $key), $shardID);
     }
     unset(self::$idCache[$type][$libraryID][$key]);
     static::uncachePrimaryData($libraryID, $key);
     if ($deleted) {
         $sql = "INSERT INTO syncDeleteLogKeys (libraryID, objectType, `key`, timestamp)\n\t\t\t\t\t\tVALUES (?, '{$type}', ?, ?) ON DUPLICATE KEY UPDATE timestamp=?";
         $timestamp = Zotero_DB::getTransactionTimestamp();
         $params = array($libraryID, $key, $timestamp, $timestamp);
         Zotero_DB::query($sql, $params, $shardID);
     }
     Zotero_DB::commit();
 }
Ejemplo n.º 8
0
 public function save()
 {
     if (!$this->libraryID) {
         trigger_error("Library ID must be set before saving", E_USER_ERROR);
     }
     Zotero_DB::beginTransaction();
     try {
         $shardID = Zotero_Shards::getByLibraryID($this->libraryID);
         $relationID = $this->id ? $this->id : Zotero_ID::get('relations');
         Z_Core::debug("Saving relation {$relationID}");
         $sql = "INSERT INTO relations\n\t\t\t\t\t(relationID, libraryID, subject, predicate, object, serverDateModified)\n\t\t\t\t\tVALUES (?, ?, ?, ?, ?, ?)";
         $timestamp = Zotero_DB::getTransactionTimestamp();
         $params = array($relationID, $this->libraryID, $this->subject, $this->predicate, $this->object, $timestamp);
         $insertID = Zotero_DB::query($sql, $params, $shardID);
         if (!$this->id) {
             if (!$insertID) {
                 throw new Exception("Relation id not available after INSERT");
             }
             $this->id = $insertID;
         }
         // Remove from delete log if it's there
         $sql = "DELETE FROM syncDeleteLogKeys WHERE libraryID=? AND objectType='relation' AND `key`=?";
         Zotero_DB::query($sql, array($this->libraryID, $this->getKey()), $shardID);
         Zotero_DB::commit();
     } catch (Exception $e) {
         Zotero_DB::rollback();
         throw $e;
     }
     return $this->id;
 }
Ejemplo n.º 9
0
 public function save()
 {
     if (!$this->libraryID) {
         trigger_error("Library ID must be set before saving", E_USER_ERROR);
     }
     Zotero_Creators::editCheck($this);
     // If empty, move on
     if ($this->firstName === '' && $this->lastName === '') {
         throw new Exception('First and last name are empty');
     }
     if ($this->fieldMode == 1 && $this->firstName !== '') {
         throw new Exception('First name must be empty in single-field mode');
     }
     if (!$this->hasChanged()) {
         Z_Core::debug("Creator {$this->id} has not changed");
         return false;
     }
     Zotero_DB::beginTransaction();
     try {
         $creatorID = $this->id ? $this->id : Zotero_ID::get('creators');
         $isNew = !$this->id;
         Z_Core::debug("Saving creator {$this->id}");
         $key = $this->key ? $this->key : $this->generateKey();
         $timestamp = Zotero_DB::getTransactionTimestamp();
         $dateAdded = $this->dateAdded ? $this->dateAdded : $timestamp;
         $dateModified = $this->changed['dateModified'] ? $this->dateModified : $timestamp;
         $fields = "firstName=?, lastName=?, fieldMode=?,\n\t\t\t\t\t\tlibraryID=?, `key`=?, dateAdded=?, dateModified=?, serverDateModified=?";
         $params = array($this->firstName, $this->lastName, $this->fieldMode, $this->libraryID, $key, $dateAdded, $dateModified, $timestamp);
         $shardID = Zotero_Shards::getByLibraryID($this->libraryID);
         try {
             if ($isNew) {
                 $sql = "INSERT INTO creators SET creatorID=?, {$fields}";
                 $stmt = Zotero_DB::getStatement($sql, true, $shardID);
                 Zotero_DB::queryFromStatement($stmt, array_merge(array($creatorID), $params));
                 // Remove from delete log if it's there
                 $sql = "DELETE FROM syncDeleteLogKeys WHERE libraryID=? AND objectType='creator' AND `key`=?";
                 Zotero_DB::query($sql, array($this->libraryID, $key), $shardID);
             } else {
                 $sql = "UPDATE creators SET {$fields} WHERE creatorID=?";
                 $stmt = Zotero_DB::getStatement($sql, true, $shardID);
                 Zotero_DB::queryFromStatement($stmt, array_merge($params, array($creatorID)));
             }
         } catch (Exception $e) {
             if (strpos($e->getMessage(), " too long") !== false) {
                 if (strlen($this->firstName) > 255) {
                     throw new Exception("=First name '" . mb_substr($this->firstName, 0, 50) . "…' too long");
                 }
                 if (strlen($this->lastName) > 255) {
                     if ($this->fieldMode == 1) {
                         throw new Exception("=Last name '" . mb_substr($this->lastName, 0, 50) . "…' too long");
                     } else {
                         throw new Exception("=Name '" . mb_substr($this->lastName, 0, 50) . "…' too long");
                     }
                 }
             }
             throw $e;
         }
         // The client updates the mod time of associated items here, but
         // we don't, because either A) this is from syncing, where appropriate
         // mod times come from the client or B) the change is made through
         // $item->setCreator(), which updates the mod time.
         //
         // If the server started to make other independent creator changes,
         // linked items would need to be updated.
         Zotero_DB::commit();
         Zotero_Creators::cachePrimaryData(array('id' => $creatorID, 'libraryID' => $this->libraryID, 'key' => $key, 'dateAdded' => $dateAdded, 'dateModified' => $dateModified, 'firstName' => $this->firstName, 'lastName' => $this->lastName, 'fieldMode' => $this->fieldMode));
     } catch (Exception $e) {
         Zotero_DB::rollback();
         throw $e;
     }
     // If successful, set values in object
     if (!$this->id) {
         $this->id = $creatorID;
     }
     if (!$this->key) {
         $this->key = $key;
     }
     $this->init();
     if ($isNew) {
         Zotero_Creators::cache($this);
         Zotero_Creators::cacheLibraryKeyID($this->libraryID, $key, $creatorID);
     }
     // TODO: invalidate memcache?
     return $this->id;
 }
Ejemplo n.º 10
0
 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();
 }
Ejemplo n.º 11
0
 public static function checkUploadForErrors($syncProcessID)
 {
     Zotero_DB::beginTransaction();
     if (Z_Core::probability(30)) {
         $sql = "UPDATE syncUploadQueue SET started=NULL, errorCheck=0 WHERE started IS NOT NULL AND errorCheck=1 AND\n\t\t\t\t\t\tstarted < (NOW() - INTERVAL 15 MINUTE) AND finished IS NULL";
         Zotero_DB::query($sql);
     }
     // Get a queued process that hasn't been error-checked and is large enough to warrant it
     $sql = "SELECT * FROM syncUploadQueue WHERE started IS NULL AND errorCheck=0\n\t\t\t\tAND dataLength>=" . self::$minErrorCheckSize . " ORDER BY added LIMIT 1 FOR UPDATE";
     $row = Zotero_DB::rowQuery($sql);
     // No pending processes
     if (!$row) {
         Zotero_DB::commit();
         return 0;
     }
     $sql = "UPDATE syncUploadQueue SET started=NOW(), errorCheck=1 WHERE syncUploadQueueID=?";
     Zotero_DB::query($sql, array($row['syncUploadQueueID']));
     // We track error processes as upload processes that just get reset back to
     // started=NULL on completion (but with errorCheck=2)
     self::addUploadProcess($row['userID'], null, $row['syncUploadQueueID'], $syncProcessID);
     Zotero_DB::commit();
     try {
         $doc = new DOMDocument();
         $doc->loadXML($row['xmldata']);
         // Get long tags
         $value = Zotero_Tags::getLongDataValueFromXML($doc);
         if ($value) {
             throw new Exception("Tag '" . $value . "' too long", Z_ERROR_TAG_TOO_LONG);
         }
         // Get long collection names
         $value = Zotero_Collections::getLongDataValueFromXML($doc);
         if ($value) {
             throw new Exception("Collection '" . $value . "' too long", Z_ERROR_COLLECTION_TOO_LONG);
         }
         // Get long creator names
         $node = Zotero_Creators::getLongDataValueFromXML($doc);
         // returns DOMNode rather than value
         if ($node) {
             if ($node->nodeName == 'firstName') {
                 throw new Exception("=First name '" . mb_substr($node->nodeValue, 0, 50) . "…' too long");
             }
             if ($node->nodeName == 'lastName') {
                 throw new Exception("=Last name '" . mb_substr($node->nodeValue, 0, 50) . "…' too long");
             }
             if ($node->nodeName == 'name') {
                 throw new Exception("=Name '" . mb_substr($node->nodeValue, 0, 50) . "…' too long");
             }
         }
         $node = Zotero_Items::getLongDataValueFromXML($doc);
         // returns DOMNode rather than value
         if ($node) {
             $fieldName = $node->getAttribute('name');
             $fieldName = Zotero_ItemFields::getLocalizedString(null, $fieldName);
             if ($fieldName) {
                 $start = "'{$fieldName}' field";
             } else {
                 $start = "Field";
             }
             throw new Exception("={$start} value '" . mb_substr($node->nodeValue, 0, 50) . "...' too long");
         }
     } catch (Exception $e) {
         //Z_Core::logError($e);
         Zotero_DB::beginTransaction();
         $sql = "UPDATE syncUploadQueue SET syncProcessID=NULL, finished=?,\n\t\t\t\t\t\terrorCode=?, errorMessage=? WHERE syncUploadQueueID=?";
         Zotero_DB::query($sql, array(Zotero_DB::getTransactionTimestamp(), $e->getCode(), serialize($e), $row['syncUploadQueueID']));
         $sql = "DELETE FROM syncUploadQueueLocks WHERE syncUploadQueueID=?";
         Zotero_DB::query($sql, $row['syncUploadQueueID']);
         self::removeUploadProcess($syncProcessID);
         Zotero_DB::commit();
         return -2;
     }
     Zotero_DB::beginTransaction();
     $sql = "UPDATE syncUploadQueue SET syncProcessID=NULL, started=NULL, errorCheck=2 WHERE syncUploadQueueID=?";
     Zotero_DB::query($sql, $row['syncUploadQueueID']);
     self::removeUploadProcess($syncProcessID);
     Zotero_DB::commit();
     return 1;
 }
Ejemplo n.º 12
0
 public static function updateFromJSON(Zotero_Item $item, $json, $isNew = false, Zotero_Item $parentItem = null, $userID = null)
 {
     self::validateJSONItem($json, $item->libraryID, $isNew ? null : $item, !is_null($parentItem));
     Zotero_DB::beginTransaction();
     // Mark library as updated
     if (!$isNew) {
         $timestamp = Zotero_Libraries::updateTimestamps($item->libraryID);
         Zotero_DB::registerTransactionTimestamp($timestamp);
     }
     $forceChange = false;
     $twoStage = false;
     // Set itemType first
     $item->setField("itemTypeID", Zotero_ItemTypes::getID($json->itemType));
     foreach ($json as $key => $val) {
         switch ($key) {
             case 'itemType':
                 continue;
             case 'deleted':
                 continue;
             case 'creators':
                 if (!$val && !$item->numCreators()) {
                     continue 2;
                 }
                 $orderIndex = -1;
                 foreach ($val as $orderIndex => $newCreatorData) {
                     if ((!isset($newCreatorData->name) || trim($newCreatorData->name) == "") && (!isset($newCreatorData->firstName) || trim($newCreatorData->firstName) == "") && (!isset($newCreatorData->lastName) || trim($newCreatorData->lastName) == "")) {
                         // This should never happen, because of check in validateJSONItem()
                         if (!$isNew) {
                             throw new Exception("Nameless creator in update request");
                         }
                         // On item creation, ignore creators with empty names,
                         // because that's in the item template that the API returns
                         break;
                     }
                     // JSON uses 'name' and 'firstName'/'lastName',
                     // so switch to just 'firstName'/'lastName'
                     if (isset($newCreatorData->name)) {
                         $newCreatorData->firstName = '';
                         $newCreatorData->lastName = $newCreatorData->name;
                         unset($newCreatorData->name);
                         $newCreatorData->fieldMode = 1;
                     } else {
                         $newCreatorData->fieldMode = 0;
                     }
                     $newCreatorTypeID = Zotero_CreatorTypes::getID($newCreatorData->creatorType);
                     // Same creator in this position
                     $existingCreator = $item->getCreator($orderIndex);
                     if ($existingCreator && $existingCreator['ref']->equals($newCreatorData)) {
                         // Just change the creatorTypeID
                         if ($existingCreator['creatorTypeID'] != $newCreatorTypeID) {
                             $item->setCreator($orderIndex, $existingCreator['ref'], $newCreatorTypeID);
                         }
                         continue;
                     }
                     // Same creator in a different position, so use that
                     $existingCreators = $item->getCreators();
                     for ($i = 0, $len = sizeOf($existingCreators); $i < $len; $i++) {
                         if ($existingCreators[$i]['ref']->equals($newCreatorData)) {
                             $item->setCreator($orderIndex, $existingCreators[$i]['ref'], $newCreatorTypeID);
                             continue;
                         }
                     }
                     // Make a fake creator to use for the data lookup
                     $newCreator = new Zotero_Creator();
                     $newCreator->libraryID = $item->libraryID;
                     foreach ($newCreatorData as $key => $val) {
                         if ($key == 'creatorType') {
                             continue;
                         }
                         $newCreator->{$key} = $val;
                     }
                     // Look for an equivalent creator in this library
                     $candidates = Zotero_Creators::getCreatorsWithData($item->libraryID, $newCreator, true);
                     if ($candidates) {
                         $c = Zotero_Creators::get($item->libraryID, $candidates[0]);
                         $item->setCreator($orderIndex, $c, $newCreatorTypeID);
                         continue;
                     }
                     // None found, so make a new one
                     $creatorID = $newCreator->save();
                     $newCreator = Zotero_Creators::get($item->libraryID, $creatorID);
                     $item->setCreator($orderIndex, $newCreator, $newCreatorTypeID);
                 }
                 // Remove all existing creators above the current index
                 if (!$isNew && ($indexes = array_keys($item->getCreators()))) {
                     $i = max($indexes);
                     while ($i > $orderIndex) {
                         $item->removeCreator($i);
                         $i--;
                     }
                 }
                 break;
             case 'tags':
                 // If item isn't yet saved, add tags below
                 if (!$item->id) {
                     $twoStage = true;
                     break;
                 }
                 if ($item->setTags($val)) {
                     $forceChange = true;
                 }
                 break;
             case 'attachments':
             case 'notes':
                 if (!$val) {
                     continue;
                 }
                 $twoStage = true;
                 break;
             case 'note':
                 $item->setNote($val);
                 break;
                 // Attachment properties
             // Attachment properties
             case 'linkMode':
                 $item->attachmentLinkMode = Zotero_Attachments::linkModeNameToNumber($val, true);
                 break;
             case 'contentType':
             case 'charset':
             case 'filename':
                 $k = "attachment" . ucwords($key);
                 $item->{$k} = $val;
                 break;
             case 'md5':
                 $item->attachmentStorageHash = $val;
                 break;
             case 'mtime':
                 $item->attachmentStorageModTime = $val;
                 break;
             default:
                 $item->setField($key, $val);
                 break;
         }
     }
     if ($parentItem) {
         $item->setSource($parentItem->id);
     }
     $item->deleted = !empty($json->deleted);
     // For changes that don't register as changes internally, force a dateModified update
     if ($forceChange) {
         $item->setField('dateModified', Zotero_DB::getTransactionTimestamp());
     }
     $item->save($userID);
     // Additional steps that have to be performed on a saved object
     if ($twoStage) {
         foreach ($json as $key => $val) {
             switch ($key) {
                 case 'attachments':
                     if (!$val) {
                         continue;
                     }
                     foreach ($val as $attachment) {
                         $childItem = new Zotero_Item();
                         $childItem->libraryID = $item->libraryID;
                         self::updateFromJSON($childItem, $attachment, true, $item, $userID);
                     }
                     break;
                 case 'notes':
                     if (!$val) {
                         continue;
                     }
                     $noteItemTypeID = Zotero_ItemTypes::getID("note");
                     foreach ($val as $note) {
                         $childItem = new Zotero_Item();
                         $childItem->libraryID = $item->libraryID;
                         $childItem->itemTypeID = $noteItemTypeID;
                         $childItem->setSource($item->id);
                         $childItem->setNote($note->note);
                         $childItem->save();
                     }
                     break;
                 case 'tags':
                     if ($item->setTags($val)) {
                         $forceChange = true;
                     }
                     break;
             }
         }
         // For changes that don't register as changes internally, force a dateModified update
         if ($forceChange) {
             $item->setField('dateModified', Zotero_DB::getTransactionTimestamp());
         }
         $item->save($userID);
     }
     Zotero_DB::commit();
 }
Ejemplo n.º 13
0
 /**
  * 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;
 }
Ejemplo n.º 14
0
	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;
	}
Ejemplo n.º 15
0
 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;
 }
Ejemplo n.º 16
0
 public static function updateFromJSON(Zotero_Item $item, $json, Zotero_Item $parentItem = null, $requestParams, $userID, $requireVersion = 0, $partialUpdate = false)
 {
     $json = Zotero_API::extractEditableJSON($json);
     $exists = Zotero_API::processJSONObjectKey($item, $json, $requestParams);
     // computerProgram used 'version' instead of 'versionNumber' before v3
     if ($requestParams['v'] < 3 && isset($json->version)) {
         $json->versionNumber = $json->version;
         unset($json->version);
     }
     Zotero_API::checkJSONObjectVersion($item, $json, $requestParams, $requireVersion);
     self::validateJSONItem($json, $item->libraryID, $exists ? $item : null, $parentItem || ($exists ? !!$item->getSourceKey() : false), $requestParams, $partialUpdate && $exists);
     $changed = false;
     $twoStage = false;
     if (!Zotero_DB::transactionInProgress()) {
         Zotero_DB::beginTransaction();
         $transactionStarted = true;
     } else {
         $transactionStarted = false;
     }
     // Set itemType first
     if (isset($json->itemType)) {
         $item->setField("itemTypeID", Zotero_ItemTypes::getID($json->itemType));
     }
     $changedDateModified = false;
     foreach ($json as $key => $val) {
         switch ($key) {
             case 'key':
             case 'version':
             case 'itemKey':
             case 'itemVersion':
             case 'itemType':
             case 'deleted':
                 continue;
             case 'parentItem':
                 $item->setSourceKey($val);
                 break;
             case 'creators':
                 if (!$val && !$item->numCreators()) {
                     continue 2;
                 }
                 $orderIndex = -1;
                 foreach ($val as $newCreatorData) {
                     // JSON uses 'name' and 'firstName'/'lastName',
                     // so switch to just 'firstName'/'lastName'
                     if (isset($newCreatorData->name)) {
                         $newCreatorData->firstName = '';
                         $newCreatorData->lastName = $newCreatorData->name;
                         unset($newCreatorData->name);
                         $newCreatorData->fieldMode = 1;
                     } else {
                         $newCreatorData->fieldMode = 0;
                     }
                     // Skip empty creators
                     if (Zotero_Utilities::unicodeTrim($newCreatorData->firstName) === "" && Zotero_Utilities::unicodeTrim($newCreatorData->lastName) === "") {
                         break;
                     }
                     $orderIndex++;
                     $newCreatorTypeID = Zotero_CreatorTypes::getID($newCreatorData->creatorType);
                     // Same creator in this position
                     $existingCreator = $item->getCreator($orderIndex);
                     if ($existingCreator && $existingCreator['ref']->equals($newCreatorData)) {
                         // Just change the creatorTypeID
                         if ($existingCreator['creatorTypeID'] != $newCreatorTypeID) {
                             $item->setCreator($orderIndex, $existingCreator['ref'], $newCreatorTypeID);
                         }
                         continue;
                     }
                     // Same creator in a different position, so use that
                     $existingCreators = $item->getCreators();
                     for ($i = 0, $len = sizeOf($existingCreators); $i < $len; $i++) {
                         if ($existingCreators[$i]['ref']->equals($newCreatorData)) {
                             $item->setCreator($orderIndex, $existingCreators[$i]['ref'], $newCreatorTypeID);
                             continue;
                         }
                     }
                     // Make a fake creator to use for the data lookup
                     $newCreator = new Zotero_Creator();
                     $newCreator->libraryID = $item->libraryID;
                     foreach ($newCreatorData as $key => $val) {
                         if ($key == 'creatorType') {
                             continue;
                         }
                         $newCreator->{$key} = $val;
                     }
                     // Look for an equivalent creator in this library
                     $candidates = Zotero_Creators::getCreatorsWithData($item->libraryID, $newCreator, true);
                     if ($candidates) {
                         $c = Zotero_Creators::get($item->libraryID, $candidates[0]);
                         $item->setCreator($orderIndex, $c, $newCreatorTypeID);
                         continue;
                     }
                     // None found, so make a new one
                     $creatorID = $newCreator->save();
                     $newCreator = Zotero_Creators::get($item->libraryID, $creatorID);
                     $item->setCreator($orderIndex, $newCreator, $newCreatorTypeID);
                 }
                 // Remove all existing creators above the current index
                 if ($exists && ($indexes = array_keys($item->getCreators()))) {
                     $i = max($indexes);
                     while ($i > $orderIndex) {
                         $item->removeCreator($i);
                         $i--;
                     }
                 }
                 break;
             case 'tags':
                 $item->setTags($val);
                 break;
             case 'collections':
                 $item->setCollections($val);
                 break;
             case 'relations':
                 $item->setRelations($val);
                 break;
             case 'attachments':
             case 'notes':
                 if (!$val) {
                     continue;
                 }
                 $twoStage = true;
                 break;
             case 'note':
                 $item->setNote($val);
                 break;
                 // Attachment properties
             // Attachment properties
             case 'linkMode':
                 $item->attachmentLinkMode = Zotero_Attachments::linkModeNameToNumber($val, true);
                 break;
             case 'contentType':
             case 'charset':
             case 'filename':
                 $k = "attachment" . ucwords($key);
                 $item->{$k} = $val;
                 break;
             case 'md5':
                 $item->attachmentStorageHash = $val;
                 break;
             case 'mtime':
                 $item->attachmentStorageModTime = $val;
                 break;
             case 'dateModified':
                 $changedDateModified = $item->setField($key, $val);
                 break;
             default:
                 $item->setField($key, $val);
                 break;
         }
     }
     if ($parentItem) {
         $item->setSource($parentItem->id);
     } else {
         if ($requestParams['v'] >= 2 && !$partialUpdate && $item->getSourceKey() && !isset($json->parentItem)) {
             $item->setSourceKey(false);
         }
     }
     $item->deleted = !empty($json->deleted);
     // If item has changed, update it with the current timestamp
     if ($item->hasChanged() && !$changedDateModified) {
         $item->dateModified = Zotero_DB::getTransactionTimestamp();
     }
     $changed = $item->save($userID) || $changed;
     // Additional steps that have to be performed on a saved object
     if ($twoStage) {
         foreach ($json as $key => $val) {
             switch ($key) {
                 case 'attachments':
                     if (!$val) {
                         continue;
                     }
                     foreach ($val as $attachmentJSON) {
                         $childItem = new Zotero_Item();
                         $childItem->libraryID = $item->libraryID;
                         self::updateFromJSON($childItem, $attachmentJSON, $item, $requestParams, $userID);
                     }
                     break;
                 case 'notes':
                     if (!$val) {
                         continue;
                     }
                     $noteItemTypeID = Zotero_ItemTypes::getID("note");
                     foreach ($val as $note) {
                         $childItem = new Zotero_Item();
                         $childItem->libraryID = $item->libraryID;
                         $childItem->itemTypeID = $noteItemTypeID;
                         $childItem->setSource($item->id);
                         $childItem->setNote($note->note);
                         $childItem->save();
                     }
                     break;
             }
         }
     }
     if ($transactionStarted) {
         Zotero_DB::commit();
     }
     return $changed;
 }
Ejemplo n.º 17
0
 public function setChildItems($itemIDs)
 {
     $shardID = Zotero_Shards::getByLibraryID($this->libraryID);
     Zotero_DB::beginTransaction();
     if (!$this->childItemsLoaded) {
         $this->loadChildItems();
     }
     $current = $this->childItems;
     $removed = array_diff($current, $itemIDs);
     $new = array_diff($itemIDs, $current);
     if ($removed) {
         $sql = "DELETE FROM collectionItems WHERE collectionID=? AND itemID IN (";
         $q = array();
         $params = array($this->id);
         foreach ($removed as $itemID) {
             $q[] = '?';
             $params[] = $itemID;
         }
         $sql .= implode(',', $q) . ")";
         Zotero_DB::query($sql, $params, $shardID);
     }
     if ($new) {
         $sql = "INSERT INTO collectionItems (collectionID, itemID) VALUES";
         $q = array();
         $params = array();
         foreach ($new as $itemID) {
             $q[] = '(?,?)';
             $params = array_merge($params, array($this->id, $itemID));
         }
         $sql .= implode(',', $q);
         Zotero_DB::query($sql, $params, $shardID);
     }
     $this->childItems = array_values(array_unique($itemIDs));
     $sql = "UPDATE collections SET dateModified=?, serverDateModified=? WHERE collectionID=?";
     $ts = Zotero_DB::getTransactionTimestamp();
     Zotero_DB::query($sql, array($ts, $ts, $this->id), $shardID);
     Zotero_DB::commit();
 }