Beispiel #1
0
 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);
 }
Beispiel #2
0
 private function generateKey()
 {
     return Zotero_ID::getKey();
 }
Beispiel #3
0
 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']);
 }
Beispiel #4
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;
 }
Beispiel #5
0
 /**
  * Add sync process and associated locks to database
  */
 private static function addUploadProcess($userID, $libraryIDs, $syncQueueID = null, $syncProcessID = null)
 {
     Zotero_DB::beginTransaction();
     $syncProcessID = $syncProcessID ? $syncProcessID : Zotero_ID::getBigInt();
     $sql = "INSERT INTO syncProcesses (syncProcessID, userID) VALUES (?, ?)";
     try {
         Zotero_DB::query($sql, array($syncProcessID, $userID));
     } catch (Exception $e) {
         $sql = "SELECT CONCAT(syncProcessID,' ',userID,' ',started) FROM syncProcesses WHERE userID=?";
         $val = Zotero_DB::valueQuery($sql, $userID);
         Z_Core::logError($val);
     }
     if ($libraryIDs) {
         $sql = "INSERT INTO syncProcessLocks VALUES ";
         $sql .= implode(', ', array_fill(0, sizeOf($libraryIDs), '(?,?)'));
         $params = array();
         foreach ($libraryIDs as $libraryID) {
             $params[] = $syncProcessID;
             $params[] = $libraryID;
         }
         Zotero_DB::query($sql, $params);
     }
     // Record the process id in the queue entry, if given
     if ($syncQueueID) {
         $sql = "UPDATE syncUploadQueue SET syncProcessID=? WHERE syncUploadQueueID=?";
         Zotero_DB::query($sql, array($syncProcessID, $syncQueueID));
     }
     Zotero_DB::commit();
     return $syncProcessID;
 }
Beispiel #6
0
 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']);
 }
Beispiel #7
0
 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']);
 }
