/** * Converts a Zotero_Item object to a SimpleXMLElement item * * @param object $item Zotero_Item object * @param array $data * @return SimpleXMLElement Item data as SimpleXML element */ public static function convertItemToXML(Zotero_Item $item, $data = array()) { $t = microtime(true); // Check cache for all items except imported attachments, // which don't have their versions updated when the client // updates their file metadata if (!$item->isImportedAttachment()) { $cacheVersion = 1; $cacheKey = "syncXMLItem_" . $item->libraryID . "/" . $item->id . "_" . $item->version . "_" . md5(json_encode($data)) . "_" . $cacheVersion . (isset(Z_CONFIG::$CACHE_VERSION_SYNC_XML_ITEM) ? "_" . Z_CONFIG::$CACHE_VERSION_SYNC_XML_ITEM : ""); $xmlstr = Z_Core::$MC->get($cacheKey); } else { $cacheKey = false; $xmlstr = false; } if ($xmlstr) { $xml = new SimpleXMLElement($xmlstr); StatsD::timing("api.items.itemToSyncXML.cached", (microtime(true) - $t) * 1000); StatsD::increment("memcached.items.itemToSyncXML.hit"); // Skip the cache every 10 times for now, to ensure cache sanity if (Z_Core::probability(10)) { //$xmlstr = $xml->saveXML(); } else { Z_Core::debug("Using cached sync XML item"); return $xml; } } $xml = new SimpleXMLElement('<item/>'); // Primary fields foreach (self::$primaryFields as $field) { switch ($field) { case 'id': case 'serverDateModified': case 'version': continue 2; case 'itemTypeID': $xmlField = 'itemType'; $xmlValue = Zotero_ItemTypes::getName($item->{$field}); break; default: $xmlField = $field; $xmlValue = $item->{$field}; } $xml[$xmlField] = $xmlValue; } // Item data $itemTypeID = $item->itemTypeID; $fieldIDs = $item->getUsedFields(); foreach ($fieldIDs as $fieldID) { $val = $item->getField($fieldID); if ($val == '') { continue; } $f = $xml->addChild('field', htmlspecialchars($val)); $fieldName = Zotero_ItemFields::getName($fieldID); // Special handling for renamed computerProgram 'version' field if ($itemTypeID == 32 && $fieldName == 'versionNumber') { $fieldName = 'version'; } $f['name'] = htmlspecialchars($fieldName); } // Deleted item flag if ($item->deleted) { $xml['deleted'] = '1'; } if ($item->isNote() || $item->isAttachment()) { $sourceItemID = $item->getSource(); if ($sourceItemID) { $sourceItem = Zotero_Items::get($item->libraryID, $sourceItemID); if (!$sourceItem) { throw new Exception("Source item {$sourceItemID} not found"); } $xml['sourceItem'] = $sourceItem->key; } } // Group modification info $createdByUserID = null; $lastModifiedByUserID = null; switch (Zotero_Libraries::getType($item->libraryID)) { case 'group': $createdByUserID = $item->createdByUserID; $lastModifiedByUserID = $item->lastModifiedByUserID; break; } if ($createdByUserID) { $xml['createdByUserID'] = $createdByUserID; } if ($lastModifiedByUserID) { $xml['lastModifiedByUserID'] = $lastModifiedByUserID; } if ($item->isAttachment()) { $xml['linkMode'] = $item->attachmentLinkMode; $xml['mimeType'] = $item->attachmentMIMEType; if ($item->attachmentCharset) { $xml['charset'] = $item->attachmentCharset; } $storageModTime = $item->attachmentStorageModTime; if ($storageModTime) { $xml['storageModTime'] = $storageModTime; } $storageHash = $item->attachmentStorageHash; if ($storageHash) { $xml['storageHash'] = $storageHash; } // TODO: get from a constant if ($item->attachmentLinkMode != 3) { $xml->addChild('path', htmlspecialchars($item->attachmentPath)); } } // Note if ($item->isNote() || $item->isAttachment()) { // Get htmlspecialchars'ed note $note = $item->getNote(false, true); if ($note !== '') { $xml->addChild('note', $note); } else { if ($item->isNote()) { $xml->addChild('note', ''); } } } // Creators $creators = $item->getCreators(); if ($creators) { foreach ($creators as $index => $creator) { $c = $xml->addChild('creator'); $c['key'] = $creator['ref']->key; $c['creatorType'] = htmlspecialchars(Zotero_CreatorTypes::getName($creator['creatorTypeID'])); $c['index'] = $index; if (empty($data['updatedCreators']) || !in_array($creator['ref']->id, $data['updatedCreators'])) { $cNode = dom_import_simplexml($c); $creatorXML = Zotero_Creators::convertCreatorToXML($creator['ref'], $cNode->ownerDocument); $cNode->appendChild($creatorXML); } } } // Related items $relatedKeys = $item->relatedItems; $keys = array(); foreach ($relatedKeys as $relatedKey) { if (Zotero_Items::getByLibraryAndKey($item->libraryID, $relatedKey)) { $keys[] = $relatedKey; } } if ($keys) { $xml->related = implode(' ', $keys); } if ($xmlstr) { $uncached = $xml->saveXML(); if ($xmlstr != $uncached) { error_log("Cached sync XML item does not match"); error_log(" Cached: " . $xmlstr); error_log("Uncached: " . $uncached); } } else { $xmlstr = $xml->saveXML(); if ($cacheKey) { Z_Core::$MC->set($cacheKey, $xmlstr, 3600); // 1 hour for now } StatsD::timing("api.items.itemToSyncXML.uncached", (microtime(true) - $t) * 1000); StatsD::increment("memcached.items.itemToSyncXML.miss"); } return $xml; }
public static function updateFromJSON(Zotero_Item $item, $json, $isNew = false, Zotero_Item $parentItem = null, $userID = null) { self::validateJSONItem($json, $item->libraryID, $isNew ? null : $item, !is_null($parentItem)); Zotero_DB::beginTransaction(); // Mark library as updated if (!$isNew) { $timestamp = Zotero_Libraries::updateTimestamps($item->libraryID); Zotero_DB::registerTransactionTimestamp($timestamp); } $forceChange = false; $twoStage = false; // Set itemType first $item->setField("itemTypeID", Zotero_ItemTypes::getID($json->itemType)); foreach ($json as $key => $val) { switch ($key) { case 'itemType': continue; case 'deleted': continue; case 'creators': if (!$val && !$item->numCreators()) { continue 2; } $orderIndex = -1; foreach ($val as $orderIndex => $newCreatorData) { if ((!isset($newCreatorData->name) || trim($newCreatorData->name) == "") && (!isset($newCreatorData->firstName) || trim($newCreatorData->firstName) == "") && (!isset($newCreatorData->lastName) || trim($newCreatorData->lastName) == "")) { // This should never happen, because of check in validateJSONItem() if (!$isNew) { throw new Exception("Nameless creator in update request"); } // On item creation, ignore creators with empty names, // because that's in the item template that the API returns break; } // 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; } $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 (!$isNew && ($indexes = array_keys($item->getCreators()))) { $i = max($indexes); while ($i > $orderIndex) { $item->removeCreator($i); $i--; } } break; case 'tags': // If item isn't yet saved, add tags below if (!$item->id) { $twoStage = true; break; } if ($item->setTags($val)) { $forceChange = true; } 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; default: $item->setField($key, $val); break; } } if ($parentItem) { $item->setSource($parentItem->id); } $item->deleted = !empty($json->deleted); // For changes that don't register as changes internally, force a dateModified update if ($forceChange) { $item->setField('dateModified', Zotero_DB::getTransactionTimestamp()); } $item->save($userID); // 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 $attachment) { $childItem = new Zotero_Item(); $childItem->libraryID = $item->libraryID; self::updateFromJSON($childItem, $attachment, true, $item, $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; case 'tags': if ($item->setTags($val)) { $forceChange = true; } break; } } // For changes that don't register as changes internally, force a dateModified update if ($forceChange) { $item->setField('dateModified', Zotero_DB::getTransactionTimestamp()); } $item->save($userID); } Zotero_DB::commit(); }