public static function getLibraryURI($libraryID, $skipNames = false) { $libraryType = Zotero_Libraries::getType($libraryID); switch ($libraryType) { case 'user': $id = Zotero_Users::getUserIDFromLibraryID($libraryID); return self::getUserURI($id, $skipNames); case 'group': $id = Zotero_Groups::getGroupIDFromLibraryID($libraryID); $group = Zotero_Groups::get($id); return self::getGroupURI($group, $skipNames); } }
public static function getLibraryURI($libraryID, $www = false, $useSlug = false) { $libraryType = Zotero_Libraries::getType($libraryID); switch ($libraryType) { case 'user': $id = Zotero_Users::getUserIDFromLibraryID($libraryID); return self::getUserURI($id, $www, $useSlug); case 'publications': $id = Zotero_Users::getUserIDFromLibraryID($libraryID); return self::getUserURI($id, $www, $useSlug) . "/publications"; case 'group': $id = Zotero_Groups::getGroupIDFromLibraryID($libraryID); $group = Zotero_Groups::get($id); return self::getGroupURI($group, $www, $useSlug); default: throw new Exception("Invalid library type '{$libraryType}'"); } }
public static function getUserKeysWithLibrary($userID, $libraryID) { $libraryType = Zotero_Libraries::getType($libraryID); $sql = "SELECT keyID FROM `keys` JOIN keyPermissions USING (keyID) " . "WHERE userID=? AND (libraryID=?"; // If group library, include keys with access to all groups if ($libraryType == 'group') { $sql .= " OR libraryID=0"; } $sql .= ") AND permission='library' AND granted=1"; $keyIDs = Zotero_DB::columnQuery($sql, [$userID, $libraryID]); $keys = []; if ($keyIDs) { foreach ($keyIDs as $keyID) { $keyObj = new Zotero_Key(); $keyObj->id = $keyID; $keys[] = $keyObj; } } return $keys; }
private static function validateJSONItem($json, $libraryID, $item = null, $isChild = false) { $isNew = !$item; 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 item updates", Z_ERROR_INVALID_INPUT); } 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 { $requiredProps = array('itemType', 'creators', 'tags'); } } } foreach ($requiredProps as $prop) { if (!isset($json->{$prop})) { throw new Exception("'{$prop}' property not provided", Z_ERROR_INVALID_INPUT); } } foreach ($json as $key => $val) { switch ($key) { case 'itemType': if (!is_string($val)) { throw new Exception("'itemType' must be a string", Z_ERROR_INVALID_INPUT); } if ($isChild) { 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("'tags' property must be an array", Z_ERROR_INVALID_INPUT); } foreach ($val as $tag) { $empty = true; 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 'creators': if (!is_array($val)) { throw new Exception("'creators' 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($json->itemType); if (!Zotero_CreatorTypes::isValidForItemType($creatorTypeID, $itemTypeID)) { throw new Exception("'{$v}' is not a valid creator type for item type '" . $json->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 ($json->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 (!$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 ($item && $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 ($json->itemType != 'attachment') { throw new Exception("'{$key}' is valid only for attachment items", Z_ERROR_INVALID_INPUT); } switch ($key) { case 'filename': case 'md5': case 'mtime': if (strpos($json->linkMode, '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; 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; } } }
public function toResponseJSON($requestParams=[], Zotero_Permissions $permissions, $sharedData=null) { $t = microtime(true); if (!$this->loaded['primaryData']) { $this->loadPrimaryData(); } if (!$this->loaded['itemData']) { $this->loadItemData(); } // Uncached stuff or parts of the cache key $version = $this->version; $parent = $this->getSource(); $isRegularItem = !$parent && $this->isRegularItem(); $downloadDetails = $permissions->canAccess($this->libraryID, 'files') ? Zotero_Storage::getDownloadDetails($this) : false; if ($isRegularItem) { $numChildren = $permissions->canAccess($this->libraryID, 'notes') ? $this->numChildren() : $this->numAttachments(); } $libraryType = Zotero_Libraries::getType($this->libraryID); // Any query parameters that have an effect on the output // need to be added here $allowedParams = [ 'include', 'style', 'css', 'linkwrap' ]; $cachedParams = Z_Array::filterKeys($requestParams, $allowedParams); $cacheVersion = 1; $cacheKey = "jsonEntry_" . $this->libraryID . "/" . $this->id . "_" . md5( $version . json_encode($cachedParams) . ($downloadDetails ? 'hasFile' : '') // For groups, include the group WWW URL, which can change . ($libraryType == 'group' ? Zotero_URI::getItemURI($this, true) : '') ) . "_" . $requestParams['v'] // For code-based changes . "_" . $cacheVersion // For data-based changes . (isset(Z_CONFIG::$CACHE_VERSION_RESPONSE_JSON_ITEM) ? "_" . Z_CONFIG::$CACHE_VERSION_RESPONSE_JSON_ITEM : "") // If there's bib content, include the bib cache version . ((in_array('bib', $requestParams['include']) && isset(Z_CONFIG::$CACHE_VERSION_BIB)) ? "_" . Z_CONFIG::$CACHE_VERSION_BIB : ""); $cached = Z_Core::$MC->get($cacheKey); if (false && $cached) { // Make sure numChildren reflects the current permissions if ($isRegularItem) { $json = json_decode($cached); $json['numChildren'] = $numChildren; $cached = json_encode($json); } //StatsD::timing("api.items.itemToResponseJSON.cached", (microtime(true) - $t) * 1000); //StatsD::increment("memcached.items.itemToResponseJSON.hit"); // Skip the cache every 10 times for now, to ensure cache sanity if (!Z_Core::probability(10)) { return $cached; } } $json = [ 'key' => $this->key, 'version' => $version, 'library' => Zotero_Libraries::toJSON($this->libraryID) ]; $json['links'] = [ 'self' => [ 'href' => Zotero_API::getItemURI($this), 'type' => 'application/json' ], 'alternate' => [ 'href' => Zotero_URI::getItemURI($this, true), 'type' => 'text/html' ] ]; if ($parent) { $parentItem = Zotero_Items::get($this->libraryID, $parent); $json['links']['up'] = [ 'href' => Zotero_API::getItemURI($parentItem), 'type' => 'application/json' ]; } // If appropriate permissions and the file is stored in ZFS, get file request link if ($downloadDetails) { $details = $downloadDetails; $type = $this->attachmentMIMEType; if ($type) { $json['links']['enclosure'] = [ 'type' => $type ]; } $json['links']['enclosure']['href'] = $details['url']; if (!empty($details['filename'])) { $json['links']['enclosure']['title'] = $details['filename']; } if (isset($details['size'])) { $json['links']['enclosure']['length'] = $details['size']; } } // 'meta' $json['meta'] = new stdClass; if (Zotero_Libraries::getType($this->libraryID) == 'group') { $createdByUserID = $this->createdByUserID; $lastModifiedByUserID = $this->lastModifiedByUserID; if ($createdByUserID) { $json['meta']->createdByUser = Zotero_Users::toJSON($createdByUserID); } if ($lastModifiedByUserID && $lastModifiedByUserID != $createdByUserID) { $json['meta']->lastModifiedByUser = Zotero_Users::toJSON($lastModifiedByUserID); } } if ($isRegularItem) { $val = $this->getCreatorSummary(); if ($val !== '') { $json['meta']->creatorSummary = $val; } $val = $this->getField('date', true, true, true); if ($val !== '') { $sqlDate = Zotero_Date::multipartToSQL($val); if (substr($sqlDate, 0, 4) !== '0000') { $json['meta']->parsedDate = Zotero_Date::sqlToISO8601($sqlDate); } } $json['meta']->numChildren = $numChildren; } // 'include' $include = $requestParams['include']; foreach ($include as $type) { if ($type == 'html') { $json[$type] = trim($this->toHTML()); } else if ($type == 'citation') { if (isset($sharedData[$type][$this->libraryID . "/" . $this->key])) { $html = $sharedData[$type][$this->libraryID . "/" . $this->key]; } else { if ($sharedData !== null) { //error_log("Citation not found in sharedData -- retrieving individually"); } $html = Zotero_Cite::getCitationFromCiteServer($this, $requestParams); } $json[$type] = $html; } else if ($type == 'bib') { if (isset($sharedData[$type][$this->libraryID . "/" . $this->key])) { $html = $sharedData[$type][$this->libraryID . "/" . $this->key]; } else { if ($sharedData !== null) { //error_log("Bibliography not found in sharedData -- retrieving individually"); } $html = Zotero_Cite::getBibliographyFromCitationServer([$this], $requestParams); // Strip prolog $html = preg_replace('/^<\?xml.+\n/', "", $html); $html = trim($html); } $json[$type] = $html; } else if ($type == 'data') { $json[$type] = $this->toJSON(true, $requestParams, true); } else if ($type == 'csljson') { $json[$type] = $this->toCSLItem(); } else if (in_array($type, Zotero_Translate::$exportFormats)) { $export = Zotero_Translate::doExport([$this], $type); $json[$type] = $export['body']; unset($export); } } // TEMP if ($cached) { $uncached = Zotero_Utilities::formatJSON($json); if ($cached != $uncached) { error_log("Cached JSON item entry does not match"); error_log(" Cached: " . $cached); error_log("Uncached: " . $uncached); //Z_Core::$MC->set($cacheKey, $uncached, 3600); // 1 hour for now } } else { /*Z_Core::$MC->set($cacheKey, $xmlstr, 3600); // 1 hour for now StatsD::timing("api.items.itemToAtom.uncached", (microtime(true) - $t) * 1000); StatsD::increment("memcached.items.itemToAtom.miss");*/ } return $json; }
public function toSolrDocument() { $doc = new SolrInputDocument(); $uri = Zotero_Solr::getItemURI($this->libraryID, $this->key); $doc->addField("uri", $uri); // Primary fields foreach (Zotero_Items::$primaryFields as $field) { switch ($field) { case 'itemID': case 'numAttachments': case 'numNotes': continue 2; case 'itemTypeID': $xmlField = 'itemType'; $xmlValue = Zotero_ItemTypes::getName($this->{$field}); break; case 'dateAdded': case 'dateModified': case 'serverDateModified': $xmlField = $field; $xmlValue = Zotero_Date::sqlToISO8601($this->{$field}); break; default: $xmlField = $field; $xmlValue = $this->{$field}; } $doc->addField($xmlField, $xmlValue); } // Title for sorting $title = $this->getDisplayTitle(true); $title = $title ? $title : ''; // Strip HTML from note titles if ($this->isNote()) { // Clean and strip HTML, giving us an HTML-encoded plaintext string $title = strip_tags($GLOBALS['HTMLPurifier']->purify($title)); // Unencode plaintext string $title = html_entity_decode($title); } // Strip some characters $sortTitle = preg_replace("/^[\\[\\'\"]*(.*)[\\]\\'\"]*\$/", "\$1", $title); if ($sortTitle) { $doc->addField('titleSort', $sortTitle); } // Item data $fieldIDs = $this->getUsedFields(); foreach ($fieldIDs as $fieldID) { $val = $this->getField($fieldID); if ($val == '') { continue; } $fieldName = Zotero_ItemFields::getName($fieldID); switch ($fieldName) { // As is case 'title': $val = $title; break; // Date fields // Date fields case 'date': // Add user part as text $doc->addField($fieldName . "_t", Zotero_Date::multipartToStr($val)); // Add as proper date, if there is one $sqlDate = Zotero_Date::multipartToSQL($val); if (!$sqlDate || $sqlDate == '0000-00-00') { continue 2; } $fieldName .= "_tdt"; $val = Zotero_Date::sqlToISO8601($sqlDate); break; case 'accessDate': if (!Zotero_Date::isSQLDateTime($val)) { continue 2; } $fieldName .= "_tdt"; $val = Zotero_Date::sqlToISO8601($val); break; default: $fieldName .= "_t"; } $doc->addField($fieldName, $val); } // Deleted item flag if ($this->getDeleted()) { $doc->addField('deleted', true); } if ($this->isNote() || $this->isAttachment()) { $sourceItemID = $this->getSource(); if ($sourceItemID) { $sourceItem = Zotero_Items::get($this->libraryID, $sourceItemID); if (!$sourceItem) { throw new Exception("Source item {$sourceItemID} not found"); } $doc->addField('sourceItem', $sourceItem->key); } } // Group modification info $createdByUserID = null; $lastModifiedByUserID = null; switch (Zotero_Libraries::getType($this->libraryID)) { case 'group': $createdByUserID = $this->createdByUserID; $lastModifiedByUserID = $this->lastModifiedByUserID; break; } if ($createdByUserID) { $doc->addField('createdByUserID', $createdByUserID); } if ($lastModifiedByUserID) { $doc->addField('lastModifiedByUserID', $lastModifiedByUserID); } // Note if ($this->isNote()) { $doc->addField('note', $this->getNote()); } if ($this->isAttachment()) { $doc->addField('linkMode', $this->attachmentLinkMode); $doc->addField('mimeType', $this->attachmentMIMEType); if ($this->attachmentCharset) { $doc->addField('charset', $this->attachmentCharset); } // TODO: get from a constant if ($this->attachmentLinkMode != 3) { $doc->addField('path', $this->attachmentPath); } $note = $this->getNote(); if ($note) { $doc->addField('note', $note); } } // Creators $creators = $this->getCreators(); if ($creators) { foreach ($creators as $index => $creator) { $c = $creator['ref']; $doc->addField('creatorKey', $c->key); if ($c->fieldMode == 0) { $doc->addField('creatorFirstName', $c->firstName); } $doc->addField('creatorLastName', $c->lastName); $doc->addField('creatorType', Zotero_CreatorTypes::getName($creator['creatorTypeID'])); $doc->addField('creatorIndex', $index); } } // Tags $tags = $this->getTags(); if ($tags) { foreach ($tags as $tag) { $doc->addField('tagKey', $tag->key); $doc->addField('tag', $tag->name); $doc->addField('tagType', $tag->type); } } // Related items /*$related = $this->relatedItems; if ($related) { $related = Zotero_Items::get($this->libraryID, $related); $keys = array(); foreach ($related as $item) { $doc->addField('relatedItem', $item->key); } }*/ return $doc; }
public static function notify($event, $type, $ids, $extraData) { if (!isset(Z_CONFIG::$SNS_TOPIC_STREAM_EVENTS)) { error_log('WARNING: Z_CONFIG::$SNS_TOPIC_STREAM_EVENTS not set ' . '-- skipping stream notifications'); return; } if ($type == "library") { switch ($event) { case "modify": $event = "topicUpdated"; break; case "delete": $event = "topicDeleted"; break; default: return; } $entries = []; foreach ($ids as $id) { $libraryID = $id; // For most libraries, get topic from URI if ($event != 'topicDeleted') { // Convert 'http://zotero.org/users/...' to '/users/...' $topic = str_replace(Zotero_URI::getBaseURI(), "/", Zotero_URI::getLibraryURI($libraryID)); } else { $topic = '/' . Zotero_Libraries::getType($libraryID) . "s/" . Zotero_Libraries::getLibraryTypeID($libraryID); } $message = ["event" => $event, "topic" => $topic]; if (!empty($extraData[$id])) { foreach ($extraData[$id] as $key => $val) { $message[$key] = $val; } } foreach (self::$messageReceivers as $receiver) { $receiver(Z_CONFIG::$SNS_TOPIC_STREAM_EVENTS, json_encode($message, JSON_UNESCAPED_SLASHES)); } } } else { if ($type == "apikey-library") { switch ($event) { case "add": $event = "topicAdded"; break; case "remove": $event = "topicRemoved"; break; default: return; } $entries = []; foreach ($ids as $id) { list($apiKey, $libraryID) = explode("-", $id); // Get topic from URI $topic = str_replace(Zotero_URI::getBaseURI(), "/", Zotero_URI::getLibraryURI($libraryID)); $message = ["event" => $event, "apiKey" => $apiKey, "topic" => $topic]; if (!empty($extraData[$id])) { foreach ($extraData[$id] as $key => $val) { $message[$key] = $val; } } self::send($message); } } } }
protected function getFeedNamePrefix($libraryID = false) { $prefix = "Zotero / "; if ($libraryID) { $type = Zotero_Libraries::getType($this->objectLibraryID); } else { $type = false; } switch ($type) { case "user": $title = $prefix . Zotero_Libraries::getName($this->objectLibraryID); break; case "group": $title = $prefix . "" . Zotero_Libraries::getName($this->objectLibraryID) . " Group"; break; default: return $prefix; } return $title . " / "; }
public static function toJSON($libraryID) { // TODO: cache $libraryType = Zotero_Libraries::getType($libraryID); if ($libraryType == 'user') { $objectUserID = Zotero_Users::getUserIDFromLibraryID($libraryID); $json = ['type' => $libraryType, 'id' => $objectUserID, 'name' => self::getName($libraryID), 'links' => ['alternate' => ['href' => Zotero_URI::getUserURI($objectUserID, true), 'type' => 'text/html']]]; } else { if ($libraryType == 'publications') { $objectUserID = Zotero_Users::getUserIDFromLibraryID($libraryID); $json = ['type' => $libraryType, 'id' => $objectUserID, 'name' => self::getName($libraryID), 'links' => ['alternate' => ['href' => Zotero_URI::getUserURI($objectUserID, true) . "/publications", 'type' => 'text/html']]]; } else { if ($libraryType == 'group') { $objectGroupID = Zotero_Groups::getGroupIDFromLibraryID($libraryID); $group = Zotero_Groups::get($objectGroupID); $json = ['type' => $libraryType, 'id' => $objectGroupID, 'name' => self::getName($libraryID), 'links' => ['alternate' => ['href' => Zotero_URI::getGroupURI($group, true), 'type' => 'text/html']]]; } else { throw new Exception("Invalid library type '{$libraryType}'"); } } } return $json; }
public static function isEditable($obj) { $type = static::field('object'); // Only enforce for sync controller for now if (empty($GLOBALS['controller']) || !$GLOBALS['controller'] instanceof SyncController) { return true; } // Make sure user has access privileges to delete $userID = $GLOBALS['controller']->userID; if (!$userID) { return true; } $objectLibraryID = $obj->libraryID; $libraryType = Zotero_Libraries::getType($objectLibraryID); switch ($libraryType) { case 'user': if (!empty($GLOBALS['controller']->userLibraryID)) { $userLibraryID = $GLOBALS['controller']->userLibraryID; } else { $userLibraryID = Zotero_Users::getLibraryIDFromUserID($userID); } if ($objectLibraryID != $userLibraryID) { return false; } return true; case 'group': $groupID = Zotero_Groups::getGroupIDFromLibraryID($objectLibraryID); $group = Zotero_Groups::get($groupID); if (!$group->hasUser($userID) || !$group->userCanEdit($userID)) { return false; } if ($type == 'item' && $obj->isImportedAttachment() && !$group->userCanEditFiles($userID)) { return false; } return true; default: throw new Exception("Unsupported library type '{$libraryType}'"); } }
/** * This should be called after canAccess() */ public function canWrite($libraryID) { if ($this->super) { return true; } if ($libraryID === 0) { return false; } if (!$libraryID) { throw new Exception('libraryID not provided'); } if (!empty($this->permissions[$libraryID]['write'])) { return true; } $libraryType = Zotero_Libraries::getType($libraryID); switch ($libraryType) { case 'user': return false; // Write permissions match key's write access to user library // Write permissions match key's write access to user library case 'publications': $userLibraryID = Zotero_Users::getLibraryIDFromUserID($this->userID); return $this->canWrite($userLibraryID); case 'group': $groupID = Zotero_Groups::getGroupIDFromLibraryID($libraryID); // If key has write access to all groups, grant access if user // has write access to group if (!empty($this->permissions[0]['write'])) { $group = Zotero_Groups::get($groupID); return $group->userCanEdit($this->userID); } return false; default: throw new Exception("Unsupported library type '{$libraryType}'"); } }
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); } }
private function handleUploadError(Exception $e, $xmldata) { $msg = $e->getMessage(); if ($msg[0] == '=') { $msg = substr($msg, 1); $explicit = true; // TODO: more specific error messages } else { $explicit = false; } switch ($e->getCode()) { case Z_ERROR_TAG_TOO_LONG: case Z_ERROR_COLLECTION_TOO_LONG: break; default: Z_Core::logError($msg); } if (!$explicit && Z_ENV_TESTING_SITE) { switch ($e->getCode()) { case Z_ERROR_COLLECTION_NOT_FOUND: case Z_ERROR_CREATOR_NOT_FOUND: case Z_ERROR_ITEM_NOT_FOUND: case Z_ERROR_TAG_TOO_LONG: case Z_ERROR_LIBRARY_ACCESS_DENIED: case Z_ERROR_TAG_LINKED_ITEM_NOT_FOUND: break; default: throw $e; } $id = 'N/A'; } else { $id = substr(md5(uniqid(rand(), true)), 0, 8); $str = date("D M j G:i:s T Y") . "\n"; $str .= "IP address: " . $_SERVER['REMOTE_ADDR'] . "\n"; if (isset($_SERVER['HTTP_X_ZOTERO_VERSION'])) { $str .= "Version: " . $_SERVER['HTTP_X_ZOTERO_VERSION'] . "\n"; } $str .= $msg; switch ($e->getCode()) { // Don't log uploaded data for some errors case Z_ERROR_TAG_TOO_LONG: case Z_ERROR_FIELD_TOO_LONG: case Z_ERROR_NOTE_TOO_LONG: case Z_ERROR_COLLECTION_TOO_LONG: break; default: $str .= "\n\n" . $xmldata; } if (!file_put_contents(Z_CONFIG::$SYNC_ERROR_PATH . $id, $str)) { error_log("Unable to save error report to " . Z_CONFIG::$SYNC_ERROR_PATH . $id); } } Zotero_DB::rollback(true); switch ($e->getCode()) { case Z_ERROR_LIBRARY_ACCESS_DENIED: preg_match('/[Ll]ibrary ([0-9]+)/', $e->getMessage(), $matches); $libraryID = $matches ? $matches[1] : null; $this->error(400, 'LIBRARY_ACCESS_DENIED', "Cannot make changes to library (Report ID: {$id})", array('libraryID' => $libraryID)); break; case Z_ERROR_ITEM_NOT_FOUND: case Z_ERROR_COLLECTION_NOT_FOUND: case Z_ERROR_CREATOR_NOT_FOUND: error_log($e); $this->error(500, "FULL_SYNC_REQUIRED", "Please perform a full sync in the Sync->Reset pane of the Zotero preferences. (Report ID: {$id})"); break; case Z_ERROR_TAG_TOO_LONG: $message = $e->getMessage(); preg_match("/Tag '(.+)' too long/s", $message, $matches); if ($matches) { $name = $matches[1]; $this->error(400, "TAG_TOO_LONG", "Tag '" . mb_substr($name, 0, 50) . "…' too long", array(), array("tag" => $name)); } break; case Z_ERROR_COLLECTION_TOO_LONG: $message = $e->getMessage(); preg_match("/Collection '(.+)' too long/s", $message, $matches); if ($matches) { $name = $matches[1]; $this->error(400, "COLLECTION_TOO_LONG", "Collection '" . mb_substr($name, 0, 50) . "…' too long", array(), array("collection" => $name)); } break; case Z_ERROR_NOTE_TOO_LONG: preg_match("/Note '(.+)' too long(?: for item '(.+)\\/(.+)')?/s", $msg, $matches); if ($matches) { $name = $matches[1]; $libraryID = false; if (isset($matches[2])) { $libraryID = (int) $matches[2]; $itemKey = $matches[3]; if (Zotero_Libraries::getType($libraryID) == 'group') { $groupID = Zotero_Groups::getGroupIDFromLibraryID($libraryID); $group = Zotero_Groups::get($groupID); $libraryName = $group->name; } else { $libraryName = false; } } else { $itemKey = ''; } $showNoteKey = false; if (isset($_SERVER['HTTP_X_ZOTERO_VERSION'])) { require_once '../model/ToolkitVersionComparator.inc.php'; $showNoteKey = ToolkitVersionComparator::compare($_SERVER['HTTP_X_ZOTERO_VERSION'], "4.0.27") < 0; } if ($showNoteKey) { $this->error(400, "ERROR_PROCESSING_UPLOAD_DATA", "The note '" . mb_substr($name, 0, 50) . "…' in " . ($libraryName === false ? "your library " : "the group '{$libraryName}' ") . "is too long to sync to zotero.org.\n\n" . "Search for the excerpt above or copy and paste " . "'{$itemKey}' into the Zotero search bar. " . "Shorten the note, or delete it and empty the Zotero " . "trash, and then try syncing again."); } else { $this->error(400, "NOTE_TOO_LONG", "The note '" . mb_substr($name, 0, 50) . "…' in " . ($libraryName === false ? "your library " : "the group '{$libraryName}' ") . "is too long to sync to zotero.org.\n\n" . "Shorten the note, or delete it and empty the Zotero " . "trash, and then try syncing again.", [], $libraryID ? ["item" => $libraryID . "/" . $itemKey] : []); } } break; case Z_ERROR_FIELD_TOO_LONG: preg_match("/(.+) field value '(.+)\\.\\.\\.' too long(?: for item '(.+)')?/s", $msg, $matches); if ($matches) { $fieldName = $matches[1]; $value = $matches[2]; if (isset($matches[3])) { $parts = explode("/", $matches[3]); $libraryID = (int) $parts[0]; $itemKey = $parts[1]; if (Zotero_Libraries::getType($libraryID) == 'group') { $groupID = Zotero_Groups::getGroupIDFromLibraryID($libraryID); $group = Zotero_Groups::get($groupID); $libraryName = "the group '" . $group->name . "'"; } else { $libraryName = "your personal library"; } } else { $libraryName = "one of your libraries"; $itemKey = false; } $this->error(400, "ERROR_PROCESSING_UPLOAD_DATA", "The {$fieldName} field value '{$value}…' in {$libraryName} is " . "too long to sync to zotero.org.\n\n" . "Search for the excerpt above " . ($itemKey === false ? "using " : "or copy and paste " . "'{$itemKey}' into ") . "the Zotero search bar. " . "Shorten the field, or delete the item and empty the " . "Zotero trash, and then try syncing again."); } break; case Z_ERROR_ARRAY_SIZE_MISMATCH: $this->error(400, 'DATABASE_TOO_LARGE', "Databases of this size cannot yet be synced. Please check back soon. (Report ID: {$id})"); break; case Z_ERROR_TAG_LINKED_ITEM_NOT_FOUND: $this->error(400, 'WRONG_LIBRARY_TAG_ITEM', "Error processing uploaded data (Report ID: {$id})"); break; case Z_ERROR_SHARD_READ_ONLY: case Z_ERROR_SHARD_UNAVAILABLE: $this->error(503, 'SERVER_ERROR', Z_CONFIG::$MAINTENANCE_MESSAGE); break; } if (strpos($msg, "Lock wait timeout exceeded; try restarting transaction") !== false || strpos($msg, "MySQL error: Deadlock found when trying to get lock; try restarting transaction") !== false) { $this->error(500, 'TIMEOUT', "Sync upload timed out. Please try again in a few minutes. (Report ID: {$id})"); } if (strpos($msg, "Data too long for column 'xmldata'") !== false) { $this->error(400, 'DATABASE_TOO_LARGE', "Databases of this size cannot yet be synced. Please check back soon. (Report ID: {$id})"); } // On certain messages, send 400 to prevent auto-retry if (strpos($msg, " too long") !== false || strpos($msg, "First and last name are empty") !== false) { $this->error(400, 'ERROR_PROCESSING_UPLOAD_DATA', $explicit ? $msg : "Error processing uploaded data (Report ID: {$id})"); } if (preg_match("/Incorrect datetime value: '([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2})' " . "for column 'date(Added|Modified)'/", $msg, $matches)) { if (isset($_SERVER['HTTP_X_ZOTERO_VERSION'])) { require_once '../model/ToolkitVersionComparator.inc.php'; if (ToolkitVersionComparator::compare($_SERVER['HTTP_X_ZOTERO_VERSION'], "2.1rc1") < 0) { $msg = "Invalid timestamp '{$matches[1]}' in uploaded data. Upgrade to Zotero 2.1rc1 when available to fix automatically."; } else { $msg = "Invalid timestamp '{$matches[1]}' in uploaded data. Sync again to correct automatically."; } } else { $msg = "Invalid timestamp '{$matches[1]}' in uploaded data. Upgrade to Zotero 2.1rc1 when available to fix automatically."; } $this->error(400, 'INVALID_TIMESTAMP', $msg); } $this->error(500, 'ERROR_PROCESSING_UPLOAD_DATA', $explicit ? $msg : "Error processing uploaded data (Report ID: {$id})"); }
/** * Handle S3 request * * Permission-checking provided by items() */ private function _handleFileRequest($item) { if (!$this->permissions->canAccess($this->objectLibraryID, 'files')) { $this->e403(); } $this->allowMethods(array('HEAD', 'GET', 'POST', 'PATCH')); if (!$item->isAttachment()) { $this->e400("Item is not an attachment"); } // File info for 4.0 client sync // // Use of HEAD method was discontinued after 2.0.8/2.1b1 due to // compatibility problems with proxies and security software if ($this->method == 'GET' && $this->fileMode == 'info') { $info = Zotero_Storage::getLocalFileItemInfo($item); if (!$info) { $this->e404(); } StatsD::increment("storage.info", 1); /* header("Last-Modified: " . gmdate('r', $info['uploaded'])); header("Content-Type: " . $info['type']); */ header("Content-Length: " . $info['size']); header("ETag: " . $info['hash']); header("X-Zotero-Filename: " . $info['filename']); header("X-Zotero-Modification-Time: " . $info['mtime']); header("X-Zotero-Compressed: " . ($info['zip'] ? 'Yes' : 'No')); header_remove("X-Powered-By"); $this->end(); } else { if ($this->method == 'GET') { $info = Zotero_Storage::getLocalFileItemInfo($item); if (!$info) { $this->e404(); } // File viewing if ($this->fileView) { $url = Zotero_Attachments::getTemporaryURL($item, !empty($_GET['int'])); if (!$url) { $this->e500(); } StatsD::increment("storage.view", 1); $this->redirect($url); exit; } // File download $url = Zotero_Storage::getDownloadURL($item, 60); if (!$url) { $this->e404(); } // Provide some headers to let 5.0 client skip download header("Zotero-File-Modification-Time: {$info['mtime']}"); header("Zotero-File-MD5: {$info['hash']}"); header("Zotero-File-Size: {$info['size']}"); header("Zotero-File-Compressed: " . ($info['zip'] ? 'Yes' : 'No')); StatsD::increment("storage.download", 1); Zotero_Storage::logDownload($item, $this->userID, IPAddress::getIP()); $this->redirect($url); exit; } else { if ($this->method == 'POST' || $this->method == 'PATCH') { if (!$item->isImportedAttachment()) { $this->e400("Cannot upload file for linked file/URL attachment item"); } $libraryID = $item->libraryID; $type = Zotero_Libraries::getType($libraryID); if ($type == 'group') { $groupID = Zotero_Groups::getGroupIDFromLibraryID($libraryID); $group = Zotero_Groups::get($groupID); if (!$group->userCanEditFiles($this->userID)) { $this->e403("You do not have file editing access"); } } else { $group = null; } // If not the client, require If-Match or If-None-Match if (!$this->httpAuth) { if (empty($_SERVER['HTTP_IF_MATCH']) && empty($_SERVER['HTTP_IF_NONE_MATCH'])) { $this->e428("If-Match/If-None-Match header not provided"); } if (!empty($_SERVER['HTTP_IF_MATCH'])) { if (!preg_match('/^"?([a-f0-9]{32})"?$/', $_SERVER['HTTP_IF_MATCH'], $matches)) { $this->e400("Invalid ETag in If-Match header"); } if (!$item->attachmentStorageHash) { $this->e412("ETag set but file does not exist"); } if ($item->attachmentStorageHash != $matches[1]) { $this->libraryVersion = $item->version; $this->libraryVersionOnFailure = true; $this->e412("ETag does not match current version of file"); } } else { if ($_SERVER['HTTP_IF_NONE_MATCH'] != "*") { $this->e400("Invalid value for If-None-Match header"); } if (Zotero_Storage::getLocalFileItemInfo($item)) { $this->libraryVersion = $item->version; $this->libraryVersionOnFailure = true; $this->e412("If-None-Match: * set but file exists"); } } } // // Upload authorization // if (!isset($_POST['update']) && !isset($_REQUEST['upload'])) { $info = new Zotero_StorageFileInfo(); // Validate upload metadata if (empty($_REQUEST['md5'])) { $this->e400('MD5 hash not provided'); } if (!preg_match('/[abcdefg0-9]{32}/', $_REQUEST['md5'])) { $this->e400('Invalid MD5 hash'); } if (!isset($_REQUEST['filename']) || $_REQUEST['filename'] === "") { $this->e400('Filename not provided'); } // Multi-file upload // // For ZIP files, the filename and hash of the ZIP file are different from those // of the main file. We use the former for S3, and we store the latter in the // upload log to set the attachment metadata with them on file registration. if (!empty($_REQUEST['zipMD5'])) { if (!preg_match('/[abcdefg0-9]{32}/', $_REQUEST['zipMD5'])) { $this->e400('Invalid ZIP MD5 hash'); } if (empty($_REQUEST['zipFilename'])) { $this->e400('ZIP filename not provided'); } $info->zip = true; $info->hash = $_REQUEST['zipMD5']; $info->filename = $_REQUEST['zipFilename']; $info->itemFilename = $_REQUEST['filename']; $info->itemHash = $_REQUEST['md5']; } else { if (!empty($_REQUEST['zipFilename'])) { $this->e400('ZIP MD5 hash not provided'); } else { $info->zip = !empty($_REQUEST['zip']); $info->filename = $_REQUEST['filename']; $info->hash = $_REQUEST['md5']; } } if (empty($_REQUEST['mtime'])) { $this->e400('File modification time not provided'); } $info->mtime = $_REQUEST['mtime']; if (!isset($_REQUEST['filesize'])) { $this->e400('File size not provided'); } $info->size = $_REQUEST['filesize']; if (!is_numeric($info->size)) { $this->e400("Invalid file size"); } $info->contentType = isset($_REQUEST['contentType']) ? $_REQUEST['contentType'] : null; if (!preg_match("/^[a-zA-Z0-9\\-\\/]+\$/", $info->contentType)) { $info->contentType = null; } $info->charset = isset($_REQUEST['charset']) ? $_REQUEST['charset'] : null; if (!preg_match("/^[a-zA-Z0-9\\-]+\$/", $info->charset)) { $info->charset = null; } $contentTypeHeader = $info->contentType . ($info->contentType && $info->charset ? "; charset=" . $info->charset : ""); // Reject file if it would put account over quota if ($group) { $quota = Zotero_Storage::getEffectiveUserQuota($group->ownerUserID); $usage = Zotero_Storage::getUserUsage($group->ownerUserID); } else { $quota = Zotero_Storage::getEffectiveUserQuota($this->objectUserID); $usage = Zotero_Storage::getUserUsage($this->objectUserID); } $total = $usage['total']; $fileSizeMB = round($info->size / 1024 / 1024, 1); if ($total + $fileSizeMB > $quota) { StatsD::increment("storage.upload.quota", 1); $this->e413("File would exceed quota ({$total} + {$fileSizeMB} > {$quota})"); } Zotero_DB::query("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE"); Zotero_DB::beginTransaction(); // See if file exists with this filename $localInfo = Zotero_Storage::getLocalFileInfo($info); if ($localInfo) { $storageFileID = $localInfo['storageFileID']; // Verify file size if ($localInfo['size'] != $info->size) { throw new Exception("Specified file size incorrect for existing file " . $info->hash . "/" . $info->filename . " ({$localInfo['size']} != {$info->size})"); } } else { $oldStorageFileID = Zotero_Storage::getFileByHash($info->hash, $info->zip); if ($oldStorageFileID) { // Verify file size $localInfo = Zotero_Storage::getFileInfoByID($oldStorageFileID); if ($localInfo['size'] != $info->size) { throw new Exception("Specified file size incorrect for duplicated file " . $info->hash . "/" . $info->filename . " ({$localInfo['size']} != {$info->size})"); } // Create new file on S3 with new name $storageFileID = Zotero_Storage::duplicateFile($oldStorageFileID, $info->filename, $info->zip, $contentTypeHeader); } } // If we already have a file, add/update storageFileItems row and stop if (!empty($storageFileID)) { Zotero_Storage::updateFileItemInfo($item, $storageFileID, $info, $this->httpAuth); Zotero_DB::commit(); StatsD::increment("storage.upload.existing", 1); if ($this->httpAuth) { $this->queryParams['format'] = null; header('Content-Type: application/xml'); echo "<exists/>"; } else { $this->queryParams['format'] = null; header('Content-Type: application/json'); $this->libraryVersion = $item->version; echo json_encode(array('exists' => 1)); } $this->end(); } Zotero_DB::commit(); // Add request to upload queue $uploadKey = Zotero_Storage::queueUpload($this->userID, $info); // User over queue limit if (!$uploadKey) { header('Retry-After: ' . Zotero_Storage::$uploadQueueTimeout); if ($this->httpAuth) { $this->e413("Too many queued uploads"); } else { $this->e429("Too many queued uploads"); } } StatsD::increment("storage.upload.new", 1); // Output XML for client requests (which use HTTP Auth) if ($this->httpAuth) { $params = Zotero_Storage::generateUploadPOSTParams($item, $info, true); $this->queryParams['format'] = null; header('Content-Type: application/xml'); $xml = new SimpleXMLElement('<upload/>'); $xml->url = Zotero_Storage::getUploadBaseURL(); $xml->key = $uploadKey; foreach ($params as $key => $val) { $xml->params->{$key} = $val; } echo $xml->asXML(); } else { if (!empty($_REQUEST['params']) && $_REQUEST['params'] == "1") { $params = array("url" => Zotero_Storage::getUploadBaseURL(), "params" => array()); foreach (Zotero_Storage::generateUploadPOSTParams($item, $info) as $key => $val) { $params['params'][$key] = $val; } } else { $params = Zotero_Storage::getUploadPOSTData($item, $info); } $params['uploadKey'] = $uploadKey; $this->queryParams['format'] = null; header('Content-Type: application/json'); echo json_encode($params); } exit; } // // API partial upload and post-upload file registration // if (isset($_REQUEST['upload'])) { $uploadKey = $_REQUEST['upload']; if (!$uploadKey) { $this->e400("Upload key not provided"); } $info = Zotero_Storage::getUploadInfo($uploadKey); if (!$info) { $this->e400("Upload key not found"); } // Partial upload if ($this->method == 'PATCH') { if (empty($_REQUEST['algorithm'])) { throw new Exception("Algorithm not specified", Z_ERROR_INVALID_INPUT); } $storageFileID = Zotero_Storage::patchFile($item, $info, $_REQUEST['algorithm'], $this->body); } else { $remoteInfo = Zotero_Storage::getRemoteFileInfo($info); if (!$remoteInfo) { error_log("Remote file {$info->hash}/{$info->filename} not found"); $this->e400("Remote file not found"); } if ($remoteInfo->size != $info->size) { error_log("Uploaded file size does not match " . "({$remoteInfo->size} != {$info->size}) " . "for file {$info->hash}/{$info->filename}"); } } // Set an automatic shared lock in getLocalFileInfo() to prevent // two simultaneous transactions from adding a file Zotero_DB::query("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE"); Zotero_DB::beginTransaction(); if (!isset($storageFileID)) { // Check if file already exists, which can happen if two identical // files are uploaded simultaneously $fileInfo = Zotero_Storage::getLocalFileInfo($info); if ($fileInfo) { $storageFileID = $fileInfo['storageFileID']; } else { $storageFileID = Zotero_Storage::addFile($info); } } Zotero_Storage::updateFileItemInfo($item, $storageFileID, $info); Zotero_Storage::logUpload($this->userID, $item, $uploadKey, IPAddress::getIP()); Zotero_DB::commit(); header("HTTP/1.1 204 No Content"); header("Last-Modified-Version: " . $item->version); exit; } // // Client post-upload file registration // if (isset($_POST['update'])) { $this->allowMethods(array('POST')); if (empty($_POST['mtime'])) { throw new Exception('File modification time not provided'); } $uploadKey = $_POST['update']; $info = Zotero_Storage::getUploadInfo($uploadKey); if (!$info) { $this->e400("Upload key not found"); } $remoteInfo = Zotero_Storage::getRemoteFileInfo($info); if (!$remoteInfo) { $this->e400("Remote file not found"); } if (!isset($info->size)) { throw new Exception("Size information not available"); } $info->mtime = $_POST['mtime']; // Set an automatic shared lock in getLocalFileInfo() to prevent // two simultaneous transactions from adding a file Zotero_DB::query("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE"); Zotero_DB::beginTransaction(); // Check if file already exists, which can happen if two identical // files are uploaded simultaneously $fileInfo = Zotero_Storage::getLocalFileInfo($info); if ($fileInfo) { $storageFileID = $fileInfo['storageFileID']; } else { $storageFileID = Zotero_Storage::addFile($info); } Zotero_Storage::updateFileItemInfo($item, $storageFileID, $info, true); Zotero_Storage::logUpload($this->userID, $item, $uploadKey, IPAddress::getIP()); Zotero_DB::commit(); header("HTTP/1.1 204 No Content"); exit; } throw new Exception("Invalid request", Z_ERROR_INVALID_INPUT); } } } exit; }
public static function getUserIDFromLibraryID($libraryID, $libraryType = null) { if (isset(self::$libraryUserIDs[$libraryID])) { return self::$libraryUserIDs[$libraryID]; } $cacheKey = 'libraryUserID_' . $libraryID; $userID = Z_Core::$MC->get($cacheKey); if ($userID) { self::$libraryUserIDs[$libraryID] = $userID; return $userID; } if ($libraryType == null) { $libraryType = Zotero_Libraries::getType($libraryID); } switch ($libraryType) { case 'user': $sql = "SELECT userID FROM users WHERE libraryID=?"; break; case 'publications': $sql = "SELECT userID FROM userPublications WHERE libraryID=?"; break; case 'group': $sql = "SELECT userID FROM groupUsers JOIN groups USING (groupID) " . "WHERE libraryID=? AND role='owner'"; break; } $userID = Zotero_DB::valueQuery($sql, $libraryID); if (!$userID) { if (!Zotero_Libraries::exists($libraryID)) { throw new Exception("Library {$libraryID} does not exist"); } // Wrong library type passed error_log("Wrong library type passed for library {$libraryID} " . "in Zotero_Users::getUserIDFromLibraryID()"); return self::getUserIDFromLibraryID($libraryID); } self::$libraryUserIDs[$libraryID] = $userID; Z_Core::$MC->set($cacheKey, $userID); return $userID; }
public static function getLibraryURI($libraryID) { $libraryType = Zotero_Libraries::getType($libraryID); switch ($libraryType) { case 'user': $id = Zotero_Users::getUserIDFromLibraryID($libraryID); return self::getBaseURI() . "users/{$id}"; case 'group': $id = Zotero_Groups::getGroupIDFromLibraryID($libraryID); return self::getBaseURI() . "groups/{$id}"; } }
private function load() { if ($this->id) { $sql = "SELECT * FROM `keys` WHERE keyID=?"; $row = Zotero_DB::rowQuery($sql, $this->id); } else { if ($this->key) { $sql = "SELECT * FROM `keys` WHERE `key`=?"; $row = Zotero_DB::rowQuery($sql, $this->key); } } if (!$row) { return false; } $this->loadFromRow($row); $sql = "SELECT * FROM keyPermissions WHERE keyID=?"; $rows = Zotero_DB::query($sql, $this->id); foreach ($rows as $row) { $this->permissions[$row['libraryID']][$row['permission']] = !!$row['granted']; if ($row['permission'] == 'library') { // Key-based access to library provides file access as well $this->permissions[$row['libraryID']]['files'] = !!$row['granted']; // Key-based access to group libraries provides note access as well if ($row['libraryID'] === 0 || Zotero_Libraries::getType($row['libraryID']) == 'group') { $this->permissions[$row['libraryID']]['notes'] = !!$row['granted']; } } } }
public static function getLibraryURI($libraryID) { $libraryType = Zotero_Libraries::getType($libraryID); switch ($libraryType) { case 'user': $id = Zotero_Users::getUserIDFromLibraryID($libraryID); return self::getBaseURI() . "users/{$id}"; case 'publications': $id = Zotero_Users::getUserIDFromLibraryID($libraryID); return self::getBaseURI() . "users/{$id}/publications"; case 'group': $id = Zotero_Groups::getGroupIDFromLibraryID($libraryID); return self::getBaseURI() . "groups/{$id}"; default: throw new Exception("Invalid library type '{$libraryType}'"); } }
/** * Check if search exists in the database * * @return bool TRUE if the relation exists, FALSE if not */ public function exists() { $shardID = Zotero_Shards::getByLibraryID($this->libraryID); if ($this->id) { $sql = "SELECT COUNT(*) FROM relations WHERE relationID=?"; return !!Zotero_DB::valueQuery($sql, $this->id, $shardID); } if ($this->subject && $this->predicate && $this->object) { $sql = "SELECT COUNT(*) FROM relations WHERE libraryID=? AND `key`=?"; $params = array($this->libraryID, $this->getKey()); $exists = !!Zotero_DB::valueQuery($sql, $params, $shardID); // TEMP // For linked items, check reverse order too, since client can save in reverse // order when an item is dragged from a group to a personal library if (!$exists && $this->predicate == Zotero_Relations::$linkedObjectPredicate && Zotero_Libraries::getType($this->libraryID) == 'user') { $sql = "SELECT COUNT(*) FROM relations WHERE libraryID=? AND `key`=?"; $params = [$this->libraryID, Zotero_Relations::makeKey($this->object, $this->predicate, $this->subject)]; return !!Zotero_DB::valueQuery($sql, $params, $shardID); } return $exists; } throw new Exception("ID or subject/predicate/object not set"); }