Beispiel #8
0
 private static function validateJSONItem($json, $libraryID, Zotero_Item $item = null, $isChild, $requestParams, $partialUpdate = false)
 {
     $isNew = !$item || !$item->version;
     if (!is_object($json)) {
         throw new Exception("Invalid item object (found " . gettype($json) . " '" . $json . "')", Z_ERROR_INVALID_INPUT);
     }
     if (isset($json->items) && is_array($json->items)) {
         throw new Exception("An 'items' array is not valid for single-item updates", Z_ERROR_INVALID_INPUT);
     }
     $apiVersion = $requestParams['v'];
     if ($partialUpdate) {
         $requiredProps = [];
     } else {
         if (isset($json->itemType) && $json->itemType == "attachment") {
             $requiredProps = array('linkMode', 'tags');
         } else {
             if (isset($json->itemType) && $json->itemType == "attachment") {
                 $requiredProps = array('tags');
             } else {
                 if ($isNew) {
                     $requiredProps = array('itemType');
                 } else {
                     if ($apiVersion < 2) {
                         $requiredProps = array('itemType', 'tags');
                     } else {
                         $requiredProps = array('itemType', 'tags', 'relations');
                         if (!$isChild) {
                             $requiredProps[] = 'collections';
                         }
                     }
                 }
             }
         }
     }
     foreach ($requiredProps as $prop) {
         if (!isset($json->{$prop})) {
             throw new Exception("'{$prop}' property not provided", Z_ERROR_INVALID_INPUT);
         }
     }
     // For partial updates where item type isn't provided, use the existing item type
     if (!isset($json->itemType) && $partialUpdate) {
         $itemType = Zotero_ItemTypes::getName($item->itemTypeID);
     } else {
         $itemType = $json->itemType;
     }
     foreach ($json as $key => $val) {
         switch ($key) {
             // Handled by Zotero_API::checkJSONObjectVersion()
             case 'key':
             case 'version':
                 if ($apiVersion < 3) {
                     throw new Exception("Invalid property '{$key}'", Z_ERROR_INVALID_INPUT);
                 }
                 break;
             case 'itemKey':
             case 'itemVersion':
                 if ($apiVersion != 2) {
                     throw new Exception("Invalid property '{$key}'", Z_ERROR_INVALID_INPUT);
                 }
                 break;
             case 'parentItem':
                 if ($apiVersion < 2) {
                     throw new Exception("Invalid property '{$key}'", Z_ERROR_INVALID_INPUT);
                 }
                 if (!Zotero_ID::isValidKey($val) && $val !== false) {
                     throw new Exception("'{$key}' must be a valid item key", Z_ERROR_INVALID_INPUT);
                 }
                 break;
             case 'itemType':
                 if (!is_string($val)) {
                     throw new Exception("'itemType' must be a string", Z_ERROR_INVALID_INPUT);
                 }
                 if ($isChild || !empty($json->parentItem)) {
                     switch ($val) {
                         case 'note':
                         case 'attachment':
                             break;
                         default:
                             throw new Exception("Child item must be note or attachment", Z_ERROR_INVALID_INPUT);
                     }
                 } else {
                     if ($val == 'attachment' && (!$item || !$item->getSource())) {
                         if ($json->linkMode == 'linked_url' || $json->linkMode == 'imported_url' && (empty($json->contentType) || $json->contentType != 'application/pdf')) {
                             throw new Exception("Only file attachments and PDFs can be top-level items", Z_ERROR_INVALID_INPUT);
                         }
                     }
                 }
                 if (!Zotero_ItemTypes::getID($val)) {
                     throw new Exception("'{$val}' is not a valid itemType", Z_ERROR_INVALID_INPUT);
                 }
                 break;
             case 'tags':
                 if (!is_array($val)) {
                     throw new Exception("'{$key}' property must be an array", Z_ERROR_INVALID_INPUT);
                 }
                 foreach ($val as $tag) {
                     $empty = true;
                     if (is_string($tag)) {
                         if ($tag === "") {
                             throw new Exception("Tag cannot be empty", Z_ERROR_INVALID_INPUT);
                         }
                         continue;
                     }
                     if (!is_object($tag)) {
                         throw new Exception("Tag must be an object", Z_ERROR_INVALID_INPUT);
                     }
                     foreach ($tag as $k => $v) {
                         switch ($k) {
                             case 'tag':
                                 if (!is_scalar($v)) {
                                     throw new Exception("Invalid tag name", Z_ERROR_INVALID_INPUT);
                                 }
                                 if ($v === "") {
                                     throw new Exception("Tag cannot be empty", Z_ERROR_INVALID_INPUT);
                                 }
                                 break;
                             case 'type':
                                 if (!is_numeric($v)) {
                                     throw new Exception("Invalid tag type '{$v}'", Z_ERROR_INVALID_INPUT);
                                 }
                                 break;
                             default:
                                 throw new Exception("Invalid tag property '{$k}'", Z_ERROR_INVALID_INPUT);
                         }
                         $empty = false;
                     }
                     if ($empty) {
                         throw new Exception("Tag object is empty", Z_ERROR_INVALID_INPUT);
                     }
                 }
                 break;
             case 'collections':
                 if (!is_array($val)) {
                     throw new Exception("'{$key}' property must be an array", Z_ERROR_INVALID_INPUT);
                 }
                 if ($isChild && $val) {
                     throw new Exception("Child items cannot be assigned to collections", Z_ERROR_INVALID_INPUT);
                 }
                 foreach ($val as $k) {
                     if (!Zotero_ID::isValidKey($k)) {
                         throw new Exception("'{$k}' is not a valid collection key", Z_ERROR_INVALID_INPUT);
                     }
                 }
                 break;
             case 'relations':
                 if ($apiVersion < 2) {
                     throw new Exception("Invalid property '{$key}'", Z_ERROR_INVALID_INPUT);
                 }
                 if (!is_object($val) && !(is_array($val) && empty($val))) {
                     throw new Exception("'{$key}' property must be an object", Z_ERROR_INVALID_INPUT);
                 }
                 foreach ($val as $predicate => $object) {
                     switch ($predicate) {
                         case 'owl:sameAs':
                         case 'dc:replaces':
                         case 'dc:relation':
                             break;
                         default:
                             throw new Exception("Unsupported predicate '{$predicate}'", Z_ERROR_INVALID_INPUT);
                     }
                     $arr = is_string($object) ? [$object] : $object;
                     foreach ($arr as $uri) {
                         if (!preg_match('/^http:\\/\\/zotero.org\\/(users|groups)\\/[0-9]+\\/(publications\\/)?items\\/[A-Z0-9]{8}$/', $uri)) {
                             throw new Exception("'{$key}' values currently must be Zotero item URIs", Z_ERROR_INVALID_INPUT);
                         }
                     }
                 }
                 break;
             case 'creators':
                 if (!is_array($val)) {
                     throw new Exception("'{$key}' property must be an array", Z_ERROR_INVALID_INPUT);
                 }
                 foreach ($val as $creator) {
                     $empty = true;
                     if (!isset($creator->creatorType)) {
                         throw new Exception("creator object must contain 'creatorType'", Z_ERROR_INVALID_INPUT);
                     }
                     if ((!isset($creator->name) || trim($creator->name) == "") && (!isset($creator->firstName) || trim($creator->firstName) == "") && (!isset($creator->lastName) || trim($creator->lastName) == "")) {
                         // On item creation, ignore single nameless creator,
                         // because that's in the item template that the API returns
                         if (sizeOf($val) == 1 && $isNew) {
                             continue;
                         } else {
                             throw new Exception("creator object must contain 'firstName'/'lastName' or 'name'", Z_ERROR_INVALID_INPUT);
                         }
                     }
                     foreach ($creator as $k => $v) {
                         switch ($k) {
                             case 'creatorType':
                                 $creatorTypeID = Zotero_CreatorTypes::getID($v);
                                 if (!$creatorTypeID) {
                                     throw new Exception("'{$v}' is not a valid creator type", Z_ERROR_INVALID_INPUT);
                                 }
                                 $itemTypeID = Zotero_ItemTypes::getID($itemType);
                                 if (!Zotero_CreatorTypes::isValidForItemType($creatorTypeID, $itemTypeID)) {
                                     // Allow 'author' in all item types, but reject other invalid creator types
                                     if ($creatorTypeID != Zotero_CreatorTypes::getID('author')) {
                                         throw new Exception("'{$v}' is not a valid creator type for item type '{$itemType}'", Z_ERROR_INVALID_INPUT);
                                     }
                                 }
                                 break;
                             case 'firstName':
                                 if (!isset($creator->lastName)) {
                                     throw new Exception("'lastName' creator field must be set if 'firstName' is set", Z_ERROR_INVALID_INPUT);
                                 }
                                 if (isset($creator->name)) {
                                     throw new Exception("'firstName' and 'name' creator fields are mutually exclusive", Z_ERROR_INVALID_INPUT);
                                 }
                                 break;
                             case 'lastName':
                                 if (!isset($creator->firstName)) {
                                     throw new Exception("'firstName' creator field must be set if 'lastName' is set", Z_ERROR_INVALID_INPUT);
                                 }
                                 if (isset($creator->name)) {
                                     throw new Exception("'lastName' and 'name' creator fields are mutually exclusive", Z_ERROR_INVALID_INPUT);
                                 }
                                 break;
                             case 'name':
                                 if (isset($creator->firstName)) {
                                     throw new Exception("'firstName' and 'name' creator fields are mutually exclusive", Z_ERROR_INVALID_INPUT);
                                 }
                                 if (isset($creator->lastName)) {
                                     throw new Exception("'lastName' and 'name' creator fields are mutually exclusive", Z_ERROR_INVALID_INPUT);
                                 }
                                 break;
                             default:
                                 throw new Exception("Invalid creator property '{$k}'", Z_ERROR_INVALID_INPUT);
                         }
                         $empty = false;
                     }
                     if ($empty) {
                         throw new Exception("Creator object is empty", Z_ERROR_INVALID_INPUT);
                     }
                 }
                 break;
             case 'note':
                 switch ($itemType) {
                     case 'note':
                     case 'attachment':
                         break;
                     default:
                         throw new Exception("'note' property is valid only for note and attachment items", Z_ERROR_INVALID_INPUT);
                 }
                 break;
             case 'attachments':
             case 'notes':
                 if ($apiVersion > 1) {
                     throw new Exception("'{$key}' property is no longer supported", Z_ERROR_INVALID_INPUT);
                 }
                 if (!$isNew) {
                     throw new Exception("'{$key}' property is valid only for new items", Z_ERROR_INVALID_INPUT);
                 }
                 if (!is_array($val)) {
                     throw new Exception("'{$key}' property must be an array", Z_ERROR_INVALID_INPUT);
                 }
                 foreach ($val as $child) {
                     // Check child item type ('attachment' or 'note')
                     $t = substr($key, 0, -1);
                     if (isset($child->itemType) && $child->itemType != $t) {
                         throw new Exception("Child {$t} must be of itemType '{$t}'", Z_ERROR_INVALID_INPUT);
                     }
                     if ($key == 'note') {
                         if (!isset($child->note)) {
                             throw new Exception("'note' property not provided for child note", Z_ERROR_INVALID_INPUT);
                         }
                     }
                 }
                 break;
             case 'deleted':
                 break;
                 // Attachment properties
             // Attachment properties
             case 'linkMode':
                 try {
                     $linkMode = Zotero_Attachments::linkModeNameToNumber($val, true);
                 } catch (Exception $e) {
                     throw new Exception("'{$val}' is not a valid linkMode", Z_ERROR_INVALID_INPUT);
                 }
                 // Don't allow changing of linkMode
                 if (!$isNew && $linkMode != $item->attachmentLinkMode) {
                     throw new Exception("Cannot change attachment linkMode", Z_ERROR_INVALID_INPUT);
                 }
                 break;
             case 'contentType':
             case 'charset':
             case 'filename':
             case 'md5':
             case 'mtime':
                 if ($itemType != 'attachment') {
                     throw new Exception("'{$key}' is valid only for attachment items", Z_ERROR_INVALID_INPUT);
                 }
                 switch ($key) {
                     case 'filename':
                     case 'md5':
                     case 'mtime':
                         $lm = isset($json->linkMode) ? $json->linkMode : Zotero_Attachments::linkModeNumberToName($item->attachmentLinkMode);
                         if (strpos(strtolower($lm), 'imported_') !== 0) {
                             throw new Exception("'{$key}' is valid only for imported attachment items", Z_ERROR_INVALID_INPUT);
                         }
                         break;
                 }
                 switch ($key) {
                     case 'contentType':
                     case 'charset':
                     case 'filename':
                         $propName = 'attachment' . ucwords($key);
                         break;
                     case 'md5':
                         $propName = 'attachmentStorageHash';
                         break;
                     case 'mtime':
                         $propName = 'attachmentStorageModTime';
                         break;
                 }
                 if (Zotero_Libraries::getType($libraryID) == 'group') {
                     if ($item && $item->{$propName} !== $val || !$item && $val !== null && $val !== "") {
                         throw new Exception("Cannot change '{$key}' directly in group library", Z_ERROR_INVALID_INPUT);
                     }
                 } else {
                     if ($key == 'md5') {
                         if ($val && !preg_match("/^[a-f0-9]{32}\$/", $val)) {
                             throw new Exception("'{$val}' is not a valid MD5 hash", Z_ERROR_INVALID_INPUT);
                         }
                     }
                 }
                 break;
             case 'accessDate':
                 if ($apiVersion >= 3 && $val !== '' && $val != 'CURRENT_TIMESTAMP' && !Zotero_Date::isSQLDate($val) && !Zotero_Date::isSQLDateTime($val) && !Zotero_Date::isISO8601($val)) {
                     throw new Exception("'{$key}' must be in ISO 8601 or UTC 'YYYY-MM-DD[ hh-mm-dd]' format or 'CURRENT_TIMESTAMP' ({$val})", Z_ERROR_INVALID_INPUT);
                 }
                 break;
             case 'dateAdded':
                 if (!Zotero_Date::isSQLDateTime($val) && !Zotero_Date::isISO8601($val)) {
                     throw new Exception("'{$key}' must be in ISO 8601 or UTC 'YYYY-MM-DD hh-mm-dd' format", Z_ERROR_INVALID_INPUT);
                 }
                 if (!$isNew) {
                     // Convert ISO date to SQL date for equality comparison
                     if (Zotero_Date::isISO8601($val)) {
                         $val = Zotero_Date::iso8601ToSQL($val);
                     }
                     if ($val != $item->{$key}) {
                         throw new Exception("'{$key}' cannot be modified for existing items", Z_ERROR_INVALID_INPUT);
                     }
                 }
                 break;
             case 'dateModified':
                 if (!Zotero_Date::isSQLDateTime($val) && !Zotero_Date::isISO8601($val)) {
                     throw new Exception("'{$key}' must be in ISO 8601 or UTC 'YYYY-MM-DD hh-mm-dd' format ({$val})", Z_ERROR_INVALID_INPUT);
                 }
                 break;
             default:
                 if (!Zotero_ItemFields::getID($key)) {
                     throw new Exception("Invalid property '{$key}'", Z_ERROR_INVALID_INPUT);
                 }
                 if (is_array($val)) {
                     throw new Exception("Unexpected array for property '{$key}'", Z_ERROR_INVALID_INPUT);
                 }
                 break;
         }
     }
     // Publications libraries have additional restrictions
     if (Zotero_Libraries::getType($libraryID) == 'publications') {
         Zotero_Publications::validateJSONItem($json);
     }
 }
 public function collections()
 {
     // Check for general library access
     if (!$this->permissions->canAccess($this->objectLibraryID)) {
         $this->e403();
     }
     if ($this->isWriteMethod()) {
         // Check for library write access
         if (!$this->permissions->canWrite($this->objectLibraryID)) {
             $this->e403("Write access denied");
         }
         // Make sure library hasn't been modified
         if (!$this->singleObject) {
             $libraryTimestampChecked = $this->checkLibraryIfUnmodifiedSinceVersion();
         }
         Zotero_Libraries::updateVersionAndTimestamp($this->objectLibraryID);
     }
     $collectionIDs = array();
     $collectionKeys = array();
     $results = array();
     // Single collection
     if ($this->singleObject) {
         $this->allowMethods(['HEAD', 'GET', 'PUT', 'PATCH', 'DELETE']);
         if (!Zotero_ID::isValidKey($this->objectKey)) {
             $this->e404();
         }
         $collection = Zotero_Collections::getByLibraryAndKey($this->objectLibraryID, $this->objectKey);
         if ($this->isWriteMethod()) {
             $collection = $this->handleObjectWrite('collection', $collection ? $collection : null);
             $this->queryParams['content'] = ['json'];
         }
         if (!$collection) {
             $this->e404("Collection not found");
         }
         $this->libraryVersion = $collection->version;
         if ($this->method == 'HEAD') {
             $this->end();
         }
         switch ($this->queryParams['format']) {
             case 'atom':
                 $this->responseXML = Zotero_Collections::convertCollectionToAtom($collection, $this->queryParams);
                 break;
             case 'json':
                 $json = $collection->toResponseJSON($this->queryParams, $this->permissions);
                 echo Zotero_Utilities::formatJSON($json);
                 break;
             default:
                 throw new Exception("Unexpected format '" . $this->queryParams['format'] . "'");
         }
     } else {
         $this->allowMethods(['HEAD', 'GET', 'POST', 'DELETE']);
         $this->libraryVersion = Zotero_Libraries::getUpdatedVersion($this->objectLibraryID);
         if ($this->scopeObject) {
             $this->allowMethods(array('GET'));
             switch ($this->scopeObject) {
                 case 'collections':
                     $collection = Zotero_Collections::getByLibraryAndKey($this->objectLibraryID, $this->scopeObjectKey);
                     if (!$collection) {
                         $this->e404("Collection not found");
                     }
                     $title = "Child Collections of ‘{$collection->name}'’";
                     $collectionIDs = $collection->getChildCollections();
                     break;
                 default:
                     throw new Exception("Invalid collections scope object '{$this->scopeObject}'");
             }
         } else {
             // Top-level items
             if ($this->subset == 'top') {
                 $this->allowMethods(array('GET'));
                 $title = "Top-Level Collections";
                 $results = Zotero_Collections::search($this->objectLibraryID, true, $this->queryParams);
             } else {
                 // Create a collection
                 if ($this->method == 'POST') {
                     $this->queryParams['format'] = 'writereport';
                     $obj = $this->jsonDecode($this->body);
                     $results = Zotero_Collections::updateMultipleFromJSON($obj, $this->queryParams, $this->objectLibraryID, $this->userID, $this->permissions, $libraryTimestampChecked ? 0 : 1, null);
                     if ($cacheKey = $this->getWriteTokenCacheKey()) {
                         Z_Core::$MC->set($cacheKey, true, $this->writeTokenCacheTime);
                     }
                     if ($this->apiVersion < 2) {
                         $uri = Zotero_API::getCollectionsURI($this->objectLibraryID);
                         $keys = array_merge(get_object_vars($results['success']), get_object_vars($results['unchanged']));
                         $queryString = "collectionKey=" . urlencode(implode(",", $keys)) . "&format=atom&content=json&order=collectionKeyList&sort=asc";
                         if ($this->apiKey) {
                             $queryString .= "&key=" . $this->apiKey;
                         }
                         $uri .= "?" . $queryString;
                         $this->queryParams = Zotero_API::parseQueryParams($queryString, $this->action, true, $this->apiVersion);
                         $title = "Collections";
                         $results = Zotero_Collections::search($this->objectLibraryID, false, $this->queryParams);
                     }
                 } else {
                     if ($this->method == 'DELETE') {
                         Zotero_DB::beginTransaction();
                         foreach ($this->queryParams['collectionKey'] as $collectionKey) {
                             Zotero_Collections::delete($this->objectLibraryID, $collectionKey);
                         }
                         Zotero_DB::commit();
                         $this->e204();
                     } else {
                         $title = "Collections";
                         $results = Zotero_Collections::search($this->objectLibraryID, false, $this->queryParams);
                     }
                 }
             }
         }
         if ($collectionIDs) {
             $this->queryParams['collectionIDs'] = $collectionIDs;
             $results = Zotero_Collections::search($this->objectLibraryID, false, $this->queryParams);
         }
         $options = ['action' => $this->action, 'uri' => $this->uri, 'results' => $results, 'requestParams' => $this->queryParams, 'permissions' => $this->permissions, 'head' => $this->method == 'HEAD'];
         switch ($this->queryParams['format']) {
             case 'atom':
                 $this->responseXML = Zotero_API::multiResponse(array_merge($options, ['title' => $this->getFeedNamePrefix($this->objectLibraryID) . $title]));
                 break;
             case 'json':
             case 'keys':
             case 'versions':
             case 'writereport':
                 Zotero_API::multiResponse($options);
                 break;
             default:
                 throw new Exception("Unexpected format '" . $this->queryParams['format'] . "'");
         }
     }
     $this->end();
 }
