Example #1
0
 public function testConvert()
 {
     $original = "Abcdefg Âéìøü 这是一个测试。";
     $str = Zotero_Attachments::encodeRelativeDescriptorString($original);
     // assert
     $str = Zotero_Attachments::decodeRelativeDescriptorString($str);
     $this->assertEquals($original, $str);
 }
Example #2
0
 private static function validateJSONItem($json, $libraryID, Zotero_Item $item = null, $isChild, $requestParams, $partialUpdate = false)
 {
     $isNew = !$item || !$item->version;
     if (!is_object($json)) {
         throw new Exception("Invalid item object (found " . gettype($json) . " '" . $json . "')", Z_ERROR_INVALID_INPUT);
     }
     if (isset($json->items) && is_array($json->items)) {
         throw new Exception("An 'items' array is not valid for single-item updates", Z_ERROR_INVALID_INPUT);
     }
     $apiVersion = $requestParams['v'];
     if ($partialUpdate) {
         $requiredProps = [];
     } else {
         if (isset($json->itemType) && $json->itemType == "attachment") {
             $requiredProps = array('linkMode', 'tags');
         } else {
             if (isset($json->itemType) && $json->itemType == "attachment") {
                 $requiredProps = array('tags');
             } else {
                 if ($isNew) {
                     $requiredProps = array('itemType');
                 } else {
                     if ($apiVersion < 2) {
                         $requiredProps = array('itemType', 'tags');
                     } else {
                         $requiredProps = array('itemType', 'tags', 'relations');
                         if (!$isChild) {
                             $requiredProps[] = 'collections';
                         }
                     }
                 }
             }
         }
     }
     foreach ($requiredProps as $prop) {
         if (!isset($json->{$prop})) {
             throw new Exception("'{$prop}' property not provided", Z_ERROR_INVALID_INPUT);
         }
     }
     // For partial updates where item type isn't provided, use the existing item type
     if (!isset($json->itemType) && $partialUpdate) {
         $itemType = Zotero_ItemTypes::getName($item->itemTypeID);
     } else {
         $itemType = $json->itemType;
     }
     foreach ($json as $key => $val) {
         switch ($key) {
             // Handled by Zotero_API::checkJSONObjectVersion()
             case 'key':
             case 'version':
                 if ($apiVersion < 3) {
                     throw new Exception("Invalid property '{$key}'", Z_ERROR_INVALID_INPUT);
                 }
                 break;
             case 'itemKey':
             case 'itemVersion':
                 if ($apiVersion != 2) {
                     throw new Exception("Invalid property '{$key}'", Z_ERROR_INVALID_INPUT);
                 }
                 break;
             case 'parentItem':
                 if ($apiVersion < 2) {
                     throw new Exception("Invalid property '{$key}'", Z_ERROR_INVALID_INPUT);
                 }
                 if (!Zotero_ID::isValidKey($val) && $val !== false) {
                     throw new Exception("'{$key}' must be a valid item key", Z_ERROR_INVALID_INPUT);
                 }
                 break;
             case 'itemType':
                 if (!is_string($val)) {
                     throw new Exception("'itemType' must be a string", Z_ERROR_INVALID_INPUT);
                 }
                 if ($isChild || !empty($json->parentItem)) {
                     switch ($val) {
                         case 'note':
                         case 'attachment':
                             break;
                         default:
                             throw new Exception("Child item must be note or attachment", Z_ERROR_INVALID_INPUT);
                     }
                 } else {
                     if ($val == 'attachment' && (!$item || !$item->getSource())) {
                         if ($json->linkMode == 'linked_url' || $json->linkMode == 'imported_url' && (empty($json->contentType) || $json->contentType != 'application/pdf')) {
                             throw new Exception("Only file attachments and PDFs can be top-level items", Z_ERROR_INVALID_INPUT);
                         }
                     }
                 }
                 if (!Zotero_ItemTypes::getID($val)) {
                     throw new Exception("'{$val}' is not a valid itemType", Z_ERROR_INVALID_INPUT);
                 }
                 break;
             case 'tags':
                 if (!is_array($val)) {
                     throw new Exception("'{$key}' property must be an array", Z_ERROR_INVALID_INPUT);
                 }
                 foreach ($val as $tag) {
                     $empty = true;
                     if (is_string($tag)) {
                         if ($tag === "") {
                             throw new Exception("Tag cannot be empty", Z_ERROR_INVALID_INPUT);
                         }
                         continue;
                     }
                     if (!is_object($tag)) {
                         throw new Exception("Tag must be an object", Z_ERROR_INVALID_INPUT);
                     }
                     foreach ($tag as $k => $v) {
                         switch ($k) {
                             case 'tag':
                                 if (!is_scalar($v)) {
                                     throw new Exception("Invalid tag name", Z_ERROR_INVALID_INPUT);
                                 }
                                 if ($v === "") {
                                     throw new Exception("Tag cannot be empty", Z_ERROR_INVALID_INPUT);
                                 }
                                 break;
                             case 'type':
                                 if (!is_numeric($v)) {
                                     throw new Exception("Invalid tag type '{$v}'", Z_ERROR_INVALID_INPUT);
                                 }
                                 break;
                             default:
                                 throw new Exception("Invalid tag property '{$k}'", Z_ERROR_INVALID_INPUT);
                         }
                         $empty = false;
                     }
                     if ($empty) {
                         throw new Exception("Tag object is empty", Z_ERROR_INVALID_INPUT);
                     }
                 }
                 break;
             case 'collections':
                 if (!is_array($val)) {
                     throw new Exception("'{$key}' property must be an array", Z_ERROR_INVALID_INPUT);
                 }
                 if ($isChild && $val) {
                     throw new Exception("Child items cannot be assigned to collections", Z_ERROR_INVALID_INPUT);
                 }
                 foreach ($val as $k) {
                     if (!Zotero_ID::isValidKey($k)) {
                         throw new Exception("'{$k}' is not a valid collection key", Z_ERROR_INVALID_INPUT);
                     }
                 }
                 break;
             case 'relations':
                 if ($apiVersion < 2) {
                     throw new Exception("Invalid property '{$key}'", Z_ERROR_INVALID_INPUT);
                 }
                 if (!is_object($val) && !(is_array($val) && empty($val))) {
                     throw new Exception("'{$key}' property must be an object", Z_ERROR_INVALID_INPUT);
                 }
                 foreach ($val as $predicate => $object) {
                     switch ($predicate) {
                         case 'owl:sameAs':
                         case 'dc:replaces':
                         case 'dc:relation':
                             break;
                         default:
                             throw new Exception("Unsupported predicate '{$predicate}'", Z_ERROR_INVALID_INPUT);
                     }
                     $arr = is_string($object) ? [$object] : $object;
                     foreach ($arr as $uri) {
                         if (!preg_match('/^http:\\/\\/zotero.org\\/(users|groups)\\/[0-9]+\\/(publications\\/)?items\\/[A-Z0-9]{8}$/', $uri)) {
                             throw new Exception("'{$key}' values currently must be Zotero item URIs", Z_ERROR_INVALID_INPUT);
                         }
                     }
                 }
                 break;
             case 'creators':
                 if (!is_array($val)) {
                     throw new Exception("'{$key}' property must be an array", Z_ERROR_INVALID_INPUT);
                 }
                 foreach ($val as $creator) {
                     $empty = true;
                     if (!isset($creator->creatorType)) {
                         throw new Exception("creator object must contain 'creatorType'", Z_ERROR_INVALID_INPUT);
                     }
                     if ((!isset($creator->name) || trim($creator->name) == "") && (!isset($creator->firstName) || trim($creator->firstName) == "") && (!isset($creator->lastName) || trim($creator->lastName) == "")) {
                         // On item creation, ignore single nameless creator,
                         // because that's in the item template that the API returns
                         if (sizeOf($val) == 1 && $isNew) {
                             continue;
                         } else {
                             throw new Exception("creator object must contain 'firstName'/'lastName' or 'name'", Z_ERROR_INVALID_INPUT);
                         }
                     }
                     foreach ($creator as $k => $v) {
                         switch ($k) {
                             case 'creatorType':
                                 $creatorTypeID = Zotero_CreatorTypes::getID($v);
                                 if (!$creatorTypeID) {
                                     throw new Exception("'{$v}' is not a valid creator type", Z_ERROR_INVALID_INPUT);
                                 }
                                 $itemTypeID = Zotero_ItemTypes::getID($itemType);
                                 if (!Zotero_CreatorTypes::isValidForItemType($creatorTypeID, $itemTypeID)) {
                                     // Allow 'author' in all item types, but reject other invalid creator types
                                     if ($creatorTypeID != Zotero_CreatorTypes::getID('author')) {
                                         throw new Exception("'{$v}' is not a valid creator type for item type '{$itemType}'", Z_ERROR_INVALID_INPUT);
                                     }
                                 }
                                 break;
                             case 'firstName':
                                 if (!isset($creator->lastName)) {
                                     throw new Exception("'lastName' creator field must be set if 'firstName' is set", Z_ERROR_INVALID_INPUT);
                                 }
                                 if (isset($creator->name)) {
                                     throw new Exception("'firstName' and 'name' creator fields are mutually exclusive", Z_ERROR_INVALID_INPUT);
                                 }
                                 break;
                             case 'lastName':
                                 if (!isset($creator->firstName)) {
                                     throw new Exception("'firstName' creator field must be set if 'lastName' is set", Z_ERROR_INVALID_INPUT);
                                 }
                                 if (isset($creator->name)) {
                                     throw new Exception("'lastName' and 'name' creator fields are mutually exclusive", Z_ERROR_INVALID_INPUT);
                                 }
                                 break;
                             case 'name':
                                 if (isset($creator->firstName)) {
                                     throw new Exception("'firstName' and 'name' creator fields are mutually exclusive", Z_ERROR_INVALID_INPUT);
                                 }
                                 if (isset($creator->lastName)) {
                                     throw new Exception("'lastName' and 'name' creator fields are mutually exclusive", Z_ERROR_INVALID_INPUT);
                                 }
                                 break;
                             default:
                                 throw new Exception("Invalid creator property '{$k}'", Z_ERROR_INVALID_INPUT);
                         }
                         $empty = false;
                     }
                     if ($empty) {
                         throw new Exception("Creator object is empty", Z_ERROR_INVALID_INPUT);
                     }
                 }
                 break;
             case 'note':
                 switch ($itemType) {
                     case 'note':
                     case 'attachment':
                         break;
                     default:
                         throw new Exception("'note' property is valid only for note and attachment items", Z_ERROR_INVALID_INPUT);
                 }
                 break;
             case 'attachments':
             case 'notes':
                 if ($apiVersion > 1) {
                     throw new Exception("'{$key}' property is no longer supported", Z_ERROR_INVALID_INPUT);
                 }
                 if (!$isNew) {
                     throw new Exception("'{$key}' property is valid only for new items", Z_ERROR_INVALID_INPUT);
                 }
                 if (!is_array($val)) {
                     throw new Exception("'{$key}' property must be an array", Z_ERROR_INVALID_INPUT);
                 }
                 foreach ($val as $child) {
                     // Check child item type ('attachment' or 'note')
                     $t = substr($key, 0, -1);
                     if (isset($child->itemType) && $child->itemType != $t) {
                         throw new Exception("Child {$t} must be of itemType '{$t}'", Z_ERROR_INVALID_INPUT);
                     }
                     if ($key == 'note') {
                         if (!isset($child->note)) {
                             throw new Exception("'note' property not provided for child note", Z_ERROR_INVALID_INPUT);
                         }
                     }
                 }
                 break;
             case 'deleted':
                 break;
                 // Attachment properties
             // Attachment properties
             case 'linkMode':
                 try {
                     $linkMode = Zotero_Attachments::linkModeNameToNumber($val, true);
                 } catch (Exception $e) {
                     throw new Exception("'{$val}' is not a valid linkMode", Z_ERROR_INVALID_INPUT);
                 }
                 // Don't allow changing of linkMode
                 if (!$isNew && $linkMode != $item->attachmentLinkMode) {
                     throw new Exception("Cannot change attachment linkMode", Z_ERROR_INVALID_INPUT);
                 }
                 break;
             case 'contentType':
             case 'charset':
             case 'filename':
             case 'md5':
             case 'mtime':
                 if ($itemType != 'attachment') {
                     throw new Exception("'{$key}' is valid only for attachment items", Z_ERROR_INVALID_INPUT);
                 }
                 switch ($key) {
                     case 'filename':
                     case 'md5':
                     case 'mtime':
                         $lm = isset($json->linkMode) ? $json->linkMode : Zotero_Attachments::linkModeNumberToName($item->attachmentLinkMode);
                         if (strpos(strtolower($lm), 'imported_') !== 0) {
                             throw new Exception("'{$key}' is valid only for imported attachment items", Z_ERROR_INVALID_INPUT);
                         }
                         break;
                 }
                 switch ($key) {
                     case 'contentType':
                     case 'charset':
                     case 'filename':
                         $propName = 'attachment' . ucwords($key);
                         break;
                     case 'md5':
                         $propName = 'attachmentStorageHash';
                         break;
                     case 'mtime':
                         $propName = 'attachmentStorageModTime';
                         break;
                 }
                 if (Zotero_Libraries::getType($libraryID) == 'group') {
                     if ($item && $item->{$propName} !== $val || !$item && $val !== null && $val !== "") {
                         throw new Exception("Cannot change '{$key}' directly in group library", Z_ERROR_INVALID_INPUT);
                     }
                 } else {
                     if ($key == 'md5') {
                         if ($val && !preg_match("/^[a-f0-9]{32}\$/", $val)) {
                             throw new Exception("'{$val}' is not a valid MD5 hash", Z_ERROR_INVALID_INPUT);
                         }
                     }
                 }
                 break;
             case 'accessDate':
                 if ($apiVersion >= 3 && $val !== '' && $val != 'CURRENT_TIMESTAMP' && !Zotero_Date::isSQLDate($val) && !Zotero_Date::isSQLDateTime($val) && !Zotero_Date::isISO8601($val)) {
                     throw new Exception("'{$key}' must be in ISO 8601 or UTC 'YYYY-MM-DD[ hh-mm-dd]' format or 'CURRENT_TIMESTAMP' ({$val})", Z_ERROR_INVALID_INPUT);
                 }
                 break;
             case 'dateAdded':
                 if (!Zotero_Date::isSQLDateTime($val) && !Zotero_Date::isISO8601($val)) {
                     throw new Exception("'{$key}' must be in ISO 8601 or UTC 'YYYY-MM-DD hh-mm-dd' format", Z_ERROR_INVALID_INPUT);
                 }
                 if (!$isNew) {
                     // Convert ISO date to SQL date for equality comparison
                     if (Zotero_Date::isISO8601($val)) {
                         $val = Zotero_Date::iso8601ToSQL($val);
                     }
                     if ($val != $item->{$key}) {
                         throw new Exception("'{$key}' cannot be modified for existing items", Z_ERROR_INVALID_INPUT);
                     }
                 }
                 break;
             case 'dateModified':
                 if (!Zotero_Date::isSQLDateTime($val) && !Zotero_Date::isISO8601($val)) {
                     throw new Exception("'{$key}' must be in ISO 8601 or UTC 'YYYY-MM-DD hh-mm-dd' format ({$val})", Z_ERROR_INVALID_INPUT);
                 }
                 break;
             default:
                 if (!Zotero_ItemFields::getID($key)) {
                     throw new Exception("Invalid property '{$key}'", Z_ERROR_INVALID_INPUT);
                 }
                 if (is_array($val)) {
                     throw new Exception("Unexpected array for property '{$key}'", Z_ERROR_INVALID_INPUT);
                 }
                 break;
         }
     }
     // Publications libraries have additional restrictions
     if (Zotero_Libraries::getType($libraryID) == 'publications') {
         Zotero_Publications::validateJSONItem($json);
     }
 }
