public static function updateFromJSON(Zotero_Item $item, $json, Zotero_Item $parentItem = null, $requestParams, $userID, $requireVersion = 0, $partialUpdate = false) { $json = Zotero_API::extractEditableJSON($json); $exists = Zotero_API::processJSONObjectKey($item, $json, $requestParams); // computerProgram used 'version' instead of 'versionNumber' before v3 if ($requestParams['v'] < 3 && isset($json->version)) { $json->versionNumber = $json->version; unset($json->version); } Zotero_API::checkJSONObjectVersion($item, $json, $requestParams, $requireVersion); self::validateJSONItem($json, $item->libraryID, $exists ? $item : null, $parentItem || ($exists ? !!$item->getSourceKey() : false), $requestParams, $partialUpdate && $exists); $changed = false; $twoStage = false; if (!Zotero_DB::transactionInProgress()) { Zotero_DB::beginTransaction(); $transactionStarted = true; } else { $transactionStarted = false; } // Set itemType first if (isset($json->itemType)) { $item->setField("itemTypeID", Zotero_ItemTypes::getID($json->itemType)); } $changedDateModified = false; foreach ($json as $key => $val) { switch ($key) { case 'key': case 'version': case 'itemKey': case 'itemVersion': case 'itemType': case 'deleted': continue; case 'parentItem': $item->setSourceKey($val); break; case 'creators': if (!$val && !$item->numCreators()) { continue 2; } $orderIndex = -1; foreach ($val as $newCreatorData) { // JSON uses 'name' and 'firstName'/'lastName', // so switch to just 'firstName'/'lastName' if (isset($newCreatorData->name)) { $newCreatorData->firstName = ''; $newCreatorData->lastName = $newCreatorData->name; unset($newCreatorData->name); $newCreatorData->fieldMode = 1; } else { $newCreatorData->fieldMode = 0; } // Skip empty creators if (Zotero_Utilities::unicodeTrim($newCreatorData->firstName) === "" && Zotero_Utilities::unicodeTrim($newCreatorData->lastName) === "") { break; } $orderIndex++; $newCreatorTypeID = Zotero_CreatorTypes::getID($newCreatorData->creatorType); // Same creator in this position $existingCreator = $item->getCreator($orderIndex); if ($existingCreator && $existingCreator['ref']->equals($newCreatorData)) { // Just change the creatorTypeID if ($existingCreator['creatorTypeID'] != $newCreatorTypeID) { $item->setCreator($orderIndex, $existingCreator['ref'], $newCreatorTypeID); } continue; } // Same creator in a different position, so use that $existingCreators = $item->getCreators(); for ($i = 0, $len = sizeOf($existingCreators); $i < $len; $i++) { if ($existingCreators[$i]['ref']->equals($newCreatorData)) { $item->setCreator($orderIndex, $existingCreators[$i]['ref'], $newCreatorTypeID); continue; } } // Make a fake creator to use for the data lookup $newCreator = new Zotero_Creator(); $newCreator->libraryID = $item->libraryID; foreach ($newCreatorData as $key => $val) { if ($key == 'creatorType') { continue; } $newCreator->{$key} = $val; } // Look for an equivalent creator in this library $candidates = Zotero_Creators::getCreatorsWithData($item->libraryID, $newCreator, true); if ($candidates) { $c = Zotero_Creators::get($item->libraryID, $candidates[0]); $item->setCreator($orderIndex, $c, $newCreatorTypeID); continue; } // None found, so make a new one $creatorID = $newCreator->save(); $newCreator = Zotero_Creators::get($item->libraryID, $creatorID); $item->setCreator($orderIndex, $newCreator, $newCreatorTypeID); } // Remove all existing creators above the current index if ($exists && ($indexes = array_keys($item->getCreators()))) { $i = max($indexes); while ($i > $orderIndex) { $item->removeCreator($i); $i--; } } break; case 'tags': $item->setTags($val); break; case 'collections': $item->setCollections($val); break; case 'relations': $item->setRelations($val); break; case 'attachments': case 'notes': if (!$val) { continue; } $twoStage = true; break; case 'note': $item->setNote($val); break; // Attachment properties // Attachment properties case 'linkMode': $item->attachmentLinkMode = Zotero_Attachments::linkModeNameToNumber($val, true); break; case 'contentType': case 'charset': case 'filename': $k = "attachment" . ucwords($key); $item->{$k} = $val; break; case 'md5': $item->attachmentStorageHash = $val; break; case 'mtime': $item->attachmentStorageModTime = $val; break; case 'dateModified': $changedDateModified = $item->setField($key, $val); break; default: $item->setField($key, $val); break; } } if ($parentItem) { $item->setSource($parentItem->id); } else { if ($requestParams['v'] >= 2 && !$partialUpdate && $item->getSourceKey() && !isset($json->parentItem)) { $item->setSourceKey(false); } } $item->deleted = !empty($json->deleted); // If item has changed, update it with the current timestamp if ($item->hasChanged() && !$changedDateModified) { $item->dateModified = Zotero_DB::getTransactionTimestamp(); } $changed = $item->save($userID) || $changed; // Additional steps that have to be performed on a saved object if ($twoStage) { foreach ($json as $key => $val) { switch ($key) { case 'attachments': if (!$val) { continue; } foreach ($val as $attachmentJSON) { $childItem = new Zotero_Item(); $childItem->libraryID = $item->libraryID; self::updateFromJSON($childItem, $attachmentJSON, $item, $requestParams, $userID); } break; case 'notes': if (!$val) { continue; } $noteItemTypeID = Zotero_ItemTypes::getID("note"); foreach ($val as $note) { $childItem = new Zotero_Item(); $childItem->libraryID = $item->libraryID; $childItem->itemTypeID = $noteItemTypeID; $childItem->setSource($item->id); $childItem->setNote($note->note); $childItem->save(); } break; } } } if ($transactionStarted) { Zotero_DB::commit(); } return $changed; }
/** * @param Zotero_Collection $collection The collection object to update; * this should be either an existing * collection or a new collection * with a library assigned. * @param object $json Collection data to write * @param boolean [$requireVersion=0] See Zotero_API::checkJSONObjectVersion() * @return boolean True if the collection was changed, false otherwise */ public static function updateFromJSON(Zotero_Collection $collection, $json, $requestParams, $userID, $requireVersion = 0, $partialUpdate = false) { $json = Zotero_API::extractEditableJSON($json); $exists = Zotero_API::processJSONObjectKey($collection, $json, $requestParams); Zotero_API::checkJSONObjectVersion($collection, $json, $requestParams, $requireVersion); self::validateJSONCollection($json, $requestParams, $partialUpdate && $exists); $changed = false; if (!Zotero_DB::transactionInProgress()) { Zotero_DB::beginTransaction(); $transactionStarted = true; } else { $transactionStarted = false; } if (isset($json->name)) { $collection->name = $json->name; } if ($requestParams['v'] >= 2 && isset($json->parentCollection)) { $collection->parentKey = $json->parentCollection; } else { if ($requestParams['v'] < 2 && isset($json->parent)) { $collection->parentKey = $json->parent; } else { if (!$partialUpdate) { $collection->parent = false; } } } $changed = $collection->save() || $changed; if ($requestParams['v'] >= 2) { if (isset($json->relations)) { $changed = $collection->setRelations($json->relations, $userID) || $changed; } else { if (!$partialUpdate) { $changed = $collection->setRelations(new stdClass(), $userID) || $changed; } } } if ($transactionStarted) { Zotero_DB::commit(); } return $changed; }
/** * For single-object requests for some actions, require If-Unmodified-Since-Version, the * deprecated If-Match, or a JSON version property, and make sure the object hasn't been * modified * * @param {String} $objectType * @param {Zotero_DataObject} * @return {Boolean} - True if the object has been cleared for writing, or false if the JSON * version property still needs to pass */ protected function checkSingleObjectWriteVersion($objectType, $obj = null, $json = false) { if (!is_object($obj) && !is_null($obj)) { throw new Exception('$obj must be a data object or null'); } // In versions below 3, no writes to missing objects if (!$obj && $this->apiVersion < 3) { $this->e404(ucwords($objectType) . " not found"); } if (!in_array($objectType, array('item', 'collection', 'search', 'setting'))) { throw new Exception("Invalid object type"); } if (Z_CONFIG::$TESTING_SITE && !empty($_GET['skipetag'])) { return true; } // If-Match (deprecated) if ($this->apiVersion < 2) { if (empty($_SERVER['HTTP_IF_MATCH'])) { if ($this->method == 'DELETE') { $this->e428("If-Match must be provided for delete requests"); } else { return false; } } if (!preg_match('/^"?([a-f0-9]{32})"?$/', $_SERVER['HTTP_IF_MATCH'], $matches)) { $this->e400("Invalid ETag in If-Match header"); } if ($obj->etag != $matches[1]) { $this->e412("ETag does not match current version of {$objectType}"); } return true; } // Get version from If-Unmodified-Since-Version header $headerVersion = isset($_SERVER['HTTP_IF_UNMODIFIED_SINCE_VERSION']) ? $_SERVER['HTTP_IF_UNMODIFIED_SINCE_VERSION'] : false; // Get version from JSON 'version' property if ($json) { $json = Zotero_API::extractEditableJSON($json); if ($this->apiVersion >= 3) { $versionProp = 'version'; } else { $versionProp = $objectType == 'setting' ? 'version' : $objectType . "Version"; } $propVersion = isset($json->{$versionProp}) ? $json->{$versionProp} : false; } else { $propVersion = false; } if ($this->method == 'DELETE' && $headerVersion === false) { $this->e428("If-Unmodified-Since-Version must be provided for delete requests"); } if ($headerVersion !== false) { if (!is_numeric($headerVersion)) { $this->e400("Invalid If-Unmodified-Since-Version value '{$headerVersion}'"); } $headerVersion = (int) $headerVersion; } if ($propVersion !== false) { if (!is_numeric($propVersion)) { $this->e400("Invalid JSON 'version' property value '{$propVersion}'"); } $propVersion = (int) $propVersion; } // If both header and property given, they have to match if ($headerVersion !== false && $propVersion !== false && $headerVersion !== $propVersion) { $this->e400("If-Unmodified-Since-Version value does not match JSON '{$versionProp}' property " . "({$headerVersion} != {$propVersion})"); } $version = $headerVersion !== false ? $headerVersion : $propVersion; // If object doesn't exist, version has to be 0 if (!$obj) { if ($version !== 0) { $this->e404(ucwords($objectType) . " doesn't exist (to create, use version 0)"); } return true; } if ($version === false) { throw new HTTPException("Either If-Unmodified-Since-Version or object version " . "property must be provided for key-based writes", 428); } if ($obj->version !== $version) { $this->libraryVersion = $obj->version; $this->e412(ucwords($objectType) . " has been modified since specified version " . "(expected {$version}, found " . $obj->version . ")"); } return true; }
/** * @param Zotero_Searches $search The search object to update; * this should be either an existing * search or a new search * with a library assigned. * @param object $json Search data to write * @param boolean $requireVersion See Zotero_API::checkJSONObjectVersion() * @return bool True if the search was changed, false otherwise */ public static function updateFromJSON(Zotero_Search $search, $json, $requestParams, $userID, $requireVersion = 0, $partialUpdate = false) { $json = Zotero_API::extractEditableJSON($json); $exists = Zotero_API::processJSONObjectKey($search, $json, $requestParams); Zotero_API::checkJSONObjectVersion($search, $json, $requestParams, $requireVersion); self::validateJSONSearch($json, $requestParams, $partialUpdate && $exists); if (isset($json->name)) { $search->name = $json->name; } if (isset($json->conditions)) { $conditions = []; foreach ($json->conditions as $condition) { $newCondition = get_object_vars($condition); // Parse 'mode' (e.g., '/regexp') out of condition name if (preg_match('/(.+)\\/(.+)/', $newCondition['condition'], $matches)) { $newCondition['condition'] = $matches[1]; $newCondition['mode'] = $matches[2]; } else { $newCondition['mode'] = ""; } $conditions[] = $newCondition; } $search->updateConditions($conditions); } return !!$search->save(); }