Beispiel #10
0
 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']);
 }
Beispiel #11
0
 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;
 }
Beispiel #12
0
 public function items()
 {
     // Check for general library access
     if (!$this->permissions->canAccess($this->objectLibraryID)) {
         $this->e403();
     }
     if ($this->isWriteMethod()) {
         // Check for library write access
         if (!$this->permissions->canWrite($this->objectLibraryID)) {
             $this->e403("Write access denied");
         }
         // Make sure library hasn't been modified
         if (!$this->singleObject) {
             $libraryTimestampChecked = $this->checkLibraryIfUnmodifiedSinceVersion();
         }
         // We don't update the library version in file mode, because currently
         // to avoid conflicts in the client the timestamp can't change
         // when the client updates file metadata
         if (!$this->fileMode) {
             Zotero_Libraries::updateVersionAndTimestamp($this->objectLibraryID);
         }
     }
     $itemIDs = array();
     $itemKeys = array();
     $results = array();
     $title = "";
     //
     // Single item
     //
     if ($this->singleObject) {
         if ($this->fileMode) {
             if ($this->fileView) {
                 $this->allowMethods(array('HEAD', 'GET', 'POST'));
             } else {
                 $this->allowMethods(array('HEAD', 'GET', 'PUT', 'POST', 'PATCH'));
             }
         } else {
             $this->allowMethods(array('HEAD', 'GET', 'PUT', 'PATCH', 'DELETE'));
         }
         if (!$this->objectLibraryID || !Zotero_ID::isValidKey($this->objectKey)) {
             $this->e404();
         }
         $item = Zotero_Items::getByLibraryAndKey($this->objectLibraryID, $this->objectKey);
         if ($item) {
             // If no access to the note, don't show that it exists
             if ($item->isNote() && !$this->permissions->canAccess($this->objectLibraryID, 'notes')) {
                 $this->e404();
             }
             // Make sure URL libraryID matches item libraryID
             if ($this->objectLibraryID != $item->libraryID) {
                 $this->e404("Item does not exist");
             }
             // File access mode
             if ($this->fileMode) {
                 $this->_handleFileRequest($item);
             }
             if ($this->scopeObject) {
                 switch ($this->scopeObject) {
                     // Remove item from collection
                     case 'collections':
                         $this->allowMethods(array('DELETE'));
                         $collection = Zotero_Collections::getByLibraryAndKey($this->objectLibraryID, $this->scopeObjectKey);
                         if (!$collection) {
                             $this->e404("Collection not found");
                         }
                         if (!$collection->hasItem($item->id)) {
                             $this->e404("Item not found in collection");
                         }
                         $collection->removeItem($item->id);
                         $this->e204();
                     default:
                         $this->e400();
                 }
             }
         } else {
             // Possibly temporary workaround to block unnecessary full syncs
             if ($this->fileMode && $this->httpAuth && $this->method == 'POST') {
                 // If > 2 requests for missing file, trigger a full sync via 404
                 $cacheKey = "apiMissingFile_" . $this->objectLibraryID . "_" . $this->objectKey;
                 $set = Z_Core::$MC->get($cacheKey);
                 if (!$set) {
                     Z_Core::$MC->set($cacheKey, 1, 86400);
                 } else {
                     if ($set < 2) {
                         Z_Core::$MC->increment($cacheKey);
                     } else {
                         Z_Core::$MC->delete($cacheKey);
                         $this->e404("A file sync error occurred. Please sync again.");
                     }
                 }
                 $this->e500("A file sync error occurred. Please sync again.");
             }
         }
         if ($this->isWriteMethod()) {
             $item = $this->handleObjectWrite('item', $item ? $item : null);
             if ($this->apiVersion < 2 && ($this->method == 'PUT' || $this->method == 'PATCH')) {
                 $this->queryParams['format'] = 'atom';
                 $this->queryParams['content'] = ['json'];
             }
         }
         if (!$item) {
             $this->e404("Item does not exist");
         }
         $this->libraryVersion = $item->version;
         if ($this->method == 'HEAD') {
             $this->end();
         }
         // Display item
         switch ($this->queryParams['format']) {
             case 'atom':
                 $this->responseXML = Zotero_Items::convertItemToAtom($item, $this->queryParams, $this->permissions);
                 break;
             case 'bib':
                 echo Zotero_Cite::getBibliographyFromCitationServer(array($item), $this->queryParams);
                 break;
             case 'csljson':
                 $json = Zotero_Cite::getJSONFromItems(array($item), true);
                 echo Zotero_Utilities::formatJSON($json);
                 break;
             case 'json':
                 $json = $item->toResponseJSON($this->queryParams, $this->permissions);
                 echo Zotero_Utilities::formatJSON($json);
                 break;
             default:
                 $export = Zotero_Translate::doExport(array($item), $this->queryParams['format']);
                 $this->queryParams['format'] = null;
                 header("Content-Type: " . $export['mimeType']);
                 echo $export['body'];
                 break;
         }
     } else {
         $this->allowMethods(array('HEAD', 'GET', 'POST', 'DELETE'));
         $this->libraryVersion = Zotero_Libraries::getUpdatedVersion($this->objectLibraryID);
         $includeTrashed = $this->queryParams['includeTrashed'];
         if ($this->scopeObject) {
             $this->allowMethods(array('GET', 'POST'));
             switch ($this->scopeObject) {
                 case 'collections':
                     // TEMP
                     if (Zotero_ID::isValidKey($this->scopeObjectKey)) {
                         $collection = Zotero_Collections::getByLibraryAndKey($this->objectLibraryID, $this->scopeObjectKey);
                     } else {
                         $collection = false;
                     }
                     if (!$collection) {
                         // If old collectionID, redirect
                         if ($this->method == 'GET' && Zotero_Utilities::isPosInt($this->scopeObjectKey)) {
                             $collection = Zotero_Collections::get($this->objectLibraryID, $this->scopeObjectKey);
                             if ($collection) {
                                 $qs = !empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '';
                                 $base = Zotero_API::getCollectionURI($collection);
                                 $this->redirect($base . "/items" . $qs, 301);
                             }
                         }
                         $this->e404("Collection not found");
                     }
                     // Add items to collection
                     if ($this->method == 'POST') {
                         $itemKeys = explode(' ', $this->body);
                         $itemIDs = array();
                         foreach ($itemKeys as $key) {
                             try {
                                 $item = Zotero_Items::getByLibraryAndKey($this->objectLibraryID, $key);
                             } catch (Exception $e) {
                                 if ($e->getCode() == Z_ERROR_OBJECT_LIBRARY_MISMATCH) {
                                     $item = false;
                                 } else {
                                     throw $e;
                                 }
                             }
                             if (!$item) {
                                 throw new Exception("Item '{$key}' not found in library", Z_ERROR_INVALID_INPUT);
                             }
                             if ($item->getSource()) {
                                 throw new Exception("Child items cannot be added to collections directly", Z_ERROR_INVALID_INPUT);
                             }
                             $itemIDs[] = $item->id;
                         }
                         $collection->addItems($itemIDs);
                         $this->e204();
                     }
                     if ($this->subset == 'top' || $this->apiVersion < 2) {
                         $title = "Top-Level Items in Collection ‘" . $collection->name . "’";
                         $itemIDs = $collection->getItems();
                     } else {
                         $title = "Items in Collection ‘" . $collection->name . "’";
                         $itemIDs = $collection->getItems(true);
                     }
                     break;
                 case 'tags':
                     if ($this->apiVersion >= 2) {
                         $this->e404();
                     }
                     $this->allowMethods(array('GET'));
                     $tagIDs = Zotero_Tags::getIDs($this->objectLibraryID, $this->scopeObjectName);
                     if (!$tagIDs) {
                         $this->e404("Tag not found");
                     }
                     foreach ($tagIDs as $tagID) {
                         $tag = new Zotero_Tag();
                         $tag->libraryID = $this->objectLibraryID;
                         $tag->id = $tagID;
                         // Use a real tag name, in case case differs
                         if (!$title) {
                             $title = "Items of Tag ‘" . $tag->name . "’";
                         }
                         $itemKeys = array_merge($itemKeys, $tag->getLinkedItems(true));
                     }
                     $itemKeys = array_unique($itemKeys);
                     break;
                 default:
                     $this->e404();
             }
         } else {
             // Top-level items
             if ($this->subset == 'top') {
                 $this->allowMethods(array('GET'));
                 $title = "Top-Level Items";
                 $results = Zotero_Items::search($this->objectLibraryID, true, $this->queryParams, $includeTrashed, $this->permissions);
             } else {
                 if ($this->subset == 'trash') {
                     $this->allowMethods(array('GET'));
                     $title = "Deleted Items";
                     $this->queryParams['trashedItemsOnly'] = true;
                     $includeTrashed = true;
                     $results = Zotero_Items::search($this->objectLibraryID, false, $this->queryParams, $includeTrashed, $this->permissions);
                 } else {
                     if ($this->subset == 'children') {
                         $item = Zotero_Items::getByLibraryAndKey($this->objectLibraryID, $this->objectKey);
                         if (!$item) {
                             $this->e404("Item not found");
                         }
                         if ($item->isAttachment()) {
                             $this->e400("/children cannot be called on attachment items");
                         }
                         if ($item->isNote()) {
                             $this->e400("/children cannot be called on note items");
                         }
                         if ($item->getSource()) {
                             $this->e400("/children cannot be called on child items");
                         }
                         // Create new child items
                         if ($this->method == 'POST') {
                             if ($this->apiVersion >= 2) {
                                 $this->allowMethods(array('GET'));
                             }
                             Zotero_DB::beginTransaction();
                             $obj = $this->jsonDecode($this->body);
                             $results = Zotero_Items::updateMultipleFromJSON($obj, $this->queryParams, $this->objectLibraryID, $this->userID, $this->permissions, $libraryTimestampChecked ? 0 : 1, $item);
                             Zotero_DB::commit();
                             if ($cacheKey = $this->getWriteTokenCacheKey()) {
                                 Z_Core::$MC->set($cacheKey, true, $this->writeTokenCacheTime);
                             }
                             $uri = Zotero_API::getItemsURI($this->objectLibraryID);
                             $keys = array_merge(get_object_vars($results['success']), get_object_vars($results['unchanged']));
                             $queryString = "itemKey=" . urlencode(implode(",", $keys)) . "&format=atom&content=json&order=itemKeyList&sort=asc";
                             if ($this->apiKey) {
                                 $queryString .= "&key=" . $this->apiKey;
                             }
                             $uri .= "?" . $queryString;
                             $this->queryParams = Zotero_API::parseQueryParams($queryString, $this->action, false);
                             $this->responseCode = 201;
                             $title = "Items";
                             $results = Zotero_Items::search($this->objectLibraryID, false, $this->queryParams, $includeTrashed, $this->permissions);
                         } else {
                             $title = "Child Items of ‘" . $item->getDisplayTitle() . "’";
                             $notes = $item->getNotes();
                             $attachments = $item->getAttachments();
                             $itemIDs = array_merge($notes, $attachments);
                         }
                     } else {
                         // Create new items
                         if ($this->method == 'POST') {
                             $this->queryParams['format'] = 'writereport';
                             $obj = $this->jsonDecode($this->body);
                             // Server-side translation
                             if (isset($obj->url)) {
                                 if ($this->apiVersion == 1) {
                                     Zotero_DB::beginTransaction();
                                 }
                                 $token = $this->getTranslationToken($obj);
                                 $results = Zotero_Items::addFromURL($obj, $this->queryParams, $this->objectLibraryID, $this->userID, $this->permissions, $token);
                                 if ($this->apiVersion == 1) {
                                     Zotero_DB::commit();
                                 }
                                 // Multiple choices
                                 if ($results instanceof stdClass) {
                                     $this->queryParams['format'] = null;
                                     header("Content-Type: application/json");
                                     if ($this->queryParams['v'] >= 2) {
                                         echo Zotero_Utilities::formatJSON(['url' => $obj->url, 'token' => $token, 'items' => $results->select]);
                                     } else {
                                         echo Zotero_Utilities::formatJSON($results->select);
                                     }
                                     $this->e300();
                                 } else {
                                     if (is_int($results)) {
                                         switch ($results) {
                                             case 501:
                                                 $this->e501("No translators found for URL");
                                                 break;
                                             default:
                                                 $this->e500("Error translating URL");
                                         }
                                     } else {
                                         if ($this->apiVersion == 1) {
                                             $uri = Zotero_API::getItemsURI($this->objectLibraryID);
                                             $keys = array_merge(get_object_vars($results['success']), get_object_vars($results['unchanged']));
                                             $queryString = "itemKey=" . urlencode(implode(",", $keys)) . "&format=atom&content=json&order=itemKeyList&sort=asc";
                                             if ($this->apiKey) {
                                                 $queryString .= "&key=" . $this->apiKey;
                                             }
                                             $uri .= "?" . $queryString;
                                             $this->queryParams = Zotero_API::parseQueryParams($queryString, $this->action, false);
                                             $this->responseCode = 201;
                                             $title = "Items";
                                             $results = Zotero_Items::search($this->objectLibraryID, false, $this->queryParams, $includeTrashed, $this->permissions);
                                         }
                                     }
                                 }
                                 // Otherwise return write status report
                             } else {
                                 if ($this->apiVersion < 2) {
                                     Zotero_DB::beginTransaction();
                                 }
                                 $results = Zotero_Items::updateMultipleFromJSON($obj, $this->queryParams, $this->objectLibraryID, $this->userID, $this->permissions, $libraryTimestampChecked ? 0 : 1, null);
                                 if ($this->apiVersion < 2) {
                                     Zotero_DB::commit();
                                     $uri = Zotero_API::getItemsURI($this->objectLibraryID);
                                     $keys = array_merge(get_object_vars($results['success']), get_object_vars($results['unchanged']));
                                     $queryString = "itemKey=" . urlencode(implode(",", $keys)) . "&format=atom&content=json&order=itemKeyList&sort=asc";
                                     if ($this->apiKey) {
                                         $queryString .= "&key=" . $this->apiKey;
                                     }
                                     $uri .= "?" . $queryString;
                                     $this->queryParams = Zotero_API::parseQueryParams($queryString, $this->action, false);
                                     $this->responseCode = 201;
                                     $title = "Items";
                                     $results = Zotero_Items::search($this->objectLibraryID, false, $this->queryParams, $includeTrashed, $this->permissions);
                                 }
                             }
                             if ($cacheKey = $this->getWriteTokenCacheKey()) {
                                 Z_Core::$MC->set($cacheKey, true, $this->writeTokenCacheTime);
                             }
                         } else {
                             if ($this->method == 'DELETE') {
                                 Zotero_DB::beginTransaction();
                                 foreach ($this->queryParams['itemKey'] as $itemKey) {
                                     Zotero_Items::delete($this->objectLibraryID, $itemKey);
                                 }
                                 Zotero_DB::commit();
                                 $this->e204();
                             } else {
                                 $title = "Items";
                                 $results = Zotero_Items::search($this->objectLibraryID, false, $this->queryParams, $includeTrashed, $this->permissions);
                             }
                         }
                     }
                 }
             }
         }
         if ($itemIDs || $itemKeys) {
             if ($itemIDs) {
                 $this->queryParams['itemIDs'] = $itemIDs;
             }
             if ($itemKeys) {
                 $this->queryParams['itemKey'] = $itemKeys;
             }
             $results = Zotero_Items::search($this->objectLibraryID, false, $this->queryParams, $includeTrashed, $this->permissions);
         }
         if ($this->queryParams['format'] == 'bib') {
             $maxBibItems = Zotero_API::MAX_BIBLIOGRAPHY_ITEMS;
             if ($results['total'] > $maxBibItems) {
                 $this->e413("Cannot generate bibliography with more than {$maxBibItems} items");
             }
         }
         $this->generateMultiResponse($results, $title);
     }
     $this->end();
 }
 public 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;
 }
 public function run()
 {
     // Catch TERM and unregister from the database
     //pcntl_signal(SIGTERM, array($this, 'handleSignal'));
     //pcntl_signal(SIGINT, array($this, 'handleSignal'));
     $this->log("Starting " . $this->mode . " processor daemon");
     $this->register();
     // Bind
     $socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
     $success = socket_bind($socket, $this->addr, $this->port);
     if (!$success) {
         $code = socket_last_error($socket);
         $this->unregister();
         die(socket_strerror($code));
     }
     $buffer = 'GO';
     $mode = null;
     $first = true;
     $lastPurge = 0;
     do {
         if ($first) {
             $first = false;
         } else {
             //$this->log("Waiting for command");
             $from = '';
             $port = 0;
             socket_recvfrom($socket, $buffer, 32, MSG_WAITALL, $from, $port);
         }
         //pcntl_signal_dispatch();
         // Processor return value
         if (preg_match('/^(DONE|NONE|LOCK|ERROR) ([0-9]+)/', $buffer, $return)) {
             $signal = $return[1];
             $id = $return[2];
             $this->removeProcessor($id);
             if ($signal == "DONE" || $signal == "ERROR") {
             } else {
                 if ($signal == "NONE") {
                     continue;
                 } else {
                     if ($signal == "LOCK") {
                         $this->log("LOCK received — waiting " . $this->lockWait . " second" . $this->pluralize($this->lockWait));
                         sleep($this->lockWait);
                     }
                 }
             }
             $buffer = "GO";
         }
         if ($buffer == "NEXT" || $buffer == "GO") {
             if ($lastPurge == 0) {
                 $lastPurge = microtime(true);
             } else {
                 if (microtime(true) - $lastPurge >= $this->minPurgeInterval) {
                     $purged = $this->purgeProcessors();
                     $this->log("Purged {$purged} lost processor" . $this->pluralize($purged));
                     $purged = $this->purgeOldProcesses();
                     $this->log("Purged {$purged} old process" . $this->pluralize($purged, "es"));
                     $lastPurge = microtime(true);
                 }
             }
             $numProcessors = $this->countProcessors();
             if ($numProcessors >= $this->maxProcessors) {
                 //$this->log("Already at max " . $this->maxProcessors . " processors");
                 continue;
             }
             try {
                 $queuedProcesses = $this->countQueuedProcesses();
                 $this->log($numProcessors . " processor" . $this->pluralize($numProcessors) . ", " . $queuedProcesses . " queued process" . $this->pluralize($queuedProcesses, "es"));
                 // Nothing queued, so go back and wait
                 if (!$queuedProcesses) {
                     continue;
                 }
                 // Wanna be startin' somethin'
                 $maxToStart = $this->maxProcessors - $numProcessors;
                 if ($queuedProcesses > $maxToStart) {
                     $toStart = $maxToStart;
                 } else {
                     $toStart = 1;
                 }
                 if ($toStart <= 0) {
                     $this->log("No processors to start");
                     continue;
                 }
                 $this->log("Starting {$toStart} new processor" . $this->pluralize($toStart));
                 // Start new processors
                 for ($i = 0; $i < $toStart; $i++) {
                     $id = Zotero_ID::getBigInt();
                     $pid = shell_exec(Z_CONFIG::$CLI_PHP_PATH . " " . Z_ENV_BASE_PATH . "processor/" . $this->mode . "/processor.php {$id} > /dev/null & echo \$!");
                     $this->processors[$id] = $pid;
                 }
             } catch (Exception $e) {
                 // If lost connection to MySQL, exit so we can be restarted
                 if (strpos($e->getMessage(), "MySQL error: MySQL server has gone away") === 0) {
                     $this->log($e);
                     $this->log("Lost connection to DB — exiting");
                     exit;
                 }
                 $this->log($e);
             }
         }
     } while ($buffer != 'QUIT');
     $this->log("QUIT received — exiting");
     $this->unregister();
 }