Example #3
0
	public function toJSON($asArray=false, $requestParams=array(), $includeEmpty=false, $unformattedFields=false) {
		if ($this->id || $this->key) {
			if (!$this->loaded['primaryData']) {
				$this->loadPrimaryData();
			}
			if (!$this->loaded['itemData']) {
				$this->loadItemData();
			}
		}
		
		if (!isset($requestParams['v'])) {
			$requestParams['v'] = 3;
		}
		
		$regularItem = $this->isRegularItem();
		
		$arr = array();
		if ($requestParams['v'] >= 2) {
			if ($requestParams['v'] >= 3) {
				$arr['key'] = $this->key;
				$arr['version'] = $this->version;
			}
			else {
				$arr['itemKey'] = $this->key;
				$arr['itemVersion'] = $this->version;
			}
			
			$key = $this->getSourceKey();
			if ($key) {
				$arr['parentItem'] = $key;
			}
		}
		$arr['itemType'] = Zotero_ItemTypes::getName($this->itemTypeID);
		
		if ($this->isAttachment()) {
			$val = $this->attachmentLinkMode;
			$arr['linkMode'] = strtolower(Zotero_Attachments::linkModeNumberToName($val));
		}
		
		// For regular items, show title and creators first
		if ($regularItem) {
			// Get 'title' or the equivalent base-mapped field
			$titleFieldID = Zotero_ItemFields::getFieldIDFromTypeAndBase($this->itemTypeID, 'title');
			$titleFieldName = Zotero_ItemFields::getName($titleFieldID);
			if ($includeEmpty || $this->itemData[$titleFieldID] !== false) {
				$arr[$titleFieldName] = $this->itemData[$titleFieldID] !== false ? $this->itemData[$titleFieldID] : "";
			}
			
			// Creators
			$arr['creators'] = array();
			$creators = $this->getCreators();
			foreach ($creators as $creator) {
				$c = array();
				$c['creatorType'] = Zotero_CreatorTypes::getName($creator['creatorTypeID']);
				
				// Single-field mode
				if ($creator['ref']->fieldMode == 1) {
					$c['name'] = $creator['ref']->lastName;
				}
				// Two-field mode
				else {
					$c['firstName'] = $creator['ref']->firstName;
					$c['lastName'] = $creator['ref']->lastName;
				}
				$arr['creators'][] = $c;
			}
			if (!$arr['creators'] && !$includeEmpty) {
				unset($arr['creators']);
			}
		}
		else {
			$titleFieldID = false;
		}
		
		// Item metadata
		$fields = array_keys($this->itemData);
		foreach ($fields as $field) {
			if ($field == $titleFieldID) {
				continue;
			}
			
			if ($unformattedFields) {
				$value = $this->itemData[$field];
			}
			else {
				$value = $this->getField($field);
			}
			
			if (!$includeEmpty && ($value === false || $value === "")) {
				continue;
			}
			
			$fieldName = Zotero_ItemFields::getName($field);
			// TEMP
			if ($fieldName == 'versionNumber') {
				if ($requestParams['v'] < 3) {
					$fieldName = 'version';
				}
			}
			else if ($fieldName == 'accessDate') {
				if ($requestParams['v'] >= 3 && $value !== false && $value !== "") {
					$value = Zotero_Date::sqlToISO8601($value);
				}
			}
			$arr[$fieldName] = ($value !== false && $value !== "") ? $value : "";
		}
		
		if ($requestParams['v'] >= 3) {
			$arr['dateAdded'] = Zotero_Date::sqlToISO8601($this->dateAdded);
			$arr['dateModified'] = Zotero_Date::sqlToISO8601($this->dateModified);
		}
		
		// Embedded note for notes and attachments
		if (!$regularItem) {
			// Use sanitized version
			$arr['note'] = $this->getNote(true);
		}
		
		if ($this->isAttachment()) {
			$val = $this->attachmentLinkMode;
			$arr['linkMode'] = strtolower(Zotero_Attachments::linkModeNumberToName($val));
			
			$val = $this->attachmentMIMEType;
			if ($includeEmpty || ($val !== false && $val !== "")) {
				$arr['contentType'] = $val;
			}
			
			$val = $this->attachmentCharset;
			if ($includeEmpty || ($val !== false && $val !== "")) {
				$arr['charset'] = $val;
			}
			
			if ($this->isImportedAttachment()) {
				$arr['filename'] = $this->attachmentFilename;
				
				$val = $this->attachmentStorageHash;
				if ($includeEmpty || $val) {
					$arr['md5'] = $val;
				}
				
				$val = $this->attachmentStorageModTime;
				if ($includeEmpty || $val) {
					$arr['mtime'] = $val;
				}
			}
		}
		
		if ($this->getDeleted()) {
			$arr['deleted'] = 1;
		}
		
		// Tags
		$arr['tags'] = array();
		$tags = $this->getTags();
		if ($tags) {
			foreach ($tags as $tag) {
				$t = array(
					'tag' => $tag->name
				);
				if ($tag->type != 0) {
					$t['type'] = $tag->type;
				}
				$arr['tags'][] = $t;
			}
		}
		
		if ($requestParams['v'] >= 2) {
			if ($this->isTopLevelItem()) {
				$collections = $this->getCollections(true);
				$arr['collections'] = $collections;
			}
			
			$arr['relations'] = $this->getRelations();
		}
		
		if ($asArray) {
			return $arr;
		}
		
		// Before v3, additional characters were escaped in the JSON, for unclear reasons
		$escapeAll = $requestParams['v'] <= 2;
		
		return Zotero_Utilities::formatJSON($arr, $escapeAll);
	}
