private function _testCreateByPut($objectType) { $objectTypePlural = API::getPluralObjectType($objectType); $json = API::createUnsavedDataObject($objectType); require_once '../../model/ID.inc.php'; $key = \Zotero_ID::getKey(); $response = API::userPut(self::$config['userID'], "{$objectTypePlural}/{$key}", json_encode($json), ["Content-Type: application/json", "If-Unmodified-Since-Version: 0"]); $this->assert204($response); }
/** * Import an item by URL using the translation server * * Initial request: * * { * "url": "http://..." * } * * Item selection for multi-item results: * * { * "url": "http://...", * "token": "<token>" * "items": { * "0": "Item 1 Title", * "3": "Item 2 Title" * } * } * * Returns an array of keys of added items (like updateMultipleFromJSON) or an object * with a 'select' property containing an array of titles for multi-item results */ public static function addFromURL($json, $requestParams, $libraryID, $userID, Zotero_Permissions $permissions, $translationToken) { if (!$translationToken) { throw new Exception("Translation token not provided"); } self::validateJSONURL($json, $requestParams); $cacheKey = 'addFromURLKeyMappings_' . md5($json->url . $translationToken); // Replace numeric keys with URLs for selected items if (isset($json->items) && $requestParams['v'] >= 2) { $keyMappings = Z_Core::$MC->get($cacheKey); $newItems = []; foreach ($json->items as $number => $title) { if (!isset($keyMappings[$number])) { throw new Exception("Index '{$number}' not found for URL and token", Z_ERROR_INVALID_INPUT); } $url = $keyMappings[$number]; $newItems[$url] = $title; } $json->items = $newItems; } $response = Zotero_Translate::doWeb($json->url, $translationToken, isset($json->items) ? $json->items : null); if (!$response || is_int($response)) { return $response; } if (isset($response->items)) { if ($requestParams['v'] >= 2) { $response = $response->items; for ($i = 0, $len = sizeOf($response); $i < $len; $i++) { // Assign key here so that we can add notes if necessary do { $itemKey = Zotero_ID::getKey(); } while (Zotero_Items::existsByLibraryAndKey($libraryID, $itemKey)); $response[$i]->key = $itemKey; // Pull out notes and stick in separate items if (isset($response[$i]->notes)) { foreach ($response[$i]->notes as $note) { $newNote = (object) ["itemType" => "note", "note" => $note->note, "parentItem" => $itemKey]; $response[] = $newNote; } unset($response[$i]->notes); } // TODO: link attachments, or not possible from translation-server? } } try { self::validateMultiObjectJSON($response, $requestParams); } catch (Exception $e) { error_log($e); error_log(json_encode($response)); throw new Exception("Invalid JSON from doWeb()"); } } else { if (isset($response->select)) { // Replace URLs with numeric keys for found items if ($requestParams['v'] >= 2) { $keyMappings = []; $newItems = new stdClass(); $number = 0; foreach ($response->select as $url => $title) { $keyMappings[$number] = $url; $newItems->{$number} = $title; $number++; } Z_Core::$MC->set($cacheKey, $keyMappings, 600); $response->select = $newItems; } return $response; } else { throw new Exception("Invalid return value from doWeb()"); } } return self::updateMultipleFromJSON($response, $requestParams, $libraryID, $userID, $permissions, false, null); }
public function save($userID=false) { if (!$this->_libraryID) { trigger_error("Library ID must be set before saving", E_USER_ERROR); } Zotero_Items::editCheck($this, $userID); if (!$this->hasChanged()) { Z_Core::debug("Item $this->id has not changed"); return false; } $this->cacheEnabled = false; // Make sure there are no gaps in the creator indexes $creators = $this->getCreators(); $lastPos = -1; foreach ($creators as $pos=>$creator) { if ($pos != $lastPos + 1) { trigger_error("Creator index $pos out of sequence for item $this->id", E_USER_ERROR); } $lastPos++; } $shardID = Zotero_Shards::getByLibraryID($this->_libraryID); $env = []; Zotero_DB::beginTransaction(); try { // // New item, insert and return id // if (!$this->id || (empty($this->changed['version']) && !$this->exists())) { Z_Core::debug('Saving data for new item to database'); $isNew = $env['isNew'] = true; $sqlColumns = array(); $sqlValues = array(); // // Primary fields // $itemID = $this->_id = $this->_id ? $this->_id : Zotero_ID::get('items'); $key = $this->_key = $this->_key ? $this->_key : Zotero_ID::getKey(); $sqlColumns = array( 'itemID', 'itemTypeID', 'libraryID', 'key', 'dateAdded', 'dateModified', 'serverDateModified', 'version' ); $timestamp = Zotero_DB::getTransactionTimestamp(); $dateAdded = $this->_dateAdded ? $this->_dateAdded : $timestamp; $dateModified = $this->_dateModified ? $this->_dateModified : $timestamp; $version = Zotero_Libraries::getUpdatedVersion($this->_libraryID); $sqlValues = array( $itemID, $this->_itemTypeID, $this->_libraryID, $key, $dateAdded, $dateModified, $timestamp, $version ); $sql = 'INSERT INTO items (`' . implode('`, `', $sqlColumns) . '`) VALUES ('; // Insert placeholders for bind parameters for ($i=0; $i<sizeOf($sqlValues); $i++) { $sql .= '?, '; } $sql = substr($sql, 0, -2) . ')'; // Save basic data to items table try { $insertID = Zotero_DB::query($sql, $sqlValues, $shardID); } catch (Exception $e) { if (strpos($e->getMessage(), "Incorrect datetime value") !== false) { preg_match("/Incorrect datetime value: '([^']+)'/", $e->getMessage(), $matches); throw new Exception("=Invalid date value '{$matches[1]}' for item $key", Z_ERROR_INVALID_INPUT); } throw $e; } if (!$this->_id) { if (!$insertID) { throw new Exception("Item id not available after INSERT"); } $itemID = $insertID; $this->_serverDateModified = $timestamp; } // Group item data if (Zotero_Libraries::getType($this->_libraryID) == 'group' && $userID) { $sql = "INSERT INTO groupItems VALUES (?, ?, ?)"; Zotero_DB::query($sql, array($itemID, $userID, $userID), $shardID); } // // ItemData // if (!empty($this->changed['itemData'])) { // Use manual bound parameters to speed things up $origInsertSQL = "INSERT INTO itemData (itemID, fieldID, value) VALUES "; $insertSQL = $origInsertSQL; $insertParams = array(); $insertCounter = 0; $maxInsertGroups = 40; $max = Zotero_Items::$maxDataValueLength; $fieldIDs = array_keys($this->changed['itemData']); foreach ($fieldIDs as $fieldID) { $value = $this->getField($fieldID, true, false, true); if ($value == 'CURRENT_TIMESTAMP' && Zotero_ItemFields::getID('accessDate') == $fieldID) { $value = Zotero_DB::getTransactionTimestamp(); } // Check length if (strlen($value) > $max) { $fieldName = Zotero_ItemFields::getLocalizedString( $this->_itemTypeID, $fieldID ); $msg = "=$fieldName field value " . "'" . mb_substr($value, 0, 50) . "...' too long"; if ($this->_key) { $msg .= " for item '" . $this->_libraryID . "/" . $key . "'"; } throw new Exception($msg, Z_ERROR_FIELD_TOO_LONG); } if ($insertCounter < $maxInsertGroups) { $insertSQL .= "(?,?,?),"; $insertParams = array_merge( $insertParams, array($itemID, $fieldID, $value) ); } if ($insertCounter == $maxInsertGroups - 1) { $insertSQL = substr($insertSQL, 0, -1); $stmt = Zotero_DB::getStatement($insertSQL, true, $shardID); Zotero_DB::queryFromStatement($stmt, $insertParams); $insertSQL = $origInsertSQL; $insertParams = array(); $insertCounter = -1; } $insertCounter++; } if ($insertCounter > 0 && $insertCounter < $maxInsertGroups) { $insertSQL = substr($insertSQL, 0, -1); $stmt = Zotero_DB::getStatement($insertSQL, true, $shardID); Zotero_DB::queryFromStatement($stmt, $insertParams); } } // // Creators // if (!empty($this->changed['creators'])) { $indexes = array_keys($this->changed['creators']); // TODO: group queries $sql = "INSERT INTO itemCreators (itemID, creatorID, creatorTypeID, orderIndex) VALUES "; $placeholders = array(); $sqlValues = array(); $cacheRows = array(); foreach ($indexes as $orderIndex) { Z_Core::debug('Adding creator in position ' . $orderIndex, 4); $creator = $this->getCreator($orderIndex); if (!$creator) { continue; } if ($creator['ref']->hasChanged()) { Z_Core::debug("Auto-saving changed creator {$creator['ref']->id}"); try { $creator['ref']->save(); } catch (Exception $e) { // TODO: Provide the item in question /*if (strpos($e->getCode() == Z_ERROR_CREATOR_TOO_LONG)) { $msg = $e->getMessage(); $msg = str_replace( "with this name and shorten it.", "with this name, or paste '$key' into the quick search bar " . "in the Zotero toolbar, and shorten the name." ); throw new Exception($msg, Z_ERROR_CREATOR_TOO_LONG); }*/ throw $e; } } $placeholders[] = "(?, ?, ?, ?)"; array_push( $sqlValues, $itemID, $creator['ref']->id, $creator['creatorTypeID'], $orderIndex ); $cacheRows[] = array( 'creatorID' => $creator['ref']->id, 'creatorTypeID' => $creator['creatorTypeID'], 'orderIndex' => $orderIndex ); } if ($sqlValues) { $sql = $sql . implode(',', $placeholders); Zotero_DB::query($sql, $sqlValues, $shardID); } } // Deleted item if (!empty($this->changed['deleted'])) { $deleted = $this->getDeleted(); if ($deleted) { $sql = "REPLACE INTO deletedItems (itemID) VALUES (?)"; } else { $sql = "DELETE FROM deletedItems WHERE itemID=?"; } Zotero_DB::query($sql, $itemID, $shardID); } // Note if ($this->isNote() || !empty($this->changed['note'])) { if (!is_string($this->noteText)) { $this->noteText = ''; } // If we don't have a sanitized note, generate one if (is_null($this->noteTextSanitized)) { $noteTextSanitized = Zotero_Notes::sanitize($this->noteText); // But if note is sanitized already, store empty string if ($this->noteText === $noteTextSanitized) { $this->noteTextSanitized = ''; } else { $this->noteTextSanitized = $noteTextSanitized; } } $this->noteTitle = Zotero_Notes::noteToTitle( $this->noteTextSanitized === '' ? $this->noteText : $this->noteTextSanitized ); $sql = "INSERT INTO itemNotes (itemID, sourceItemID, note, noteSanitized, title, hash) VALUES (?,?,?,?,?,?)"; $parent = $this->isNote() ? $this->getSource() : null; $hash = $this->noteText ? md5($this->noteText) : ''; $bindParams = array( $itemID, $parent ? $parent : null, $this->noteText !== null ? $this->noteText : '', $this->noteTextSanitized, $this->noteTitle, $hash ); try { Zotero_DB::query($sql, $bindParams, $shardID); } catch (Exception $e) { if (strpos($e->getMessage(), "Incorrect string value") !== false) { throw new Exception("=Invalid character in note '" . Zotero_Utilities::ellipsize($title, 70) . "'", Z_ERROR_INVALID_INPUT); } throw ($e); } Zotero_Notes::updateNoteCache($this->_libraryID, $itemID, $this->noteText); Zotero_Notes::updateHash($this->_libraryID, $itemID, $hash); } // Attachment if ($this->isAttachment()) { $sql = "INSERT INTO itemAttachments (itemID, sourceItemID, linkMode, mimeType, charsetID, path, storageModTime, storageHash) VALUES (?,?,?,?,?,?,?,?)"; $parent = $this->getSource(); if ($parent) { $parentItem = Zotero_Items::get($this->_libraryID, $parent); if (!$parentItem) { throw new Exception("Parent item $parent not found"); } if ($parentItem->getSource()) { $parentKey = $parentItem->key; throw new Exception("=Parent item $parentKey cannot be a child attachment", Z_ERROR_INVALID_INPUT); } } $linkMode = $this->attachmentLinkMode; $charsetID = Zotero_CharacterSets::getID($this->attachmentCharset); $path = $this->attachmentPath; $storageModTime = $this->attachmentStorageModTime; $storageHash = $this->attachmentStorageHash; $bindParams = array( $itemID, $parent ? $parent : null, $linkMode + 1, $this->attachmentMIMEType, $charsetID ? $charsetID : null, $path ? $path : '', $storageModTime ? $storageModTime : null, $storageHash ? $storageHash : null ); Zotero_DB::query($sql, $bindParams, $shardID); } // Sort fields $sortTitle = Zotero_Items::getSortTitle($this->getDisplayTitle(true)); if (mb_substr($sortTitle, 0, 5) == mb_substr($this->getField('title', false, true), 0, 5)) { $sortTitle = null; } $creatorSummary = $this->isRegularItem() ? mb_strcut($this->getCreatorSummary(true), 0, Zotero_Creators::$creatorSummarySortLength) : ''; $sql = "INSERT INTO itemSortFields (itemID, sortTitle, creatorSummary) VALUES (?, ?, ?)"; Zotero_DB::query($sql, array($itemID, $sortTitle, $creatorSummary), $shardID); // // Source item id // if ($sourceItemID = $this->getSource()) { $newSourceItem = Zotero_Items::get($this->_libraryID, $sourceItemID); if (!$newSourceItem) { throw new Exception("Cannot set source to invalid item"); } switch (Zotero_ItemTypes::getName($this->_itemTypeID)) { case 'note': $newSourceItem->incrementNoteCount(); break; case 'attachment': $newSourceItem->incrementAttachmentCount(); break; } } // Collections if (!empty($this->changed['collections'])) { foreach ($this->collections as $collectionKey) { $collection = Zotero_Collections::getByLibraryAndKey($this->_libraryID, $collectionKey); if (!$collection) { throw new Exception("Collection with key '$collectionKey' not found", Z_ERROR_COLLECTION_NOT_FOUND); } $collection->addItem($itemID); $collection->save(); } } // Tags if (!empty($this->changed['tags'])) { foreach ($this->tags as $tag) { $tagID = Zotero_Tags::getID($this->libraryID, $tag->tag, $tag->type); if ($tagID) { $tagObj = Zotero_Tags::get($this->_libraryID, $tagID); } else { $tagObj = new Zotero_Tag; $tagObj->libraryID = $this->_libraryID; $tagObj->name = $tag->tag; $tagObj->type = (int) $tag->type ? $tag->type : 0; } $tagObj->addItem($this->_key); $tagObj->save(); } } // Related items if (!empty($this->changed['relations'])) { $uri = Zotero_URI::getItemURI($this); $sql = "INSERT IGNORE INTO relations " . "(relationID, libraryID, `key`, subject, predicate, object) " . "VALUES (?, ?, ?, ?, ?, ?)"; $insertStatement = Zotero_DB::getStatement($sql, false, $shardID); foreach ($this->relations as $rel) { $insertStatement->execute( array( Zotero_ID::get('relations'), $this->_libraryID, Zotero_Relations::makeKey($uri, $rel[0], $rel[1]), $uri, $rel[0], $rel[1] ) ); } } // Remove from delete log if it's there $sql = "DELETE FROM syncDeleteLogKeys WHERE libraryID=? AND objectType='item' AND `key`=?"; Zotero_DB::query($sql, array($this->_libraryID, $key), $shardID); } // // Existing item, update // else { Z_Core::debug('Updating database with new item data for item ' . $this->_libraryID . '/' . $this->_key, 4); $isNew = $env['isNew'] = false; // // Primary fields // $sql = "UPDATE items SET "; $sqlValues = array(); $timestamp = Zotero_DB::getTransactionTimestamp(); $version = Zotero_Libraries::getUpdatedVersion($this->_libraryID); $updateFields = array( 'itemTypeID', 'libraryID', 'key', 'dateAdded', 'dateModified' ); if (!empty($this->changed['primaryData'])) { foreach ($updateFields as $updateField) { if (in_array($updateField, $this->changed['primaryData'])) { $sql .= "`$updateField`=?, "; $sqlValues[] = $this->{"_$updateField"}; } } } $sql .= "serverDateModified=?, version=? WHERE itemID=?"; array_push( $sqlValues, $timestamp, $version, $this->_id ); Zotero_DB::query($sql, $sqlValues, $shardID); $this->_serverDateModified = $timestamp; // Group item data if (Zotero_Libraries::getType($this->_libraryID) == 'group' && $userID) { $sql = "INSERT INTO groupItems VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE lastModifiedByUserID=?"; Zotero_DB::query($sql, array($this->_id, null, $userID, $userID), $shardID); } // // ItemData // if (!empty($this->changed['itemData'])) { $del = array(); $origReplaceSQL = "REPLACE INTO itemData (itemID, fieldID, value) VALUES "; $replaceSQL = $origReplaceSQL; $replaceParams = array(); $replaceCounter = 0; $maxReplaceGroups = 40; $max = Zotero_Items::$maxDataValueLength; $fieldIDs = array_keys($this->changed['itemData']); foreach ($fieldIDs as $fieldID) { $value = $this->getField($fieldID, true, false, true); // If field changed and is empty, mark row for deletion if ($value === "") { $del[] = $fieldID; continue; } if ($value == 'CURRENT_TIMESTAMP' && Zotero_ItemFields::getID('accessDate') == $fieldID) { $value = Zotero_DB::getTransactionTimestamp(); } // Check length if (strlen($value) > $max) { $fieldName = Zotero_ItemFields::getLocalizedString( $this->_itemTypeID, $fieldID ); $msg = "=$fieldName field value " . "'" . mb_substr($value, 0, 50) . "...' too long"; if ($this->_key) { $msg .= " for item '" . $this->_libraryID . "/" . $this->_key . "'"; } throw new Exception($msg, Z_ERROR_FIELD_TOO_LONG); } if ($replaceCounter < $maxReplaceGroups) { $replaceSQL .= "(?,?,?),"; $replaceParams = array_merge($replaceParams, array($this->_id, $fieldID, $value) ); } if ($replaceCounter == $maxReplaceGroups - 1) { $replaceSQL = substr($replaceSQL, 0, -1); $stmt = Zotero_DB::getStatement($replaceSQL, true, $shardID); Zotero_DB::queryFromStatement($stmt, $replaceParams); $replaceSQL = $origReplaceSQL; $replaceParams = array(); $replaceCounter = -1; } $replaceCounter++; } if ($replaceCounter > 0 && $replaceCounter < $maxReplaceGroups) { $replaceSQL = substr($replaceSQL, 0, -1); $stmt = Zotero_DB::getStatement($replaceSQL, true, $shardID); Zotero_DB::queryFromStatement($stmt, $replaceParams); } // Update memcached with used fields $fids = array(); foreach ($this->itemData as $fieldID=>$value) { if ($value !== false && $value !== null) { $fids[] = $fieldID; } } // Delete blank fields if ($del) { $sql = 'DELETE from itemData WHERE itemID=? AND fieldID IN ('; $sqlParams = array($this->_id); foreach ($del as $d) { $sql .= '?, '; $sqlParams[] = $d; } $sql = substr($sql, 0, -2) . ')'; Zotero_DB::query($sql, $sqlParams, $shardID); } } // // Creators // if (!empty($this->changed['creators'])) { $indexes = array_keys($this->changed['creators']); $sql = "INSERT INTO itemCreators (itemID, creatorID, creatorTypeID, orderIndex) VALUES "; $placeholders = array(); $sqlValues = array(); $cacheRows = array(); foreach ($indexes as $orderIndex) { Z_Core::debug('Creator in position ' . $orderIndex . ' has changed', 4); $creator = $this->getCreator($orderIndex); $sql2 = 'DELETE FROM itemCreators WHERE itemID=? AND orderIndex=?'; Zotero_DB::query($sql2, array($this->_id, $orderIndex), $shardID); if (!$creator) { continue; } if ($creator['ref']->hasChanged()) { Z_Core::debug("Auto-saving changed creator {$creator['ref']->id}"); $creator['ref']->save(); } $placeholders[] = "(?, ?, ?, ?)"; array_push( $sqlValues, $this->_id, $creator['ref']->id, $creator['creatorTypeID'], $orderIndex ); } if ($sqlValues) { $sql = $sql . implode(',', $placeholders); Zotero_DB::query($sql, $sqlValues, $shardID); } } // Deleted item if (!empty($this->changed['deleted'])) { $deleted = $this->getDeleted(); if ($deleted) { $sql = "REPLACE INTO deletedItems (itemID) VALUES (?)"; } else { $sql = "DELETE FROM deletedItems WHERE itemID=?"; } Zotero_DB::query($sql, $this->_id, $shardID); } // In case this was previously a standalone item, // delete from any collections it may have been in if (!empty($this->changed['source']) && $this->getSource()) { $sql = "DELETE FROM collectionItems WHERE itemID=?"; Zotero_DB::query($sql, $this->_id, $shardID); } // // Note or attachment note // if (!empty($this->changed['note'])) { // If we don't have a sanitized note, generate one if (is_null($this->noteTextSanitized)) { $noteTextSanitized = Zotero_Notes::sanitize($this->noteText); // But if note is sanitized already, store empty string if ($this->noteText == $noteTextSanitized) { $this->noteTextSanitized = ''; } else { $this->noteTextSanitized = $noteTextSanitized; } } $this->noteTitle = Zotero_Notes::noteToTitle( $this->noteTextSanitized === '' ? $this->noteText : $this->noteTextSanitized ); // Only record sourceItemID in itemNotes for notes if ($this->isNote()) { $sourceItemID = $this->getSource(); } $sourceItemID = !empty($sourceItemID) ? $sourceItemID : null; $hash = $this->noteText ? md5($this->noteText) : ''; $sql = "INSERT INTO itemNotes (itemID, sourceItemID, note, noteSanitized, title, hash) VALUES (?,?,?,?,?,?) ON DUPLICATE KEY UPDATE sourceItemID=?, note=?, noteSanitized=?, title=?, hash=?"; $bindParams = array( $this->_id, $sourceItemID, $this->noteText, $this->noteTextSanitized, $this->noteTitle, $hash, $sourceItemID, $this->noteText, $this->noteTextSanitized, $this->noteTitle, $hash ); Zotero_DB::query($sql, $bindParams, $shardID); Zotero_Notes::updateNoteCache($this->_libraryID, $this->_id, $this->noteText); Zotero_Notes::updateHash($this->_libraryID, $this->_id, $hash); // TODO: handle changed source? } // Attachment if (!empty($this->changed['attachmentData'])) { $sql = "INSERT INTO itemAttachments (itemID, sourceItemID, linkMode, mimeType, charsetID, path, storageModTime, storageHash) VALUES (?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE sourceItemID=VALUES(sourceItemID), linkMode=VALUES(linkMode), mimeType=VALUES(mimeType), charsetID=VALUES(charsetID), path=VALUES(path), storageModTime=VALUES(storageModTime), storageHash=VALUES(storageHash)"; $parent = $this->getSource(); if ($parent) { $parentItem = Zotero_Items::get($this->_libraryID, $parent); if (!$parentItem) { throw new Exception("Parent item $parent not found"); } if ($parentItem->getSource()) { $parentKey = $parentItem->key; throw new Exception("=Parent item $parentKey cannot be a child attachment", Z_ERROR_INVALID_INPUT); } } $linkMode = $this->attachmentLinkMode; $charsetID = Zotero_CharacterSets::getID($this->attachmentCharset); $path = $this->attachmentPath; $storageModTime = $this->attachmentStorageModTime; $storageHash = $this->attachmentStorageHash; $bindParams = array( $this->_id, $parent ? $parent : null, $linkMode + 1, $this->attachmentMIMEType, $charsetID ? $charsetID : null, $path ? $path : '', $storageModTime ? $storageModTime : null, $storageHash ? $storageHash : null ); Zotero_DB::query($sql, $bindParams, $shardID); } // Sort fields if (!empty($this->changed['primaryData']['itemTypeID']) || !empty($this->changed['itemData']) || !empty($this->changed['creators'])) { $sql = "UPDATE itemSortFields SET sortTitle=?"; $params = array(); $sortTitle = Zotero_Items::getSortTitle($this->getDisplayTitle(true)); if (mb_substr($sortTitle, 0, 5) == mb_substr($this->getField('title', false, true), 0, 5)) { $sortTitle = null; } $params[] = $sortTitle; if (!empty($this->changed['creators'])) { $creatorSummary = mb_strcut($this->getCreatorSummary(true), 0, Zotero_Creators::$creatorSummarySortLength); $sql .= ", creatorSummary=?"; $params[] = $creatorSummary; } $sql .= " WHERE itemID=?"; $params[] = $this->_id; Zotero_DB::query($sql, $params, $shardID); } // // Source item id // if (!empty($this->changed['source'])) { $type = Zotero_ItemTypes::getName($this->_itemTypeID); $Type = ucwords($type); // Update DB, if not a note or attachment we already changed above if (empty($this->changed['attachmentData']) && (empty($this->changed['note']) || !$this->isNote())) { $sql = "UPDATE item" . $Type . "s SET sourceItemID=? WHERE itemID=?"; $parent = $this->getSource(); $bindParams = array( $parent ? $parent : null, $this->_id ); Zotero_DB::query($sql, $bindParams, $shardID); } } if (false && !empty($this->changed['source'])) { trigger_error("Unimplemented", E_USER_ERROR); $newItem = Zotero_Items::get($this->_libraryID, $sourceItemID); // FK check if ($newItem) { if ($sourceItemID) { } else { trigger_error("Cannot set $type source to invalid item $sourceItemID", E_USER_ERROR); } } $oldSourceItemID = $this->getSource(); if ($oldSourceItemID == $sourceItemID) { Z_Core::debug("$Type source hasn't changed", 4); } else { $oldItem = Zotero_Items::get($this->_libraryID, $oldSourceItemID); if ($oldSourceItemID && $oldItem) { } else { //$oldItemNotifierData = null; Z_Core::debug("Old source item $oldSourceItemID didn't exist in setSource()", 2); } // If this was an independent item, remove from any collections where it // existed previously and add source instead if there is one if (!$oldSourceItemID) { $sql = "SELECT collectionID FROM collectionItems WHERE itemID=?"; $changedCollections = Zotero_DB::query($sql, $itemID, $shardID); if ($changedCollections) { trigger_error("Unimplemented", E_USER_ERROR); if ($sourceItemID) { $sql = "UPDATE OR REPLACE collectionItems " . "SET itemID=? WHERE itemID=?"; Zotero_DB::query($sql, array($sourceItemID, $this->_id), $shardID); } else { $sql = "DELETE FROM collectionItems WHERE itemID=?"; Zotero_DB::query($sql, $this->_id, $shardID); } } } $sql = "UPDATE item{$Type}s SET sourceItemID=? WHERE itemID=?"; $bindParams = array( $sourceItemID ? $sourceItemID : null, $itemID ); Zotero_DB::query($sql, $bindParams, $shardID); //Zotero.Notifier.trigger('modify', 'item', $this->_id, notifierData); // Update the counts of the previous and new sources if ($oldItem) { /* switch ($type) { case 'note': $oldItem->decrementNoteCount(); break; case 'attachment': $oldItem->decrementAttachmentCount(); break; } */ //Zotero.Notifier.trigger('modify', 'item', oldSourceItemID, oldItemNotifierData); } if ($newItem) { /* switch ($type) { case 'note': $newItem->incrementNoteCount(); break; case 'attachment': $newItem->incrementAttachmentCount(); break; } */ //Zotero.Notifier.trigger('modify', 'item', sourceItemID, newItemNotifierData); } } } // Collections if (!empty($this->changed['collections'])) { $oldCollections = $this->previousData['collections']; $newCollections = $this->collections; $toAdd = array_diff($newCollections, $oldCollections); $toRemove = array_diff($oldCollections, $newCollections); foreach ($toAdd as $collectionKey) { $collection = Zotero_Collections::getByLibraryAndKey($this->_libraryID, $collectionKey); if (!$collection) { throw new Exception("Collection with key '$collectionKey' not found", Z_ERROR_COLLECTION_NOT_FOUND); } $collection->addItem($this->_id); $collection->save(); } foreach ($toRemove as $collectionKey) { $collection = Zotero_Collections::getByLibraryAndKey($this->_libraryID, $collectionKey); $collection->removeItem($this->_id); $collection->save(); } } if (!empty($this->changed['tags'])) { $oldTags = $this->previousData['tags']; $newTags = $this->tags; $toAdd = []; $toRemove = []; // Get new tags not in existing for ($i=0, $len=sizeOf($newTags); $i<$len; $i++) { if (!isset($newTags[$i]->type)) { $newTags[$i]->type = 0; } $name = trim($newTags[$i]->tag); $type = $newTags[$i]->type; foreach ($oldTags as $tag) { // Do a case-insensitive comparison, to match the client if (strtolower($tag->name) == strtolower($name) && $tag->type == $type) { continue 2; } } $toAdd[] = $newTags[$i]; } // Get existing tags not in new for ($i=0, $len=sizeOf($oldTags); $i<$len; $i++) { $name = $oldTags[$i]->name; $type = $oldTags[$i]->type; foreach ($newTags as $tag) { if (strtolower($tag->tag) == strtolower($name) && $tag->type == $type) { continue 2; } } $toRemove[] = $oldTags[$i]; } foreach ($toAdd as $tag) { $name = $tag->tag; $type = $tag->type; $tagID = Zotero_Tags::getID($this->_libraryID, $name, $type, true); if (!$tagID) { $tag = new Zotero_Tag; $tag->libraryID = $this->_libraryID; $tag->name = $name; $tag->type = $type; $tagID = $tag->save(); } $tag = Zotero_Tags::get($this->_libraryID, $tagID); $tag->addItem($this->_key); $tag->save(); } foreach ($toRemove as $tag) { $tag->removeItem($this->_key); $tag->save(); } } // Related items if (!empty($this->changed['relations'])) { $removed = []; $new = []; $current = $this->relations; // TEMP // Convert old-style related items into relations $sql = "SELECT `key` FROM itemRelated IR " . "JOIN items I ON (IR.linkedItemID=I.itemID) " . "WHERE IR.itemID=?"; $toMigrate = Zotero_DB::columnQuery($sql, $this->_id, $shardID); if ($toMigrate) { $prefix = Zotero_URI::getLibraryURI($this->_libraryID) . "/items/"; $new = array_map(function ($key) use ($prefix) { return [ Zotero_Relations::$relatedItemPredicate, $prefix . $key ]; }, $toMigrate); $sql = "DELETE FROM itemRelated WHERE itemID=?"; Zotero_DB::query($sql, $this->_id, $shardID); } foreach ($this->previousData['relations'] as $rel) { if (array_search($rel, $current) === false) { $removed[] = $rel; } } foreach ($current as $rel) { if (array_search($rel, $this->previousData['relations']) !== false) { continue; } $new[] = $rel; } $uri = Zotero_URI::getItemURI($this); if ($removed) { $sql = "DELETE FROM relations WHERE libraryID=? AND `key`=?"; $deleteStatement = Zotero_DB::getStatement($sql, false, $shardID); foreach ($removed as $rel) { $params = [ $this->_libraryID, Zotero_Relations::makeKey($uri, $rel[0], $rel[1]) ]; $deleteStatement->execute($params); // TEMP // For owl:sameAs, delete reverse as well, since the client // can save that way if ($rel[0] == Zotero_Relations::$linkedObjectPredicate) { $params = [ $this->_libraryID, Zotero_Relations::makeKey($rel[1], $rel[0], $uri) ]; $deleteStatement->execute($params); } } } if ($new) { $sql = "INSERT IGNORE INTO relations " . "(relationID, libraryID, `key`, subject, predicate, object) " . "VALUES (?, ?, ?, ?, ?, ?)"; $insertStatement = Zotero_DB::getStatement($sql, false, $shardID); foreach ($new as $rel) { $insertStatement->execute( array( Zotero_ID::get('relations'), $this->_libraryID, Zotero_Relations::makeKey($uri, $rel[0], $rel[1]), $uri, $rel[0], $rel[1] ) ); // If adding a related item, the version on that item has to be // updated as well (if it exists). Otherwise, requests for that // item will return cached data without the new relation. if ($rel[0] == Zotero_Relations::$relatedItemPredicate) { $relatedItem = Zotero_URI::getURIItem($rel[1]); if (!$relatedItem) { Z_Core::debug("Related item " . $rel[1] . " does not exist " . "for item " . $this->_libraryKey); continue; } // If item has already changed, assume something else is taking // care of saving it and don't do so now, to avoid endless loops // with circular relations if ($relatedItem->hasChanged()) { continue; } $relatedItem->updateVersion($userID); } } } } } Zotero_DB::commit(); } catch (Exception $e) { Zotero_DB::rollback(); throw ($e); } $this->cacheEnabled = false; $this->finalizeSave($env); if ($isNew) { Zotero_Notifier::trigger('add', 'item', $this->_libraryID . "/" . $this->_key); return $this->_id; } Zotero_Notifier::trigger('modify', 'item', $this->_libraryID . "/" . $this->_key); return true; }
public function testNewEmptyLinkAttachmentItemWithItemKey() { $key = API::createItem("book", false, $this, 'key'); API::createAttachmentItem("linked_url", [], $key, $this, 'json'); $response = API::get("items/new?itemType=attachment&linkMode=linked_url"); $json = json_decode($response->getBody(), true); $json['parentItem'] = $key; require_once '../../model/Utilities.inc.php'; require_once '../../model/ID.inc.php'; $json['key'] = \Zotero_ID::getKey(); $json['version'] = 0; $response = API::userPost( self::$config['userID'], "items", json_encode([$json]), array("Content-Type: application/json") ); $this->assert200ForObject($response); }
private function generateKey() { return Zotero_ID::getKey(); }
public function testRelatedItemRelationsSingleRequest() { $uriPrefix = "http://zotero.org/users/" . self::$config['userID'] . "/items/"; // TEMP: Use autoloader require_once '../../model/Utilities.inc.php'; require_once '../../model/ID.inc.php'; $item1Key = \Zotero_ID::getKey(); $item2Key = \Zotero_ID::getKey(); $item1URI = $uriPrefix . $item1Key; $item2URI = $uriPrefix . $item2Key; $item1JSON = API::getItemTemplate('book'); $item1JSON->itemKey = $item1Key; $item1JSON->itemVersion = 0; $item1JSON->relations->{'dc:relation'} = $item2URI; $item2JSON = API::getItemTemplate('book'); $item2JSON->itemKey = $item2Key; $item2JSON->itemVersion = 0; $response = API::postItems([$item1JSON, $item2JSON]); $this->assert200($response); $json = API::getJSONFromResponse($response); // Make sure it exists on item 1 $xml = API::getItemXML($item1JSON->itemKey); $data = API::parseDataFromAtomEntry($xml); $json = json_decode($data['content'], true); $this->assertCount(1, $json['relations']); $this->assertEquals($item2URI, $json['relations']['dc:relation']); // And item 2, since related items are bidirectional $xml = API::getItemXML($item2JSON->itemKey); $data = API::parseDataFromAtomEntry($xml); $json = json_decode($data['content'], true); $this->assertCount(1, $json['relations']); $this->assertEquals($item1URI, $json['relations']['dc:relation']); }
public function testKeyedItemWithTags() { API::userClear(self::$config['userID']); // Create items with tags require_once '../../model/ID.inc.php'; $itemKey = \Zotero_ID::getKey(); $json = API::createItem("book", ["key" => $itemKey, "version" => 0, "tags" => [["tag" => "a"], ["tag" => "b"]]], $this, 'responseJSON'); $json = API::getItem($itemKey, $this, 'json')['data']; $this->assertCount(2, $json['tags']); $this->assertContains(['tag' => 'a'], $json['tags']); $this->assertContains(['tag' => 'b'], $json['tags']); }
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; }
public function save($userID = false) { if (!$this->_libraryID) { trigger_error("Library ID must be set before saving", E_USER_ERROR); } Zotero_Collections::editCheck($this, $userID); if (!$this->hasChanged()) { Z_Core::debug("Collection {$this->_id} has not changed"); return false; } $env = []; $isNew = $env['isNew'] = !$this->_id; Zotero_DB::beginTransaction(); try { $collectionID = $env['id'] = $this->_id = $this->_id ? $this->_id : Zotero_ID::get('collections'); Z_Core::debug("Saving collection {$this->_id}"); $key = $env['key'] = $this->_key = $this->_key ? $this->_key : Zotero_ID::getKey(); $timestamp = Zotero_DB::getTransactionTimestamp(); $dateAdded = $this->_dateAdded ? $this->_dateAdded : $timestamp; $dateModified = $this->_dateModified ? $this->_dateModified : $timestamp; $version = Zotero_Libraries::getUpdatedVersion($this->_libraryID); // Verify parent if ($this->_parentKey) { $newParentCollection = Zotero_Collections::getByLibraryAndKey($this->_libraryID, $this->_parentKey); if (!$newParentCollection) { // TODO: clear caches throw new Exception("Cannot set parent to invalid collection {$this->_parentKey}"); } if (!$isNew) { if ($newParentCollection->id == $collectionID) { trigger_error("Cannot move collection {$this->_id} into itself!", E_USER_ERROR); } // If the designated parent collection is already within this // collection (which shouldn't happen), move it to the root if (!$isNew && $this->hasDescendent('collection', $newParentCollection->id)) { $newParentCollection->parent = null; $newParentCollection->save(); } } $parent = $newParentCollection->id; } else { $parent = null; } $fields = "collectionName=?, parentCollectionID=?, libraryID=?, `key`=?,\n\t\t\t\t\t\tdateAdded=?, dateModified=?, serverDateModified=?, version=?"; $params = array($this->_name, $parent, $this->_libraryID, $key, $dateAdded, $dateModified, $timestamp, $version); $params = array_merge(array($collectionID), $params, $params); $shardID = Zotero_Shards::getByLibraryID($this->_libraryID); $sql = "INSERT INTO collections SET collectionID=?, {$fields}\n\t\t\t\t\tON DUPLICATE KEY UPDATE {$fields}"; Zotero_DB::query($sql, $params, $shardID); // Remove from delete log if it's there $sql = "DELETE FROM syncDeleteLogKeys WHERE libraryID=? AND objectType='collection' AND `key`=?"; Zotero_DB::query($sql, array($this->_libraryID, $key), $shardID); Zotero_DB::commit(); if (!empty($this->changed['parentKey'])) { $objectsClass = $this->objectsClass; // Add this item to the parent's cached item lists after commit, // if the parent was loaded if ($this->_parentKey) { $parentCollectionID = $objectsClass::getIDFromLibraryAndKey($this->_libraryID, $this->_parentKey); $objectsClass::registerChildCollection($parentCollectionID, $collectionID); } else { if (!$isNew && !empty($this->previousData['parentKey'])) { $parentCollectionID = $objectsClass::getIDFromLibraryAndKey($this->_libraryID, $this->previousData['parentKey']); $objectsClass::unregisterChildCollection($parentCollectionID, $collectionID); } } } } catch (Exception $e) { Zotero_DB::rollback(); throw $e; } $this->finalizeSave($env); return $isNew ? $this->_id : true; }
public function testRelatedItemRelationsSingleRequest() { $uriPrefix = "http://zotero.org/users/" . self::$config['userID'] . "/items/"; // TEMP: Use autoloader require_once '../../model/ID.inc.php'; $item1Key = \Zotero_ID::getKey(); $item2Key = \Zotero_ID::getKey(); $item1URI = $uriPrefix . $item1Key; $item2URI = $uriPrefix . $item2Key; $item1JSON = API::getItemTemplate('book'); $item1JSON->key = $item1Key; $item1JSON->version = 0; $item1JSON->relations->{'dc:relation'} = $item2URI; $item2JSON = API::getItemTemplate('book'); $item2JSON->key = $item2Key; $item2JSON->version = 0; $response = API::postItems([$item1JSON, $item2JSON]); $this->assert200($response); $json = API::getJSONFromResponse($response); // Make sure it exists on item 1 $json = API::getItem($item1JSON->key, $this, 'json')['data']; $this->assertCount(1, $json['relations']); $this->assertEquals($item2URI, $json['relations']['dc:relation']); // And item 2, since related items are bidirectional $json = API::getItem($item2JSON->key, $this, 'json')['data']; $this->assertCount(1, $json['relations']); $this->assertEquals($item1URI, $json['relations']['dc:relation']); }
public function save($userID = false, $full = false) { if (!$this->libraryID) { trigger_error("Library ID must be set before saving", E_USER_ERROR); } Zotero_Tags::editCheck($this, $userID); if (!$this->hasChanged()) { Z_Core::debug("Tag {$this->id} has not changed"); return false; } $shardID = Zotero_Shards::getByLibraryID($this->libraryID); Zotero_DB::beginTransaction(); try { $tagID = $this->id ? $this->id : Zotero_ID::get('tags'); $isNew = !$this->id; Z_Core::debug("Saving tag {$tagID}"); $key = $this->key ? $this->key : Zotero_ID::getKey(); $timestamp = Zotero_DB::getTransactionTimestamp(); $dateAdded = $this->dateAdded ? $this->dateAdded : $timestamp; $dateModified = $this->dateModified ? $this->dateModified : $timestamp; $version = $this->changed['name'] || $this->changed['type'] ? Zotero_Libraries::getUpdatedVersion($this->libraryID) : $this->version; $fields = "name=?, `type`=?, dateAdded=?, dateModified=?,\n\t\t\t\tlibraryID=?, `key`=?, serverDateModified=?, version=?"; $params = array($this->name, $this->type ? $this->type : 0, $dateAdded, $dateModified, $this->libraryID, $key, $timestamp, $version); try { if ($isNew) { $sql = "INSERT INTO tags SET tagID=?, {$fields}"; $stmt = Zotero_DB::getStatement($sql, true, $shardID); Zotero_DB::queryFromStatement($stmt, array_merge(array($tagID), $params)); // Remove from delete log if it's there $sql = "DELETE FROM syncDeleteLogKeys WHERE libraryID=?\n\t\t\t\t\t AND objectType='tag' AND `key`=?"; Zotero_DB::query($sql, array($this->libraryID, $key), $shardID); $sql = "DELETE FROM syncDeleteLogKeys WHERE libraryID=?\n\t\t\t\t\t AND objectType='tagName' AND `key`=?"; Zotero_DB::query($sql, array($this->libraryID, $this->name), $shardID); } else { $sql = "UPDATE tags SET {$fields} WHERE tagID=?"; $stmt = Zotero_DB::getStatement($sql, true, Zotero_Shards::getByLibraryID($this->libraryID)); Zotero_DB::queryFromStatement($stmt, array_merge($params, array($tagID))); } } catch (Exception $e) { // If an incoming tag is the same as an existing tag, but with a different key, // then delete the old tag and add its linked items to the new tag if (preg_match("/Duplicate entry .+ for key 'uniqueTags'/", $e->getMessage())) { // GET existing tag $existing = Zotero_Tags::getIDs($this->libraryID, $this->name); if (!$existing) { throw new Exception("Existing tag not found"); } foreach ($existing as $id) { $tag = Zotero_Tags::get($this->libraryID, $id, true); if ($tag->__get('type') == $this->type) { $linked = $tag->getLinkedItems(true); Zotero_Tags::delete($this->libraryID, $tag->key); break; } } // Save again if ($isNew) { $sql = "INSERT INTO tags SET tagID=?, {$fields}"; $stmt = Zotero_DB::getStatement($sql, true, $shardID); Zotero_DB::queryFromStatement($stmt, array_merge(array($tagID), $params)); // Remove from delete log if it's there $sql = "DELETE FROM syncDeleteLogKeys WHERE libraryID=?\n\t\t\t\t\t\t AND objectType='tag' AND `key`=?"; Zotero_DB::query($sql, array($this->libraryID, $key), $shardID); $sql = "DELETE FROM syncDeleteLogKeys WHERE libraryID=?\n\t\t\t\t\t\t AND objectType='tagName' AND `key`=?"; Zotero_DB::query($sql, array($this->libraryID, $this->name), $shardID); } else { $sql = "UPDATE tags SET {$fields} WHERE tagID=?"; $stmt = Zotero_DB::getStatement($sql, true, Zotero_Shards::getByLibraryID($this->libraryID)); Zotero_DB::queryFromStatement($stmt, array_merge($params, array($tagID))); } $new = array_unique(array_merge($linked, $this->getLinkedItems(true))); $this->setLinkedItems($new); } else { throw $e; } } // Linked items if ($full || $this->changed['linkedItems']) { $removeKeys = array(); $currentKeys = $this->getLinkedItems(true); if ($full) { $sql = "SELECT `key` FROM itemTags JOIN items " . "USING (itemID) WHERE tagID=?"; $stmt = Zotero_DB::getStatement($sql, true, $shardID); $dbKeys = Zotero_DB::columnQueryFromStatement($stmt, $tagID); if ($dbKeys) { $removeKeys = array_diff($dbKeys, $currentKeys); $newKeys = array_diff($currentKeys, $dbKeys); } else { $newKeys = $currentKeys; } } else { if (!empty($this->previousData['linkedItems'])) { $removeKeys = array_diff($this->previousData['linkedItems'], $currentKeys); $newKeys = array_diff($currentKeys, $this->previousData['linkedItems']); } else { $newKeys = $currentKeys; } } if ($removeKeys) { $sql = "DELETE itemTags FROM itemTags JOIN items USING (itemID) " . "WHERE tagID=? AND items.key IN (" . implode(', ', array_fill(0, sizeOf($removeKeys), '?')) . ")"; Zotero_DB::query($sql, array_merge(array($this->id), $removeKeys), $shardID); } if ($newKeys) { $sql = "INSERT INTO itemTags (tagID, itemID) " . "SELECT ?, itemID FROM items " . "WHERE libraryID=? AND `key` IN (" . implode(', ', array_fill(0, sizeOf($newKeys), '?')) . ")"; Zotero_DB::query($sql, array_merge(array($tagID, $this->libraryID), $newKeys), $shardID); } //Zotero.Notifier.trigger('add', 'collection-item', $this->id . '-' . $itemID); } Zotero_DB::commit(); Zotero_Tags::cachePrimaryData(array('id' => $tagID, 'libraryID' => $this->libraryID, 'key' => $key, 'name' => $this->name, 'type' => $this->type ? $this->type : 0, 'dateAdded' => $dateAdded, 'dateModified' => $dateModified, 'version' => $version)); } catch (Exception $e) { Zotero_DB::rollback(); throw $e; } // If successful, set values in object if (!$this->id) { $this->id = $tagID; } if (!$this->key) { $this->key = $key; } $this->init(); if ($isNew) { Zotero_Tags::cache($this); } return $this->id; }
public function testCreateKeyedCollections() { require_once '../../model/ID.inc.php'; $key1 = \Zotero_ID::getKey(); $name1 = "Test Collection 2"; $name2 = "Test Subcollection"; $json = [['key' => $key1, 'version' => 0, 'name' => $name1], ['name' => $name2, 'parentCollection' => $key1]]; $response = API::userPost(self::$config['userID'], "collections", json_encode($json), ["Content-Type: application/json"]); $this->assert200($response); $libraryVersion = $response->getHeader("Last-Modified-Version"); $json = API::getJSONFromResponse($response); $this->assertCount(2, $json['successful']); // Check data in write response $this->assertEquals($json['successful'][0]['key'], $json['successful'][0]['data']['key']); $this->assertEquals($json['successful'][1]['key'], $json['successful'][1]['data']['key']); $this->assertEquals($libraryVersion, $json['successful'][0]['version']); $this->assertEquals($libraryVersion, $json['successful'][1]['version']); $this->assertEquals($libraryVersion, $json['successful'][0]['data']['version']); $this->assertEquals($libraryVersion, $json['successful'][1]['data']['version']); $this->assertEquals($name1, $json['successful'][0]['data']['name']); $this->assertEquals($name2, $json['successful'][1]['data']['name']); $this->assertFalse($json['successful'][0]['data']['parentCollection']); $this->assertEquals($key1, $json['successful'][1]['data']['parentCollection']); // Check in separate request, to be safe $keys = array_map(function ($o) { return $o['key']; }, $json['successful']); $response = API::getCollectionResponse($keys); $this->assertTotalResults(2, $response); $json = API::getJSONFromResponse($response); $this->assertEquals($name1, $json[0]['data']['name']); $this->assertFalse($json[0]['data']['parentCollection']); $this->assertEquals($name2, $json[1]['data']['name']); $this->assertEquals($key1, $json[1]['data']['parentCollection']); }
public function save($userID = false) { if (!$this->libraryID) { trigger_error("Library ID must be set before saving", E_USER_ERROR); } Zotero_Creators::editCheck($this, $userID); // 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 : Zotero_ID::getKey(); $timestamp = Zotero_DB::getTransactionTimestamp(); $dateAdded = $this->dateAdded ? $this->dateAdded : $timestamp; $dateModified = !empty($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) { $name = $this->firstName; } else { if (strlen($this->lastName) > 255) { $name = $this->lastName; } else { throw $e; } } $name = mb_substr($name, 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); } 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); } // TODO: invalidate memcache? return $this->id; }
public function save($userID = false) { if (!$this->libraryID) { trigger_error("Library ID must be set before saving", E_USER_ERROR); } Zotero_Collections::editCheck($this, $userID); if (!$this->changed) { Z_Core::debug("Collection {$this->id} has not changed"); return false; } Zotero_DB::beginTransaction(); try { $collectionID = $this->id ? $this->id : Zotero_ID::get('collections'); Z_Core::debug("Saving collection {$this->id}"); $key = $this->key ? $this->key : Zotero_ID::getKey(); $timestamp = Zotero_DB::getTransactionTimestamp(); $dateAdded = $this->dateAdded ? $this->dateAdded : $timestamp; $dateModified = $this->dateModified ? $this->dateModified : $timestamp; $version = Zotero_Libraries::getUpdatedVersion($this->libraryID); // Verify parent if ($this->_parent) { if (is_int($this->_parent)) { $newParentCollection = Zotero_Collections::get($this->libraryID, $this->_parent); } else { $newParentCollection = Zotero_Collections::getByLibraryAndKey($this->libraryID, $this->_parent); } if (!$newParentCollection) { // TODO: clear caches throw new Exception("Cannot set parent to invalid collection {$this->_parent}"); } if ($newParentCollection->id == $this->id) { trigger_error("Cannot move collection {$this->id} into itself!", E_USER_ERROR); } // If the designated parent collection is already within this // collection (which shouldn't happen), move it to the root if ($this->id && $this->hasDescendent('collection', $newParentCollection->id)) { $newParentCollection->parent = null; $newParentCollection->save(); } $parent = $newParentCollection->id; } else { $parent = null; } $fields = "collectionName=?, parentCollectionID=?, libraryID=?, `key`=?,\n\t\t\t\t\t\tdateAdded=?, dateModified=?, serverDateModified=?, version=?"; $params = array($this->name, $parent, $this->libraryID, $key, $dateAdded, $dateModified, $timestamp, $version); $params = array_merge(array($collectionID), $params, $params); $shardID = Zotero_Shards::getByLibraryID($this->libraryID); $sql = "INSERT INTO collections SET collectionID=?, {$fields}\n\t\t\t\t\tON DUPLICATE KEY UPDATE {$fields}"; $insertID = Zotero_DB::query($sql, $params, $shardID); if (!$this->id) { if (!$insertID) { throw new Exception("Collection id not available after INSERT"); } $collectionID = $insertID; } // Remove from delete log if it's there $sql = "DELETE FROM syncDeleteLogKeys WHERE libraryID=? AND objectType='collection' AND `key`=?"; Zotero_DB::query($sql, array($this->libraryID, $key), $shardID); Zotero_DB::commit(); Zotero_Collections::cachePrimaryData(array('id' => $collectionID, 'libraryID' => $this->libraryID, 'key' => $key, 'name' => $this->name, 'dateAdded' => $dateAdded, 'dateModified' => $dateModified, 'parent' => $parent, 'version' => $version)); } catch (Exception $e) { Zotero_DB::rollback(); throw $e; } if (!$this->_id) { $this->_id = $collectionID; } if (!$this->_key) { $this->_key = $key; } return $this->_id; }