Example #1
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;
	}
Example #2
0
 /**
  * $tags is an array of objects with properties 'tag' and 'type'
  */
 public function setTags($newTags)
 {
     if (!$this->id) {
         throw new Exception('itemID not set');
     }
     $numTags = $this->numTags();
     if (!$newTags && !$numTags) {
         return false;
     }
     Zotero_DB::beginTransaction();
     $existingTags = $this->getTags();
     $toAdd = array();
     $toRemove = array();
     // Get new tags not in existing
     for ($i = 0, $len = sizeOf($newTags); $i < $len; $i++) {
         if (!isset($newTags[$i]->type)) {
             $newTags[$i]->type = 0;
         }
         $name = trim($newTags[$i]->tag);
         // 'tag', not 'name', since that's what JSON uses
         $type = $newTags[$i]->type;
         foreach ($existingTags as $tag) {
             // Do a case-insensitive comparison, to match the client
             if (strtolower($tag->name) == strtolower($name) && $tag->type == $type) {
                 continue 2;
             }
         }
         $toAdd[] = $newTags[$i];
     }
     // Get existing tags not in new
     for ($i = 0, $len = sizeOf($existingTags); $i < $len; $i++) {
         $name = $existingTags[$i]->name;
         $type = $existingTags[$i]->type;
         foreach ($newTags as $tag) {
             if (strtolower($tag->tag) == strtolower($name) && $tag->type == $type) {
                 continue 2;
             }
         }
         $toRemove[] = $existingTags[$i];
     }
     foreach ($toAdd as $tag) {
         $name = $tag->tag;
         $type = $tag->type;
         $tagID = Zotero_Tags::getID($this->libraryID, $name, $type, true);
         if (!$tagID) {
             $tag = new Zotero_Tag();
             $tag->libraryID = $this->libraryID;
             $tag->name = $name;
             $tag->type = $type;
             $tagID = $tag->save();
         }
         $tag = Zotero_Tags::get($this->libraryID, $tagID);
         $tag->addItem($this->id);
         $tag->save();
     }
     foreach ($toRemove as $tag) {
         $tag->removeItem($this->id);
         $tag->save();
     }
     Zotero_DB::commit();
     return $toAdd || $toRemove;
 }