Example #4
0
 public function toJSON($asArray = false, $prettyPrint = false, $includeEmpty = false, $unformattedFields = false)
 {
     if ($this->id || $this->key) {
         if (!$this->loaded['primaryData']) {
             $this->loadPrimaryData(true);
         }
         if (!$this->loaded['itemData']) {
             $this->loadItemData();
         }
     }
     $regularItem = $this->isRegularItem();
     $arr = array();
     $arr['itemType'] = Zotero_ItemTypes::getName($this->itemTypeID);
     if ($this->isAttachment()) {
         $val = $this->attachmentLinkMode;
         $arr['linkMode'] = strtolower(Zotero_Attachments::linkModeNumberToName($val));
     }
     // For regular items, show title and creators first
     if ($regularItem) {
         // Get 'title' or the equivalent base-mapped field
         $titleFieldID = Zotero_ItemFields::getFieldIDFromTypeAndBase($this->itemTypeID, 'title');
         $titleFieldName = Zotero_ItemFields::getName($titleFieldID);
         if ($includeEmpty || $this->itemData[$titleFieldID] !== false) {
             $arr[$titleFieldName] = $this->itemData[$titleFieldID] !== false ? $this->itemData[$titleFieldID] : "";
         }
         // Creators
         $arr['creators'] = array();
         $creators = $this->getCreators();
         foreach ($creators as $creator) {
             $c = array();
             $c['creatorType'] = Zotero_CreatorTypes::getName($creator['creatorTypeID']);
             // Single-field mode
             if ($creator['ref']->fieldMode == 1) {
                 $c['name'] = $creator['ref']->lastName;
             } else {
                 $c['firstName'] = $creator['ref']->firstName;
                 $c['lastName'] = $creator['ref']->lastName;
             }
             $arr['creators'][] = $c;
         }
         if (!$arr['creators'] && !$includeEmpty) {
             unset($arr['creators']);
         }
     } else {
         $titleFieldID = false;
     }
     // Item metadata
     $fields = array_keys($this->itemData);
     foreach ($fields as $field) {
         if ($field == $titleFieldID) {
             continue;
         }
         if ($unformattedFields) {
             $value = $this->itemData[$field];
         } else {
             $value = $this->getField($field);
         }
         if (!$includeEmpty && ($value === false || $value === "")) {
             continue;
         }
         $arr[Zotero_ItemFields::getName($field)] = $value ? $value : "";
     }
     // Embedded note for notes and attachments
     if (!$regularItem) {
         // Use sanitized version
         $arr['note'] = $this->getNote(true);
     }
     if ($this->isAttachment()) {
         $val = $this->attachmentLinkMode;
         $arr['linkMode'] = strtolower(Zotero_Attachments::linkModeNumberToName($val));
         $val = $this->attachmentMIMEType;
         if ($includeEmpty || $val !== false && $val !== "") {
             $arr['contentType'] = $val;
         }
         $val = $this->attachmentCharset;
         if ($includeEmpty || $val !== false && $val !== "") {
             $arr['charset'] = $val;
         }
         if ($this->isImportedAttachment()) {
             $arr['filename'] = $this->attachmentFilename;
             $val = $this->attachmentStorageHash;
             if ($includeEmpty || $val) {
                 $arr['md5'] = $val;
             }
             $val = $this->attachmentStorageModTime;
             if ($includeEmpty || $val) {
                 $arr['mtime'] = $val;
             }
         }
     }
     if ($this->getDeleted()) {
         $arr['deleted'] = 1;
     }
     // Tags
     $arr['tags'] = array();
     $tags = $this->getTags();
     if ($tags) {
         foreach ($tags as $tag) {
             $t = array('tag' => $tag->name);
             if ($tag->type != 0) {
                 $t['type'] = $tag->type;
             }
             $arr['tags'][] = $t;
         }
     }
     if ($asArray) {
         return $arr;
     }
     $mask = JSON_HEX_TAG | JSON_HEX_AMP;
     if ($prettyPrint) {
         $json = Zotero_Utilities::json_encode_pretty($arr, $mask);
     } else {
         $json = json_encode($arr, $mask);
     }
     // Until JSON_UNESCAPED_SLASHES is available
     $json = str_replace('\\/', '/', $json);
     return $json;
 }
 public function newItem()
 {
     if (empty($_GET['itemType'])) {
         $this->e400("'itemType' not provided");
     }
     $itemType = $_GET['itemType'];
     if ($itemType == 'attachment') {
         if (empty($_GET['linkMode'])) {
             $this->e400("linkMode required for itemType=attachment");
         }
         $linkModeName = $_GET['linkMode'];
         try {
             $linkMode = Zotero_Attachments::linkModeNameToNumber(strtoupper($linkModeName));
         } catch (Exception $e) {
             $this->e400("Invalid linkMode '{$linkModeName}'");
         }
     }
     $itemTypeID = Zotero_ItemTypes::getID($itemType);
     if (!$itemTypeID) {
         $this->e400("Invalid item type '{$itemType}'");
     }
     // TODO: check If-Modified-Since and return 304 if not changed
     $cacheKey = "newItemJSON_" . $itemTypeID;
     if ($itemType == 'attachment') {
         $cacheKey .= "_" . $linkMode;
     }
     $ttl = 60;
     if ($this->queryParams['pprint']) {
         $cacheKey .= "_pprint";
     }
     $json = Z_Core::$MC->get($cacheKey);
     if ($json) {
         header("Content-Type: application/json");
         echo $json;
         exit;
     }
     // Generate template
     $json = array('itemType' => $itemType);
     if ($itemType == 'attachment') {
         $json['linkMode'] = $linkModeName;
     }
     $fieldIDs = Zotero_ItemFields::getItemTypeFields($itemTypeID);
     $first = true;
     foreach ($fieldIDs as $fieldID) {
         $fieldName = Zotero_ItemFields::getName($fieldID);
         if ($itemType == 'attachment' && $fieldName == 'url' && !preg_match('/_url$/', $linkModeName)) {
             continue;
         }
         $json[$fieldName] = "";
         if ($first && $itemType != 'note' && $itemType != 'attachment') {
             $creatorTypeID = Zotero_CreatorTypes::getPrimaryIDForType($itemTypeID);
             $creatorTypeName = Zotero_CreatorTypes::getName($creatorTypeID);
             $json['creators'] = array(array('creatorType' => $creatorTypeName, 'firstName' => '', 'lastName' => ''));
             $first = false;
         }
     }
     if ($itemType == 'note' || $itemType == 'attachment') {
         $json['note'] = '';
     }
     $json['tags'] = array();
     if ($itemType != 'note' && $itemType != 'attachment') {
         $json['attachments'] = array();
         $json['notes'] = array();
     }
     if ($itemType == 'attachment') {
         $json['contentType'] = '';
         $json['charset'] = '';
         if (preg_match('/^imported_/', $linkModeName)) {
             $json['filename'] = '';
             $json['md5'] = null;
             $json['mtime'] = null;
             //$json['zip'] = false;
         }
     }
     header("Content-Type: application/json");
     if ($this->queryParams['pprint']) {
         $json = Zotero_Utilities::json_encode_pretty($json);
         Z_Core::$MC->set($cacheKey, $json, $ttl);
     } else {
         $json = json_encode($json);
         Z_Core::$MC->set($cacheKey, $json, $ttl);
     }
     echo $json;
     exit;
 }
