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); }
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 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; }
/** * 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; }
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 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']); }
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(); }
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 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(); }
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 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; } }
/** * @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; } }
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; } }
/** * 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; }
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; }