Beispiel #15
0
	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);
	}
Beispiel #16
0
 private function checkValue($field, $value)
 {
     if (!property_exists($this, '_' . $field)) {
         trigger_error("Invalid property '{$field}'", E_USER_ERROR);
     }
     // Data validation
     switch ($field) {
         case 'id':
         case 'libraryID':
             if (!Zotero_Utilities::isPosInt($value)) {
                 $this->invalidValueError($field, $value);
             }
             break;
         case 'key':
             if (!Zotero_ID::isValidKey($value)) {
                 $this->invalidValueError($field, $value);
             }
             break;
         case 'dateAdded':
         case 'dateModified':
             if (!preg_match("/^[0-9]{4}\\-[0-9]{2}\\-[0-9]{2} ([0-1][0-9]|[2][0-3]):([0-5][0-9]):([0-5][0-9])\$/", $value)) {
                 $this->invalidValueError($field, $value);
             }
             break;
         case 'name':
             if (mb_strlen($value) > Zotero_Collections::$maxLength) {
                 throw new Exception("Collection '" . $value . "' too long", Z_ERROR_COLLECTION_TOO_LONG);
             }
             break;
     }
 }
Beispiel #17
0
	/**
	 * @param {array<string>} $itemKeys
	 * @return {Boolean}  TRUE if related items were changed, FALSE if not
	 */
	private function setRelatedItems($itemKeys) {
		if (!is_array($itemKeys))  {
			throw new Exception('$itemKeys must be an array');
		}
		
		$predicate = Zotero_Relations::$relatedItemPredicate;
		
		$relations = $this->getRelations();
		if (!isset($relations->$predicate)) {
			$relations->$predicate = [];
		}
		else if (is_string($relations->$predicate)) {
			$relations->$predicate = [$relations->$predicate];
		}
		
		$currentKeys = array_map(function ($objectURI) {
			$key = substr($objectURI, -8);
			return Zotero_ID::isValidKey($key) ? $key : false;
		}, $relations->$predicate);
		$currentKeys = array_filter($currentKeys);
		
		$oldKeys = []; // items being kept
		$newKeys = []; // new items
		
		if (!$itemKeys) {
			if (!$currentKeys) {
				Z_Core::debug("No related items added", 4);
				return false;
			}
		}
		else {
			foreach ($itemKeys as $itemKey) {
				if ($itemKey == $this->key) {
					Z_Core::debug("Can't relate item to itself in Zotero.Item.setRelatedItems()", 2);
					continue;
				}
				
				if (in_array($itemKey, $currentKeys)) {
					Z_Core::debug("Item {$this->key} is already related to item $itemKey");
					$oldKeys[] = $itemKey;
					continue;
				}
				
				// TODO: check if related on other side (like client)?
				
				$newKeys[] = $itemKey;
			}
		}
		
		// If new or changed keys, update relations with new related items
		if ($newKeys || sizeOf($oldKeys) != sizeOf($currentKeys)) {
			$prefix = Zotero_URI::getLibraryURI($this->libraryID) . "/items/";
			$relations->$predicate = array_map(function ($key) use ($prefix) {
				return $prefix . $key;
			}, array_merge($oldKeys, $newKeys));
			$this->setRelations($relations);
			return true;
		}
		else {
			Z_Core::debug('Related items not changed', 4);
			return false;
		}
	}