Example #6
0
 public function itemContent()
 {
     $this->allowMethods(array('GET', 'PUT'));
     // Check for general library access
     if (!$this->permissions->canAccess($this->objectLibraryID)) {
         $this->e403();
     }
     if (!$this->singleObject) {
         $this->e404();
     }
     if ($this->isWriteMethod()) {
         // Check for library write access
         if (!$this->permissions->canWrite($this->objectLibraryID)) {
             $this->e403("Write access denied");
         }
         Zotero_Libraries::updateVersionAndTimestamp($this->objectLibraryID);
     }
     $item = Zotero_Items::getByLibraryAndKey($this->objectLibraryID, $this->objectKey);
     if (!$item) {
         $this->e404();
     }
     // If no access to the note, don't show that it exists
     if ($item->isNote() && !$this->permissions->canAccess($this->objectLibraryID, 'notes')) {
         $this->e404();
     }
     if (!$item->isAttachment() || Zotero_Attachments::linkModeNumberToName($item->attachmentLinkMode) == 'LINKED_URL') {
         $this->e404();
     }
     if ($this->isWriteMethod()) {
         $this->libraryVersion = Zotero_Libraries::getUpdatedVersion($this->objectLibraryID);
         if ($this->method == 'PUT') {
             $this->requireContentType("application/json");
             $json = json_decode($this->body, true);
             if (!$json) {
                 $this->e400("PUT data is not valid JSON");
             }
             $stats = [];
             foreach (Zotero_FullText::$metadata as $prop) {
                 if (isset($json[$prop])) {
                     $stats[$prop] = $json[$prop];
                 }
             }
             Zotero_FullText::indexItem($item, $json['content'], $stats);
             $this->e204();
         } else {
             $this->e405();
         }
     }
     $data = Zotero_FullText::getItemData($item->libraryID, $item->key);
     if (!$data) {
         $this->e404();
     }
     $this->libraryVersion = $data['version'];
     $json = ["content" => $data['content']];
     foreach (Zotero_FullText::$metadata as $prop) {
         if (!empty($data[$prop])) {
             $json[$prop] = $data[$prop];
         }
     }
     echo Zotero_Utilities::formatJSON($json);
     $this->end();
 }
Example #7
0
 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;
         }
     }
 }
Example #8
0
 /**
  * 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 function newItem()
 {
     if (empty($_GET['itemType'])) {
         $this->e400("'itemType' not provided");
     }
     $itemType = $_GET['itemType'];
     if ($itemType == 'attachment') {
         if (empty($_GET['linkMode'])) {
             $this->e400("linkMode required for itemType=attachment");
         }
         $linkModeName = $_GET['linkMode'];
         try {
             $linkMode = Zotero_Attachments::linkModeNameToNumber(strtoupper($linkModeName));
         } catch (Exception $e) {
             $this->e400("Invalid linkMode '{$linkModeName}'");
         }
     }
     $itemTypeID = Zotero_ItemTypes::getID($itemType);
     if (!$itemTypeID) {
         $this->e400("Invalid item type '{$itemType}'");
     }
     // TODO: check If-Modified-Since and return 304 if not changed
     $cacheVersion = 1;
     $cacheKey = "newItemJSON" . "_" . $this->apiVersion . "_" . $itemTypeID . "_" . $cacheVersion;
     if ($itemType == 'attachment') {
         $cacheKey .= "_" . $linkMode;
     }
     $cacheKey .= '_' . $this->apiVersion;
     $ttl = 60;
     $json = Z_Core::$MC->get($cacheKey);
     if ($json) {
         header("Content-Type: application/json");
         echo $json;
         exit;
     }
     // Generate template
     $json = array('itemType' => $itemType);
     if ($itemType == 'attachment') {
         $json['linkMode'] = $linkModeName;
     }
     $fieldIDs = Zotero_ItemFields::getItemTypeFields($itemTypeID);
     $first = true;
     foreach ($fieldIDs as $fieldID) {
         $fieldName = Zotero_ItemFields::getName($fieldID);
         // Before v3, computerProgram's 'versionNumber' was just 'version'
         if ($this->apiVersion < 3 && $fieldID == 81) {
             $fieldName = 'version';
         }
         if ($itemType == 'attachment' && $fieldName == 'url' && !preg_match('/_url$/', $linkModeName)) {
             continue;
         }
         $json[$fieldName] = "";
         if ($first && $itemType != 'note' && $itemType != 'attachment') {
             $creatorTypeID = Zotero_CreatorTypes::getPrimaryIDForType($itemTypeID);
             $creatorTypeName = Zotero_CreatorTypes::getName($creatorTypeID);
             $json['creators'] = array(array('creatorType' => $creatorTypeName, 'firstName' => '', 'lastName' => ''));
             $first = false;
         }
     }
     if ($itemType == 'note' || $itemType == 'attachment') {
         $json['note'] = '';
     }
     $json['tags'] = array();
     if ($this->apiVersion >= 2) {
         $json['collections'] = array();
         $json['relations'] = new stdClass();
     }
     if ($this->apiVersion == 1) {
         if ($itemType != 'note' && $itemType != 'attachment') {
             $json['attachments'] = array();
             $json['notes'] = array();
         }
     }
     if ($itemType == 'attachment') {
         $json['contentType'] = '';
         $json['charset'] = '';
         if (preg_match('/^imported_/', $linkModeName)) {
             $json['filename'] = '';
             $json['md5'] = null;
             $json['mtime'] = null;
             //$json['zip'] = false;
         }
     }
     header("Content-Type: application/json");
     $json = Zotero_Utilities::formatJSON($json);
     Z_Core::$MC->set($cacheKey, $json, $ttl);
     echo $json;
     exit;
 }