Beispiel #18
0
 private function checkValue($field, $value)
 {
     if (!property_exists($this, $field)) {
         trigger_error("Invalid property '{$field}'", E_USER_ERROR);
     }
     // Data validation
     switch ($field) {
         case 'id':
         case 'libraryID':
             if (!Zotero_Utilities::isPosInt($value)) {
                 $this->invalidValueError($field, $value);
             }
             break;
         case 'key':
             if (!Zotero_ID::isValidKey($value)) {
                 $this->invalidValueError($field, $value);
             }
             break;
         case 'dateAdded':
         case 'dateModified':
             if (!preg_match("/^[0-9]{4}\\-[0-9]{2}\\-[0-9]{2} ([0-1][0-9]|[2][0-3]):([0-5][0-9]):([0-5][0-9])\$/", $value)) {
                 $this->invalidValueError($field, $value);
             }
             break;
     }
 }
Beispiel #19
0
 /**
  * Validate the object key from JSON and load the passed object with it
  *
  * @param object $object  Zotero_Item, Zotero_Collection, or Zotero_Search
  * @param json $json
  * @return boolean  True if the object exists, false if not
  */
 public static function processJSONObjectKey($object, $json, $requestParams)
 {
     $objectType = Zotero_Utilities::getObjectTypeFromObject($object);
     if (!in_array($objectType, array('item', 'collection', 'search'))) {
         throw new Exception("Invalid object type");
     }
     if ($requestParams['v'] >= 3) {
         $keyProp = 'key';
         $versionProp = 'version';
     } else {
         $keyProp = $objectType . "Key";
         $versionProp = $objectType == 'setting' ? 'version' : $objectType . "Version";
     }
     // Validate the object key if present and determine if the object is new
     if (isset($json->{$keyProp})) {
         if (!is_string($json->{$keyProp})) {
             throw new Exception("'{$keyProp}' must be a string", Z_ERROR_INVALID_INPUT);
         }
         if (!Zotero_ID::isValidKey($json->{$keyProp})) {
             throw new Exception("'" . $json->{$keyProp} . "' " . "is not a valid {$objectType} key", Z_ERROR_INVALID_INPUT);
         }
         if ($object->key) {
             if ($json->{$keyProp} != $object->key) {
                 throw new HTTPException("'{$keyProp}' property in JSON does not match " . "{$objectType} key of request", 409);
             }
             $exists = !!$object->id;
         } else {
             $object->key = $json->{$keyProp};
             $exists = !!$object->id;
         }
     } else {
         $exists = !!$object->key;
     }
     return $exists;
 }
Beispiel #20
0
 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;
 }