public function testGetLongDataValueFromXML() { $longTag = "Longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong"; $xml = <<<EOD \t\t\t<data> \t\t\t\t<creators><creator/></creators> \t\t\t\t<tags> \t\t\t\t\t<tag libraryID="1" key="AAAAAAAA" name="Test" dateAdded="2009-04-13 12:22:31" dateModified="2009-08-06 10:21:20"> \t\t\t\t\t\t<items>AAAAAAAA</items> \t\t\t\t\t</tag> \t\t\t\t\t<tag libraryID="1" key="BBBBBBBB" name="{$longTag}" dateAdded="2009-04-13 12:22:31" dateModified="2009-08-06 10:21:20"/> \t\t\t\t</tags> \t\t\t</data> EOD; $doc = new DOMDocument(); $doc->loadXML($xml); $tag = Zotero_Tags::getLongDataValueFromXML($doc); $this->assertEquals($longTag, $tag); }
public function deleted() { if ($this->apiVersion < 2) { $this->e404(); } $this->allowMethods(array('GET')); if (!$this->permissions->canAccess($this->objectLibraryID)) { $this->e403(); } $this->libraryVersion = Zotero_Libraries::getUpdatedVersion($this->objectLibraryID); // TEMP: sync transition if ($this->queryParams['sincetime'] !== null) { $deleted = array("collections" => Zotero_Collections::getDeleteLogKeys($this->objectLibraryID, $this->queryParams['sincetime'], true), "items" => Zotero_Items::getDeleteLogKeys($this->objectLibraryID, $this->queryParams['sincetime'], true), "searches" => Zotero_Searches::getDeleteLogKeys($this->objectLibraryID, $this->queryParams['sincetime'], true), "tags" => Zotero_Tags::getDeleteLogKeys($this->objectLibraryID, $this->queryParams['sincetime'], true), "settings" => Zotero_Settings::getDeleteLogKeys($this->objectLibraryID, $this->queryParams['sincetime'], true)); echo Zotero_Utilities::formatJSON($deleted); $this->end(); } if ($this->queryParams['since'] === null) { $this->e400("'since' parameter must be provided"); } $deleted = array("collections" => Zotero_Collections::getDeleteLogKeys($this->objectLibraryID, $this->queryParams['since']), "items" => Zotero_Items::getDeleteLogKeys($this->objectLibraryID, $this->queryParams['since']), "searches" => Zotero_Searches::getDeleteLogKeys($this->objectLibraryID, $this->queryParams['since']), "tags" => Zotero_Tags::getDeleteLogKeys($this->objectLibraryID, $this->queryParams['since']), "settings" => Zotero_Settings::getDeleteLogKeys($this->objectLibraryID, $this->queryParams['since'])); echo Zotero_Utilities::formatJSON($deleted); $this->end(); }
public static function search($libraryID, $onlyTopLevel = false, $params = array(), $includeTrashed = false, Zotero_Permissions $permissions = null) { $rnd = "_" . uniqid($libraryID . "_"); $results = array('results' => array(), 'total' => 0); // Default empty library if ($libraryID === 0) { return $results; } $shardID = Zotero_Shards::getByLibraryID($libraryID); $includeNotes = true; if ($permissions && !$permissions->canAccess($libraryID, 'notes')) { $includeNotes = false; } // Pass a list of itemIDs, for when the initial search is done via SQL $itemIDs = !empty($params['itemIDs']) ? $params['itemIDs'] : array(); $itemKeys = $params['itemKey']; $titleSort = !empty($params['sort']) && $params['sort'] == 'title'; $parentItemSort = !empty($params['sort']) && in_array($params['sort'], ['itemType', 'dateAdded', 'dateModified', 'serverDateModified', 'addedBy']); $sql = "SELECT SQL_CALC_FOUND_ROWS DISTINCT "; // In /top mode, use the parent item's values for most joins if ($onlyTopLevel) { $itemIDSelector = "COALESCE(IA.sourceItemID, INo.sourceItemID, I.itemID)"; $itemKeySelector = "COALESCE(IP.key, I.key)"; $itemVersionSelector = "COALESCE(IP.version, I.version)"; $itemTypeIDSelector = "COALESCE(IP.itemTypeID, I.itemTypeID)"; } else { $itemIDSelector = "I.itemID"; $itemKeySelector = "I.key"; $itemVersionSelector = "I.version"; $itemTypeIDSelector = "I.itemTypeID"; } if ($params['format'] == 'keys' || $params['format'] == 'versions') { // In /top mode, display the parent item of matching items $sql .= "{$itemKeySelector} AS `key`"; if ($params['format'] == 'versions') { $sql .= ", {$itemVersionSelector} AS version"; } } else { $sql .= "{$itemIDSelector} AS itemID"; } $sql .= " FROM items I "; $sqlParams = array($libraryID); // For /top, we need the parent itemID if ($onlyTopLevel) { $sql .= "LEFT JOIN itemAttachments IA ON (IA.itemID=I.itemID) "; } // For /top, we need the parent itemID; for 'q' we need the note; for sorting by title, // we need the note title if ($onlyTopLevel || !empty($params['q']) || $titleSort) { $sql .= "LEFT JOIN itemNotes INo ON (INo.itemID=I.itemID) "; } // For some /top requests, pull in the parent item's items row if ($onlyTopLevel && ($params['format'] == 'keys' || $params['format'] == 'versions' || $parentItemSort)) { $sql .= "LEFT JOIN items IP ON ({$itemIDSelector}=IP.itemID) "; } // Pull in titles if (!empty($params['q']) || $titleSort) { $titleFieldIDs = array_merge(array(Zotero_ItemFields::getID('title')), Zotero_ItemFields::getTypeFieldsFromBase('title')); $sql .= "LEFT JOIN itemData IDT ON (IDT.itemID=I.itemID AND IDT.fieldID IN " . "(" . implode(',', $titleFieldIDs) . ")) "; } // When sorting by title in /top mode, we need the title of the parent item if ($onlyTopLevel && $titleSort) { $titleSortDataTable = "IDTSort"; $titleSortNoteTable = "INoSort"; $sql .= "LEFT JOIN itemData IDTSort ON (IDTSort.itemID={$itemIDSelector} AND " . "IDTSort.fieldID IN (" . implode(',', $titleFieldIDs) . ")) " . "LEFT JOIN itemNotes INoSort ON (INoSort.itemID={$itemIDSelector}) "; } else { $titleSortDataTable = "IDT"; $titleSortNoteTable = "INo"; } if (!empty($params['q'])) { // Pull in creators $sql .= "LEFT JOIN itemCreators IC ON (IC.itemID=I.itemID) " . "LEFT JOIN creators C ON (C.creatorID=IC.creatorID) "; // Pull in dates $dateFieldIDs = array_merge(array(Zotero_ItemFields::getID('date')), Zotero_ItemFields::getTypeFieldsFromBase('date')); $sql .= "LEFT JOIN itemData IDD ON (IDD.itemID=I.itemID AND IDD.fieldID IN " . "(" . implode(',', $dateFieldIDs) . ")) "; } if ($includeTrashed) { if (!empty($params['trashedItemsOnly'])) { $sql .= "JOIN deletedItems DI ON (DI.itemID=I.itemID) "; } } else { $sql .= "LEFT JOIN deletedItems DI ON (DI.itemID=I.itemID) "; // In /top mode, we don't want to show results for deleted parents or children if ($onlyTopLevel) { $sql .= "LEFT JOIN deletedItems DIP ON (DIP.itemID={$itemIDSelector}) "; } } if (!empty($params['sort'])) { switch ($params['sort']) { case 'title': case 'creator': $sql .= "LEFT JOIN itemSortFields ISF ON (ISF.itemID={$itemIDSelector}) "; break; case 'date': // When sorting by date in /top mode, we need the date of the parent item if ($onlyTopLevel) { $sortTable = "IDDSort"; // Pull in dates $dateFieldIDs = array_merge(array(Zotero_ItemFields::getID('date')), Zotero_ItemFields::getTypeFieldsFromBase('date')); $sql .= "LEFT JOIN itemData IDDSort ON (IDDSort.itemID={$itemIDSelector} AND " . "IDDSort.fieldID IN (" . implode(',', $dateFieldIDs) . ")) "; } else { $sortTable = "IDD"; if (empty($params['q'])) { $dateFieldIDs = array_merge(array(Zotero_ItemFields::getID('date')), Zotero_ItemFields::getTypeFieldsFromBase('date')); $sql .= "LEFT JOIN itemData IDD ON (IDD.itemID=I.itemID AND IDD.fieldID IN (" . implode(',', $dateFieldIDs) . ")) "; } } break; case 'itemType': $locale = 'en-US'; $types = Zotero_ItemTypes::getAll($locale); // TEMP: get localized string // DEBUG: Why is attachment skipped in getAll()? $types[] = array('id' => 14, 'localized' => 'Attachment'); foreach ($types as $type) { $sql2 = "INSERT IGNORE INTO tmpItemTypeNames VALUES (?, ?, ?)"; Zotero_DB::query($sql2, array($type['id'], $locale, $type['localized']), $shardID); } // Join temp table to query $sql .= "JOIN tmpItemTypeNames TITN ON (TITN.itemTypeID={$itemTypeIDSelector}) "; break; case 'addedBy': $isGroup = Zotero_Libraries::getType($libraryID) == 'group'; if ($isGroup) { $sql2 = "SELECT DISTINCT createdByUserID FROM items\n\t\t\t\t\t\t\t\tJOIN groupItems USING (itemID) WHERE\n\t\t\t\t\t\t\t\tcreatedByUserID IS NOT NULL AND "; if ($itemIDs) { $sql2 .= "itemID IN (" . implode(', ', array_fill(0, sizeOf($itemIDs), '?')) . ") "; $createdByUserIDs = Zotero_DB::columnQuery($sql2, $itemIDs, $shardID); } else { $sql2 .= "libraryID=?"; $createdByUserIDs = Zotero_DB::columnQuery($sql2, $libraryID, $shardID); } // Populate temp table with usernames if ($createdByUserIDs) { $toAdd = array(); foreach ($createdByUserIDs as $createdByUserID) { $toAdd[] = array($createdByUserID, Zotero_Users::getUsername($createdByUserID)); } $sql2 = "INSERT IGNORE INTO tmpCreatedByUsers VALUES "; Zotero_DB::bulkInsert($sql2, $toAdd, 50, false, $shardID); // Join temp table to query $sql .= "LEFT JOIN groupItems GI ON (GI.itemID=I.itemID)\n\t\t\t\t\t\t\t\t\tLEFT JOIN tmpCreatedByUsers TCBU ON (TCBU.userID=GI.createdByUserID) "; } } break; } } $sql .= "WHERE I.libraryID=? "; if (!$includeTrashed) { $sql .= "AND DI.itemID IS NULL "; // Hide deleted parents in /top mode if ($onlyTopLevel) { $sql .= "AND DIP.itemID IS NULL "; } } // Search on title, creators, and dates if (!empty($params['q'])) { $sql .= "AND ("; $sql .= "IDT.value LIKE ? "; $sqlParams[] = '%' . $params['q'] . '%'; $sql .= "OR INo.title LIKE ? "; $sqlParams[] = '%' . $params['q'] . '%'; $sql .= "OR TRIM(CONCAT(firstName, ' ', lastName)) LIKE ? "; $sqlParams[] = '%' . $params['q'] . '%'; $sql .= "OR SUBSTR(IDD.value, 1, 4) = ?"; $sqlParams[] = $params['q']; // Full-text search if ($params['qmode'] == 'everything') { $ftKeys = Zotero_FullText::searchInLibrary($libraryID, $params['q']); if ($ftKeys) { $sql .= " OR I.key IN (" . implode(', ', array_fill(0, sizeOf($ftKeys), '?')) . ") "; $sqlParams = array_merge($sqlParams, $ftKeys); } } $sql .= ") "; } // Search on itemType if (!empty($params['itemType'])) { $itemTypes = Zotero_API::getSearchParamValues($params, 'itemType'); if ($itemTypes) { if (sizeOf($itemTypes) > 1) { throw new Exception("Cannot specify 'itemType' more than once", Z_ERROR_INVALID_INPUT); } $itemTypes = $itemTypes[0]; $itemTypeIDs = array(); foreach ($itemTypes['values'] as $itemType) { $itemTypeID = Zotero_ItemTypes::getID($itemType); if (!$itemTypeID) { throw new Exception("Invalid itemType '{$itemType}'", Z_ERROR_INVALID_INPUT); } $itemTypeIDs[] = $itemTypeID; } $sql .= "AND I.itemTypeID " . ($itemTypes['negation'] ? "NOT " : "") . "IN (" . implode(',', array_fill(0, sizeOf($itemTypeIDs), '?')) . ") "; $sqlParams = array_merge($sqlParams, $itemTypeIDs); } } if (!$includeNotes) { $sql .= "AND I.itemTypeID != 1 "; } if (!empty($params['since'])) { $sql .= "AND {$itemVersionSelector} > ? "; $sqlParams[] = $params['since']; } // TEMP: for sync transition if (!empty($params['sincetime']) && $params['sincetime'] != 1) { $sql .= "AND I.serverDateModified >= FROM_UNIXTIME(?) "; $sqlParams[] = $params['sincetime']; } // Tags // // ?tag=foo // ?tag=foo bar // phrase // ?tag=-foo // negation // ?tag=\-foo // literal hyphen (only for first character) // ?tag=foo&tag=bar // AND $tagSets = Zotero_API::getSearchParamValues($params, 'tag'); if ($tagSets) { $sql2 = "SELECT itemID FROM items WHERE libraryID=?\n"; $sqlParams2 = array($libraryID); $positives = array(); $negatives = array(); foreach ($tagSets as $set) { $tagIDs = array(); foreach ($set['values'] as $tag) { $ids = Zotero_Tags::getIDs($libraryID, $tag, true); if (!$ids) { $ids = array(0); } $tagIDs = array_merge($tagIDs, $ids); } $tagIDs = array_unique($tagIDs); $tmpSQL = "SELECT itemID FROM items JOIN itemTags USING (itemID) " . "WHERE tagID IN (" . implode(',', array_fill(0, sizeOf($tagIDs), '?')) . ")"; $ids = Zotero_DB::columnQuery($tmpSQL, $tagIDs, $shardID); if (!$ids) { // If no negative tags, skip this tag set if ($set['negation']) { continue; } // If no positive tags, return no matches return $results; } $ids = $ids ? $ids : array(); $sql2 .= " AND itemID " . ($set['negation'] ? "NOT " : "") . " IN (" . implode(',', array_fill(0, sizeOf($ids), '?')) . ")"; $sqlParams2 = array_merge($sqlParams2, $ids); } $tagItems = Zotero_DB::columnQuery($sql2, $sqlParams2, $shardID); // No matches if (!$tagItems) { return $results; } // Combine with passed ids if ($itemIDs) { $itemIDs = array_intersect($itemIDs, $tagItems); // None of the tag matches match the passed ids if (!$itemIDs) { return $results; } } else { $itemIDs = $tagItems; } } if ($itemIDs) { $sql .= "AND I.itemID IN (" . implode(', ', array_fill(0, sizeOf($itemIDs), '?')) . ") "; $sqlParams = array_merge($sqlParams, $itemIDs); } if ($itemKeys) { $sql .= "AND I.key IN (" . implode(', ', array_fill(0, sizeOf($itemKeys), '?')) . ") "; $sqlParams = array_merge($sqlParams, $itemKeys); } $sql .= "ORDER BY "; if (!empty($params['sort'])) { switch ($params['sort']) { case 'dateAdded': case 'dateModified': case 'serverDateModified': if ($onlyTopLevel) { $orderSQL = "IP." . $params['sort']; } else { $orderSQL = "I." . $params['sort']; } break; case 'itemType': $orderSQL = "TITN.itemTypeName"; /* // Optional method for sorting by localized item type name, which would avoid // the INSERT and JOIN above and allow these requests to use DB read replicas $locale = 'en-US'; $types = Zotero_ItemTypes::getAll($locale); // TEMP: get localized string // DEBUG: Why is attachment skipped in getAll()? $types[] = [ 'id' => 14, 'localized' => 'Attachment' ]; usort($types, function ($a, $b) { return strcasecmp($a['localized'], $b['localized']); }); // Pass order of localized item type names for sorting // e.g., FIELD(14, 12, 14, 26...) for sorting "Attachment" after "Artwork" $orderSQL = "FIELD($itemTypeIDSelector, " . implode(", ", array_map(function ($x) { return $x['id']; }, $types)) . ")"; // If itemTypeID isn't found in passed list (currently only for NSF Reviewer), // sort last $orderSQL = "IFNULL(NULLIF($orderSQL, 0), 99999)"; // All items have types, so no need to check for empty sort values $params['emptyFirst'] = true; */ break; case 'title': $orderSQL = "IFNULL(COALESCE(sortTitle, {$titleSortDataTable}.value, {$titleSortNoteTable}.title), '')"; break; case 'creator': $orderSQL = "ISF.creatorSummary"; break; // TODO: generic base field mapping-aware sorting // TODO: generic base field mapping-aware sorting case 'date': $orderSQL = "{$sortTable}.value"; break; case 'addedBy': if ($isGroup && $createdByUserIDs) { $orderSQL = "TCBU.username"; } else { $orderSQL = ($onlyTopLevel ? "IP" : "I") . ".dateAdded"; } break; case 'itemKeyList': $orderSQL = "FIELD(I.key," . implode(',', array_fill(0, sizeOf($itemKeys), '?')) . ")"; $sqlParams = array_merge($sqlParams, $itemKeys); break; default: $fieldID = Zotero_ItemFields::getID($params['sort']); if (!$fieldID) { throw new Exception("Invalid order field '" . $params['sort'] . "'"); } $orderSQL = "(SELECT value FROM itemData WHERE itemID=I.itemID AND fieldID=?)"; if (!$params['emptyFirst']) { $sqlParams[] = $fieldID; } $sqlParams[] = $fieldID; } if (!empty($params['direction'])) { $dir = $params['direction']; } else { $dir = "ASC"; } if (!$params['emptyFirst']) { $sql .= "IFNULL({$orderSQL}, '') = '' {$dir}, "; } $sql .= $orderSQL . " {$dir}, "; } $sql .= "I.version " . (!empty($params['direction']) ? $params['direction'] : "ASC") . ", I.itemID " . (!empty($params['direction']) ? $params['direction'] : "ASC") . " "; if (!empty($params['limit'])) { $sql .= "LIMIT ?, ?"; $sqlParams[] = $params['start'] ? $params['start'] : 0; $sqlParams[] = $params['limit']; } // Log SQL statement with embedded parameters /*if (true || !empty($_GET['sqldebug'])) { error_log($onlyTopLevel); $debugSQL = ""; $parts = explode("?", $sql); $debugSQLParams = $sqlParams; foreach ($parts as $part) { $val = array_shift($debugSQLParams); $debugSQL .= $part; if (!is_null($val)) { $debugSQL .= is_int($val) ? $val : '"' . $val . '"'; } } error_log($debugSQL . ";"); }*/ if ($params['format'] == 'versions') { $rows = Zotero_DB::query($sql, $sqlParams, $shardID); } else { $rows = Zotero_DB::columnQuery($sql, $sqlParams, $shardID); } $results['total'] = Zotero_DB::valueQuery("SELECT FOUND_ROWS()", false, $shardID); if ($rows) { if ($params['format'] == 'keys') { $results['results'] = $rows; } else { if ($params['format'] == 'versions') { foreach ($rows as $row) { $results['results'][$row['key']] = $row['version']; } } else { $results['results'] = Zotero_Items::get($libraryID, $rows); } } } return $results; }
public static function search($libraryID, $params) { $results = array('results' => array(), 'total' => 0); // Default empty library if ($libraryID === 0) { return $results; } $shardID = Zotero_Shards::getByLibraryID($libraryID); $sql = "SELECT SQL_CALC_FOUND_ROWS DISTINCT tagID FROM tags " . "JOIN itemTags USING (tagID) WHERE libraryID=? "; $sqlParams = array($libraryID); // Pass a list of tagIDs, for when the initial search is done via SQL $tagIDs = !empty($params['tagIDs']) ? $params['tagIDs'] : array(); // Filter for specific tags with "?tag=foo || bar" $tagNames = !empty($params['tag']) ? explode(' || ', $params['tag']) : array(); if ($tagIDs) { $sql .= "AND tagID IN (" . implode(', ', array_fill(0, sizeOf($tagIDs), '?')) . ") "; $sqlParams = array_merge($sqlParams, $tagIDs); } if ($tagNames) { $sql .= "AND `name` IN (" . implode(', ', array_fill(0, sizeOf($tagNames), '?')) . ") "; $sqlParams = array_merge($sqlParams, $tagNames); } if (!empty($params['q'])) { if (!is_array($params['q'])) { $params['q'] = array($params['q']); } foreach ($params['q'] as $q) { $sql .= "AND name LIKE ? "; $sqlParams[] = "%{$q}%"; } } $tagTypeSets = Zotero_API::getSearchParamValues($params, 'tagType'); if ($tagTypeSets) { $positives = array(); $negatives = array(); foreach ($tagTypeSets as $set) { if ($set['negation']) { $negatives = array_merge($negatives, $set['values']); } else { $positives = array_merge($positives, $set['values']); } } if ($positives) { $sql .= "AND type IN (" . implode(',', array_fill(0, sizeOf($positives), '?')) . ") "; $sqlParams = array_merge($sqlParams, $positives); } if ($negatives) { $sql .= "AND type NOT IN (" . implode(',', array_fill(0, sizeOf($negatives), '?')) . ") "; $sqlParams = array_merge($sqlParams, $negatives); } } if (!empty($params['since'])) { $sql .= "AND version > ? "; $sqlParams[] = $params['since']; } if (!empty($params['sort'])) { $order = $params['sort']; if ($order == 'title') { // Force a case-insensitive sort $sql .= "ORDER BY name COLLATE utf8_unicode_ci "; } else { if ($order == 'numItems') { $sql .= "GROUP BY tags.tagID ORDER BY COUNT(tags.tagID)"; } else { $sql .= "ORDER BY {$order} "; } } if (!empty($params['direction'])) { $sql .= " " . $params['direction'] . " "; } } if (!empty($params['limit'])) { $sql .= "LIMIT ?, ?"; $sqlParams[] = $params['start'] ? $params['start'] : 0; $sqlParams[] = $params['limit']; } $ids = Zotero_DB::columnQuery($sql, $sqlParams, $shardID); $results['total'] = Zotero_DB::valueQuery("SELECT FOUND_ROWS()", false, $shardID); if ($ids) { $tags = array(); foreach ($ids as $id) { $tags[] = Zotero_Tags::get($libraryID, $id); } $results['results'] = $tags; } return $results; }
protected function loadTags($reload = false) { if ($this->loaded['tags'] && !$reload) return; if (!$this->id) { return; } Z_Core::debug("Loading tags for item $this->id"); $sql = "SELECT tagID FROM itemTags JOIN tags USING (tagID) WHERE itemID=?"; $tagIDs = Zotero_DB::columnQuery( $sql, $this->id, Zotero_Shards::getByLibraryID($this->libraryID) ); $this->tags = []; if ($tagIDs) { foreach ($tagIDs as $tagID) { $this->tags[] = Zotero_Tags::get($this->libraryID, $tagID, true); } } $this->loaded['tags'] = true; $this->clearChanged('tags'); }
/** * $tags is an array of objects with properties 'tag' and 'type' */ public function setTags($newTags) { if (!$this->id) { throw new Exception('itemID not set'); } $numTags = $this->numTags(); if (!$newTags && !$numTags) { return false; } Zotero_DB::beginTransaction(); $existingTags = $this->getTags(); $toAdd = array(); $toRemove = array(); // Get new tags not in existing for ($i = 0, $len = sizeOf($newTags); $i < $len; $i++) { if (!isset($newTags[$i]->type)) { $newTags[$i]->type = 0; } $name = trim($newTags[$i]->tag); // 'tag', not 'name', since that's what JSON uses $type = $newTags[$i]->type; foreach ($existingTags as $tag) { // Do a case-insensitive comparison, to match the client if (strtolower($tag->name) == strtolower($name) && $tag->type == $type) { continue 2; } } $toAdd[] = $newTags[$i]; } // Get existing tags not in new for ($i = 0, $len = sizeOf($existingTags); $i < $len; $i++) { $name = $existingTags[$i]->name; $type = $existingTags[$i]->type; foreach ($newTags as $tag) { if (strtolower($tag->tag) == strtolower($name) && $tag->type == $type) { continue 2; } } $toRemove[] = $existingTags[$i]; } foreach ($toAdd as $tag) { $name = $tag->tag; $type = $tag->type; $tagID = Zotero_Tags::getID($this->libraryID, $name, $type, true); if (!$tagID) { $tag = new Zotero_Tag(); $tag->libraryID = $this->libraryID; $tag->name = $name; $tag->type = $type; $tagID = $tag->save(); } $tag = Zotero_Tags::get($this->libraryID, $tagID); $tag->addItem($this->id); $tag->save(); } foreach ($toRemove as $tag) { $tag->removeItem($this->id); $tag->save(); } Zotero_DB::commit(); return $toAdd || $toRemove; }
private function load() { $libraryID = $this->libraryID; $id = $this->id; $key = $this->key; if (!$libraryID) { throw new Exception("Library ID not set"); } if (!$id && !$key) { throw new Exception("ID or key not set"); } // Cache tag data for the entire library if (true) { if ($id) { Z_Core::debug("Loading data for tag {$this->libraryID}/{$this->id}"); $row = Zotero_Tags::getPrimaryDataByID($libraryID, $id); } else { Z_Core::debug("Loading data for tag {$this->libraryID}/{$this->key}"); $row = Zotero_Tags::getPrimaryDataByKey($libraryID, $key); } $this->loaded = true; if (!$row) { return; } if ($row['libraryID'] != $libraryID) { throw new Exception("libraryID {$row['libraryID']} != {$this->libraryID}"); } foreach ($row as $key => $val) { $this->{$key} = $val; } } else { // Use cached check for existence if possible if ($libraryID && $key) { if (!Zotero_Tags::existsByLibraryAndKey($libraryID, $key)) { $this->loaded = true; return; } } $shardID = Zotero_Shards::getByLibraryID($libraryID); $sql = Zotero_Tags::getPrimaryDataSQL(); if ($id) { $sql .= "tagID=?"; $stmt = Zotero_DB::getStatement($sql, false, $shardID); $data = Zotero_DB::rowQueryFromStatement($stmt, $id); } else { $sql .= "libraryID=? AND `key`=?"; $stmt = Zotero_DB::getStatement($sql, false, $shardID); $data = Zotero_DB::rowQueryFromStatement($stmt, array($libraryID, $key)); } $this->loaded = true; if (!$data) { return; } if ($data['libraryID'] != $libraryID) { throw new Exception("libraryID {$data['libraryID']} != {$libraryID}"); } foreach ($data as $k => $v) { $this->{$k} = $v; } } }
public function tags() { $this->allowMethods(array('GET')); if (!$this->permissions->canAccess($this->objectLibraryID)) { $this->e403(); } $tags = array(); $totalResults = 0; $name = $this->objectName; $fixedValues = array(); // Set of tags matching name if ($name && $this->subset != 'tags') { $tagIDs = Zotero_Tags::getIDs($this->objectLibraryID, $name); if (!$tagIDs) { $this->e404(); } $title = "Tags matching ‘" . $name . "’"; } else { if ($this->scopeObject) { // If id, redirect to key URL if ($this->scopeObjectID) { if (!in_array($this->scopeObject, array("collections", "items"))) { $this->e400(); } $className = 'Zotero_' . ucwords($this->scopeObject); $obj = call_user_func(array($className, 'get'), $this->objectLibraryID, $this->scopeObjectID); if (!$obj) { $this->e404("Scope " . substr($this->scopeObject, 0, -1) . " not found"); } $base = call_user_func(array('Zotero_API', 'get' . substr(ucwords($this->scopeObject), 0, -1) . 'URI'), $obj); $qs = !empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : ''; header("Location: " . $base . "/tags" . $qs); exit; } switch ($this->scopeObject) { case 'collections': $collection = Zotero_Collections::getByLibraryAndKey($this->objectLibraryID, $this->scopeObjectKey); if (!$collection) { $this->e404(); } $title = "Tags in Collection ‘" . $collection->name . "’"; $counts = $collection->getTagItemCounts(); $tagIDs = array(); if ($counts) { foreach ($counts as $tagID => $count) { $tagIDs[] = $tagID; $fixedValues[$tagID] = array('numItems' => $count); } } break; case 'items': $item = Zotero_Items::getByLibraryAndKey($this->objectLibraryID, $this->scopeObjectKey); if (!$item) { $this->e404(); } $title = "Tags of '" . $item->getDisplayTitle() . "'"; $tagIDs = $item->getTags(true); break; default: throw new Exception("Invalid tags scope object '{$this->scopeObject}'"); } } else { $title = "Tags"; $results = Zotero_Tags::getAllAdvanced($this->objectLibraryID, $this->queryParams); $tags = $results['objects']; $totalResults = $results['total']; } } if (!empty($tagIDs)) { foreach ($tagIDs as $tagID) { $tags[] = Zotero_Tags::get($this->objectLibraryID, $tagID); } // Fake sorting and limiting $totalResults = sizeOf($tags); $key = $this->queryParams['order']; // 'title' order means 'name' for tags if ($key == 'title') { $key = 'name'; } $dir = $this->queryParams['sort']; $cmp = create_function('$a, $b', '$dir = "' . $dir . '" == "asc" ? 1 : -1; if ($a->' . $key . ' == $b->' . $key . ') { return 0; } else { return ($a->' . $key . ' > $b->' . $key . ') ? $dir : ($dir * -1);}'); usort($tags, $cmp); $tags = array_slice($tags, $this->queryParams['start'], $this->queryParams['limit']); } $this->responseXML = Zotero_Atom::createAtomFeed($this->getFeedNamePrefix($this->objectLibraryID) . $title, $this->uri, $tags, $totalResults, $this->queryParams, $this->apiVersion, $this->permissions, $fixedValues); $this->end(); }
private static function processUploadInternal($userID, SimpleXMLElement $xml, $syncQueueID = null, $syncProcessID = null) { $userLibraryID = Zotero_Users::getLibraryIDFromUserID($userID); $affectedLibraries = self::parseAffectedLibraries($xml->asXML()); // Relations-only uploads don't have affected libraries if (!$affectedLibraries) { $affectedLibraries = array(Zotero_Users::getLibraryIDFromUserID($userID)); } $processID = self::addUploadProcess($userID, $affectedLibraries, $syncQueueID, $syncProcessID); set_time_limit(5400); $profile = false; if ($profile) { $shardID = Zotero_Shards::getByUserID($userID); Zotero_DB::profileStart($shardID); } try { Zotero_DB::beginTransaction(); // Mark libraries as updated foreach ($affectedLibraries as $libraryID) { Zotero_Libraries::updateVersion($libraryID); } $timestamp = Zotero_Libraries::updateTimestamps($affectedLibraries); Zotero_DB::registerTransactionTimestamp($timestamp); // Make sure no other upload sessions use this same timestamp // for any of these libraries, since we return >= 1 as the next // last sync time if (!Zotero_Libraries::setTimestampLock($affectedLibraries, $timestamp)) { throw new Exception("Library timestamp already used", Z_ERROR_LIBRARY_TIMESTAMP_ALREADY_USED); } $modifiedItems = array(); // Add/update creators if ($xml->creators) { // DOM $keys = array(); $xmlElements = dom_import_simplexml($xml->creators); $xmlElements = $xmlElements->getElementsByTagName('creator'); Zotero_DB::query("SET foreign_key_checks = 0"); try { $addedLibraryIDs = array(); $addedCreatorDataHashes = array(); foreach ($xmlElements as $xmlElement) { $key = $xmlElement->getAttribute('key'); if (isset($keys[$key])) { throw new Exception("Creator {$key} already processed"); } $keys[$key] = true; $creatorObj = Zotero_Creators::convertXMLToCreator($xmlElement); if (Zotero_Utilities::unicodeTrim($creatorObj->firstName) === '' && Zotero_Utilities::unicodeTrim($creatorObj->lastName) === '') { continue; } $addedLibraryIDs[] = $creatorObj->libraryID; $changed = $creatorObj->save($userID); // If the creator changed, we need to update all linked items if ($changed) { $modifiedItems = array_merge($modifiedItems, $creatorObj->getLinkedItems()); } } } catch (Exception $e) { Zotero_DB::query("SET foreign_key_checks = 1"); throw $e; } Zotero_DB::query("SET foreign_key_checks = 1"); unset($keys); unset($xml->creators); // // Manual foreign key checks // // libraryID foreach (array_unique($addedLibraryIDs) as $addedLibraryID) { $shardID = Zotero_Shards::getByLibraryID($addedLibraryID); $sql = "SELECT COUNT(*) FROM shardLibraries WHERE libraryID=?"; if (!Zotero_DB::valueQuery($sql, $addedLibraryID, $shardID)) { throw new Exception("libraryID inserted into `creators` not found in `shardLibraries` ({$addedLibraryID}, {$shardID})"); } } } // Add/update items $savedItems = array(); if ($xml->items) { $childItems = array(); // DOM $xmlElements = dom_import_simplexml($xml->items); $xmlElements = $xmlElements->getElementsByTagName('item'); foreach ($xmlElements as $xmlElement) { $libraryID = (int) $xmlElement->getAttribute('libraryID'); $key = $xmlElement->getAttribute('key'); if (isset($savedItems[$libraryID . "/" . $key])) { throw new Exception("Item {$libraryID}/{$key} already processed"); } $itemObj = Zotero_Items::convertXMLToItem($xmlElement); if (!$itemObj->getSourceKey()) { try { $modified = $itemObj->save($userID); if ($modified) { $savedItems[$libraryID . "/" . $key] = true; } } catch (Exception $e) { if (strpos($e->getMessage(), 'libraryIDs_do_not_match') !== false) { throw new Exception($e->getMessage() . " ({$key})"); } throw $e; } } else { $childItems[] = $itemObj; } } unset($xml->items); while ($childItem = array_shift($childItems)) { $libraryID = $childItem->libraryID; $key = $childItem->key; if (isset($savedItems[$libraryID . "/" . $key])) { throw new Exception("Item {$libraryID}/{$key} already processed"); } $modified = $childItem->save($userID); if ($modified) { $savedItems[$libraryID . "/" . $key] = true; } } } // Add/update collections if ($xml->collections) { $collections = array(); $collectionSets = array(); // DOM // Build an array of unsaved collection objects and the keys of child items $keys = array(); $xmlElements = dom_import_simplexml($xml->collections); $xmlElements = $xmlElements->getElementsByTagName('collection'); foreach ($xmlElements as $xmlElement) { $key = $xmlElement->getAttribute('key'); if (isset($keys[$key])) { throw new Exception("Collection {$key} already processed"); } $keys[$key] = true; $collectionObj = Zotero_Collections::convertXMLToCollection($xmlElement); $xmlItems = $xmlElement->getElementsByTagName('items')->item(0); // Fix an error if there's leading or trailing whitespace, // which was possible in 2.0.3 if ($xmlItems) { $xmlItems = trim($xmlItems->nodeValue); } $arr = array('obj' => $collectionObj, 'items' => $xmlItems ? explode(' ', $xmlItems) : array()); $collections[] = $collectionObj; $collectionSets[] = $arr; } unset($keys); unset($xml->collections); self::saveCollections($collections, $userID); unset($collections); // Set child items foreach ($collectionSets as $collection) { // Child items if (isset($collection['items'])) { $ids = array(); foreach ($collection['items'] as $key) { $item = Zotero_Items::getByLibraryAndKey($collection['obj']->libraryID, $key); if (!$item) { throw new Exception("Child item '{$key}' of collection {$collection['obj']->id} not found", Z_ERROR_ITEM_NOT_FOUND); } $ids[] = $item->id; } $collection['obj']->setItems($ids); } } unset($collectionSets); } // Add/update saved searches if ($xml->searches) { $searches = array(); $keys = array(); foreach ($xml->searches->search as $xmlElement) { $key = (string) $xmlElement['key']; if (isset($keys[$key])) { throw new Exception("Search {$key} already processed"); } $keys[$key] = true; $searchObj = Zotero_Searches::convertXMLToSearch($xmlElement); $searchObj->save($userID); } unset($xml->searches); } // Add/update tags if ($xml->tags) { $keys = array(); // DOM $xmlElements = dom_import_simplexml($xml->tags); $xmlElements = $xmlElements->getElementsByTagName('tag'); foreach ($xmlElements as $xmlElement) { // TEMP $tagItems = $xmlElement->getElementsByTagName('items'); if ($tagItems->length && $tagItems->item(0)->nodeValue == "") { error_log("Skipping tag with no linked items"); continue; } $libraryID = (int) $xmlElement->getAttribute('libraryID'); $key = $xmlElement->getAttribute('key'); $lk = $libraryID . "/" . $key; if (isset($keys[$lk])) { throw new Exception("Tag {$lk} already processed"); } $keys[$lk] = true; $itemKeysToUpdate = array(); $tagObj = Zotero_Tags::convertXMLToTag($xmlElement, $itemKeysToUpdate); // We need to update removed items, added items, and, // if the tag itself has changed, existing items $modifiedItems = array_merge($modifiedItems, array_map(function ($key) use($libraryID) { return $libraryID . "/" . $key; }, $itemKeysToUpdate)); $tagObj->save($userID, true); } unset($keys); unset($xml->tags); } // Add/update relations if ($xml->relations) { // DOM $xmlElements = dom_import_simplexml($xml->relations); $xmlElements = $xmlElements->getElementsByTagName('relation'); foreach ($xmlElements as $xmlElement) { $relationObj = Zotero_Relations::convertXMLToRelation($xmlElement, $userLibraryID); if ($relationObj->exists()) { continue; } $relationObj->save($userID); } unset($keys); unset($xml->relations); } // Add/update settings if ($xml->settings) { // DOM $xmlElements = dom_import_simplexml($xml->settings); $xmlElements = $xmlElements->getElementsByTagName('setting'); foreach ($xmlElements as $xmlElement) { $settingObj = Zotero_Settings::convertXMLToSetting($xmlElement); $settingObj->save($userID); } unset($xml->settings); } if ($xml->fulltexts) { // DOM $xmlElements = dom_import_simplexml($xml->fulltexts); $xmlElements = $xmlElements->getElementsByTagName('fulltext'); foreach ($xmlElements as $xmlElement) { Zotero_FullText::indexFromXML($xmlElement, $userID); } unset($xml->fulltexts); } // TODO: loop if ($xml->deleted) { // Delete collections if ($xml->deleted->collections) { Zotero_Collections::deleteFromXML($xml->deleted->collections, $userID); } // Delete items if ($xml->deleted->items) { Zotero_Items::deleteFromXML($xml->deleted->items, $userID); } // Delete creators if ($xml->deleted->creators) { Zotero_Creators::deleteFromXML($xml->deleted->creators, $userID); } // Delete saved searches if ($xml->deleted->searches) { Zotero_Searches::deleteFromXML($xml->deleted->searches, $userID); } // Delete tags if ($xml->deleted->tags) { $xmlElements = dom_import_simplexml($xml->deleted->tags); $xmlElements = $xmlElements->getElementsByTagName('tag'); foreach ($xmlElements as $xmlElement) { $libraryID = (int) $xmlElement->getAttribute('libraryID'); $key = $xmlElement->getAttribute('key'); $tagObj = Zotero_Tags::getByLibraryAndKey($libraryID, $key); if (!$tagObj) { continue; } // We need to update all items on the deleted tag $modifiedItems = array_merge($modifiedItems, array_map(function ($key) use($libraryID) { return $libraryID . "/" . $key; }, $tagObj->getLinkedItems(true))); } Zotero_Tags::deleteFromXML($xml->deleted->tags, $userID); } // Delete relations if ($xml->deleted->relations) { Zotero_Relations::deleteFromXML($xml->deleted->relations, $userID); } // Delete relations if ($xml->deleted->settings) { Zotero_Settings::deleteFromXML($xml->deleted->settings, $userID); } } $toUpdate = array(); foreach ($modifiedItems as $item) { // libraryID/key string if (is_string($item)) { if (isset($savedItems[$item])) { continue; } $savedItems[$item] = true; list($libraryID, $key) = explode("/", $item); $item = Zotero_Items::getByLibraryAndKey($libraryID, $key); if (!$item) { // Item was deleted continue; } } else { $lk = $item->libraryID . "/" . $item->key; if (isset($savedItems[$lk])) { continue; } $savedItems[$lk] = true; } $toUpdate[] = $item; } Zotero_Items::updateVersions($toUpdate, $userID); unset($savedItems); unset($modifiedItems); try { self::removeUploadProcess($processID); } catch (Exception $e) { if (strpos($e->getMessage(), 'MySQL server has gone away') !== false) { // Reconnect error_log("Reconnecting to MySQL master"); Zotero_DB::close(); self::removeUploadProcess($processID); } else { throw $e; } } // Send notifications for changed libraries foreach ($affectedLibraries as $libraryID) { Zotero_Notifier::trigger('modify', 'library', $libraryID); } Zotero_DB::commit(); if ($profile) { $shardID = Zotero_Shards::getByUserID($userID); Zotero_DB::profileEnd($shardID); } // Return timestamp + 1, to keep the next /updated call // (using >= timestamp) from returning this data return $timestamp + 1; } catch (Exception $e) { Zotero_DB::rollback(true); self::removeUploadProcess($processID); throw $e; } }
/** * Returns all tags assigned to items in this collection */ public function getTags($asIDs = false) { $sql = "SELECT tagID FROM tags JOIN itemTags USING (tagID)\n\t\t\t\tJOIN collectionItems USING (itemID) WHERE collectionID=? ORDER BY name"; $tagIDs = Zotero_DB::columnQuery($sql, $this->id, Zotero_Shards::getByLibraryID($this->libraryID)); if (!$tagIDs) { return false; } if ($asIDs) { return $tagIDs; } $tagObjs = array(); foreach ($tagIDs as $tagID) { $tag = Zotero_Tags::get($tagID, true); $tagObjs[] = $tag; } return $tagObjs; }
private static function processUploadInternal($userID, SimpleXMLElement $xml, $syncQueueID = null, $syncProcessID = null) { $userLibraryID = Zotero_Users::getLibraryIDFromUserID($userID); $affectedLibraries = self::parseAffectedLibraries($xml->asXML()); // Relations-only uploads don't have affected libraries if (!$affectedLibraries) { $affectedLibraries = array(Zotero_Users::getLibraryIDFromUserID($userID)); } $processID = self::addUploadProcess($userID, $affectedLibraries, $syncQueueID, $syncProcessID); set_time_limit(5400); $profile = false; if ($profile) { $shardID = Zotero_Shards::getByUserID($userID); Zotero_DB::profileStart($shardID); } try { Z_Core::$MC->begin(); Zotero_DB::beginTransaction(); // Mark libraries as updated $timestamp = Zotero_Libraries::updateTimestamps($affectedLibraries); Zotero_DB::registerTransactionTimestamp($timestamp); // Make sure no other upload sessions use this same timestamp // for any of these libraries, since we return >= 1 as the next // last sync time if (!Zotero_Libraries::setTimestampLock($affectedLibraries, $timestamp)) { throw new Exception("Library timestamp already used", Z_ERROR_LIBRARY_TIMESTAMP_ALREADY_USED); } // Add/update creators if ($xml->creators) { // DOM $keys = array(); $xmlElements = dom_import_simplexml($xml->creators); $xmlElements = $xmlElements->getElementsByTagName('creator'); Zotero_DB::query("SET foreign_key_checks = 0"); try { $addedLibraryIDs = array(); $addedCreatorDataHashes = array(); foreach ($xmlElements as $xmlElement) { $key = $xmlElement->getAttribute('key'); if (isset($keys[$key])) { throw new Exception("Creator {$key} already processed"); } $keys[$key] = true; $creatorObj = Zotero_Creators::convertXMLToCreator($xmlElement); $addedLibraryIDs[] = $creatorObj->libraryID; $creatorObj->save(); } } catch (Exception $e) { Zotero_DB::query("SET foreign_key_checks = 1"); throw $e; } Zotero_DB::query("SET foreign_key_checks = 1"); unset($keys); unset($xml->creators); // // Manual foreign key checks // // libraryID foreach ($addedLibraryIDs as $addedLibraryID) { $shardID = Zotero_Shards::getByLibraryID($addedLibraryID); $sql = "SELECT COUNT(*) FROM shardLibraries WHERE libraryID=?"; if (!Zotero_DB::valueQuery($sql, $addedLibraryID, $shardID)) { throw new Exception("libraryID inserted into `creators` not found in `shardLibraries` ({$addedLibraryID}, {$shardID})"); } } } // Add/update items if ($xml->items) { $childItems = array(); $relatedItemsStore = array(); // DOM $keys = array(); $xmlElements = dom_import_simplexml($xml->items); $xmlElements = $xmlElements->getElementsByTagName('item'); foreach ($xmlElements as $xmlElement) { $key = $xmlElement->getAttribute('key'); if (isset($keys[$key])) { throw new Exception("Item {$key} already processed"); } $keys[$key] = true; $missing = Zotero_Items::removeMissingRelatedItems($xmlElement); $itemObj = Zotero_Items::convertXMLToItem($xmlElement); if ($missing) { $relatedItemsStore[$itemObj->libraryID . '_' . $itemObj->key] = $missing; } if (!$itemObj->getSourceKey()) { try { $itemObj->save($userID); } catch (Exception $e) { if (strpos($e->getMessage(), 'libraryIDs_do_not_match') !== false) { throw new Exception($e->getMessage() . " (" . $itemObj->key . ")"); } throw $e; } } else { $childItems[] = $itemObj; } } unset($keys); unset($xml->items); while ($childItem = array_shift($childItems)) { $childItem->save($userID); } // Add back related items (which now exist) foreach ($relatedItemsStore as $itemLibraryKey => $relset) { $lk = explode('_', $itemLibraryKey); $libraryID = $lk[0]; $key = $lk[1]; $item = Zotero_Items::getByLibraryAndKey($libraryID, $key); foreach ($relset as $relKey) { $relItem = Zotero_Items::getByLibraryAndKey($libraryID, $relKey); $item->addRelatedItem($relItem->id); } $item->save(); } unset($relatedItemsStore); } // Add/update collections if ($xml->collections) { $collections = array(); $collectionSets = array(); // DOM // Build an array of unsaved collection objects and the keys of child items $keys = array(); $xmlElements = dom_import_simplexml($xml->collections); $xmlElements = $xmlElements->getElementsByTagName('collection'); foreach ($xmlElements as $xmlElement) { $key = $xmlElement->getAttribute('key'); if (isset($keys[$key])) { throw new Exception("Collection {$key} already processed"); } $keys[$key] = true; $collectionObj = Zotero_Collections::convertXMLToCollection($xmlElement); $xmlItems = $xmlElement->getElementsByTagName('items')->item(0); // Fix an error if there's leading or trailing whitespace, // which was possible in 2.0.3 if ($xmlItems) { $xmlItems = trim($xmlItems->nodeValue); } $arr = array('obj' => $collectionObj, 'items' => $xmlItems ? explode(' ', $xmlItems) : array()); $collections[] = $collectionObj; $collectionSets[] = $arr; } unset($keys); unset($xml->collections); self::saveCollections($collections); unset($collections); // Set child items foreach ($collectionSets as $collection) { // Child items if (isset($collection['items'])) { $ids = array(); foreach ($collection['items'] as $key) { $item = Zotero_Items::getByLibraryAndKey($collection['obj']->libraryID, $key); if (!$item) { throw new Exception("Child item '{$key}' of collection {$collection['obj']->id} not found", Z_ERROR_ITEM_NOT_FOUND); } $ids[] = $item->id; } $collection['obj']->setChildItems($ids); } } unset($collectionSets); } // Add/update saved searches if ($xml->searches) { $searches = array(); $keys = array(); foreach ($xml->searches->search as $xmlElement) { $key = (string) $xmlElement['key']; if (isset($keys[$key])) { throw new Exception("Search {$key} already processed"); } $keys[$key] = true; $searchObj = Zotero_Searches::convertXMLToSearch($xmlElement); $searchObj->save(); } unset($xml->searches); } // Add/update tags if ($xml->tags) { $keys = array(); // DOM $xmlElements = dom_import_simplexml($xml->tags); $xmlElements = $xmlElements->getElementsByTagName('tag'); foreach ($xmlElements as $xmlElement) { $key = $xmlElement->getAttribute('key'); if (isset($keys[$key])) { throw new Exception("Tag {$key} already processed"); } $keys[$key] = true; $tagObj = Zotero_Tags::convertXMLToTag($xmlElement); $tagObj->save(true); } unset($keys); unset($xml->tags); } // Add/update relations if ($xml->relations) { // DOM $xmlElements = dom_import_simplexml($xml->relations); $xmlElements = $xmlElements->getElementsByTagName('relation'); foreach ($xmlElements as $xmlElement) { $relationObj = Zotero_Relations::convertXMLToRelation($xmlElement, $userLibraryID); if ($relationObj->exists()) { continue; } $relationObj->save(); } unset($keys); unset($xml->relations); } // TODO: loop if ($xml->deleted) { // Delete collections if ($xml->deleted->collections) { Zotero_Collections::deleteFromXML($xml->deleted->collections); } // Delete items if ($xml->deleted->items) { Zotero_Items::deleteFromXML($xml->deleted->items); } // Delete creators if ($xml->deleted->creators) { Zotero_Creators::deleteFromXML($xml->deleted->creators); } // Delete saved searches if ($xml->deleted->searches) { Zotero_Searches::deleteFromXML($xml->deleted->searches); } // Delete tags if ($xml->deleted->tags) { Zotero_Tags::deleteFromXML($xml->deleted->tags); } // Delete tags if ($xml->deleted->relations) { Zotero_Relations::deleteFromXML($xml->deleted->relations); } } self::removeUploadProcess($processID); Zotero_DB::commit(); Z_Core::$MC->commit(); if ($profile) { $shardID = Zotero_Shards::getByUserID($userID); Zotero_DB::profileEnd($shardID); } // Return timestamp + 1, to keep the next /updated call // (using >= timestamp) from returning this data return $timestamp + 1; } catch (Exception $e) { Z_Core::$MC->rollback(); Zotero_DB::rollback(true); self::removeUploadProcess($processID); throw $e; } }
public static function search($libraryID, $onlyTopLevel = false, $params = array(), $includeTrashed = false, $asKeys = false) { $rnd = "_" . uniqid($libraryID . "_"); if ($asKeys) { $results = array('keys' => array(), 'total' => 0); } else { $results = array('items' => array(), 'total' => 0); } $shardID = Zotero_Shards::getByLibraryID($libraryID); $itemIDs = array(); $keys = array(); $deleteTempTable = array(); // Pass a list of itemIDs, for when the initial search is done via SQL if (!empty($params['itemIDs'])) { $itemIDs = $params['itemIDs']; } if (!empty($params['itemKey'])) { $keys = explode(',', $params['itemKey']); } $titleSort = !empty($params['order']) && $params['order'] == 'title'; $sql = "SELECT SQL_CALC_FOUND_ROWS DISTINCT " . ($asKeys ? "I.key" : "I.itemID") . " FROM items I "; $sqlParams = array($libraryID); if (!empty($params['q']) || $titleSort) { $titleFieldIDs = array_merge(array(Zotero_ItemFields::getID('title')), Zotero_ItemFields::getTypeFieldsFromBase('title')); $sql .= "LEFT JOIN itemData IDT ON (IDT.itemID=I.itemID AND IDT.fieldID IN (" . implode(',', $titleFieldIDs) . ")) "; } if (!empty($params['q'])) { $sql .= "LEFT JOIN itemCreators IC ON (IC.itemID=I.itemID)\n\t\t\t\t\tLEFT JOIN creators C ON (C.creatorID=IC.creatorID) "; } if ($onlyTopLevel || !empty($params['q']) || $titleSort) { $sql .= "LEFT JOIN itemNotes INo ON (INo.itemID=I.itemID) "; } if ($onlyTopLevel) { $sql .= "LEFT JOIN itemAttachments IA ON (IA.itemID=I.itemID) "; } if (!$includeTrashed) { $sql .= "LEFT JOIN deletedItems DI ON (DI.itemID=I.itemID) "; } if (!empty($params['order'])) { switch ($params['order']) { case 'title': case 'creator': $sql .= "LEFT JOIN itemSortFields ISF ON (ISF.itemID=I.itemID) "; break; case 'date': $dateFieldIDs = array_merge(array(Zotero_ItemFields::getID('date')), Zotero_ItemFields::getTypeFieldsFromBase('date')); $sql .= "LEFT JOIN itemData IDD ON (IDD.itemID=I.itemID AND IDD.fieldID IN (" . implode(',', $dateFieldIDs) . ")) "; break; case 'itemType': // Create temporary table to store item type names // // We use IF NOT EXISTS just to make sure there are // no problems with restoration from the binary log $sql2 = "CREATE TEMPORARY TABLE IF NOT EXISTS tmpItemTypeNames{$rnd}\n\t\t\t\t\t\t\t(itemTypeID SMALLINT UNSIGNED NOT NULL,\n\t\t\t\t\t\t\titemTypeName VARCHAR(255) NOT NULL,\n\t\t\t\t\t\t\tPRIMARY KEY (itemTypeID),\n\t\t\t\t\t\t\tINDEX (itemTypeName))"; Zotero_DB::query($sql2, false, $shardID); $deleteTempTable['tmpItemTypeNames'] = true; $types = Zotero_ItemTypes::getAll('en-US'); foreach ($types as $type) { $sql2 = "INSERT INTO tmpItemTypeNames{$rnd} VALUES (?, ?)"; Zotero_DB::query($sql2, array($type['id'], $type['localized']), $shardID); } // Join temp table to query $sql .= "JOIN tmpItemTypeNames{$rnd} TITN ON (TITN.itemTypeID=I.itemTypeID) "; break; case 'addedBy': $isGroup = Zotero_Libraries::getType($libraryID) == 'group'; if ($isGroup) { // Create temporary table to store usernames // // We use IF NOT EXISTS just to make sure there are // no problems with restoration from the binary log $sql2 = "CREATE TEMPORARY TABLE IF NOT EXISTS tmpCreatedByUsers{$rnd}\n\t\t\t\t\t\t\t\t(userID INT UNSIGNED NOT NULL,\n\t\t\t\t\t\t\t\tusername VARCHAR(255) NOT NULL,\n\t\t\t\t\t\t\t\tPRIMARY KEY (userID),\n\t\t\t\t\t\t\t\tINDEX (username))"; Zotero_DB::query($sql2, false, $shardID); $deleteTempTable['tmpCreatedByUsers'] = true; $sql2 = "SELECT DISTINCT createdByUserID FROM items\n\t\t\t\t\t\t\t\tJOIN groupItems USING (itemID) WHERE\n\t\t\t\t\t\t\t\tcreatedByUserID IS NOT NULL AND "; if ($itemIDs) { $sql2 .= "itemID IN (" . implode(', ', array_fill(0, sizeOf($itemIDs), '?')) . ") "; $createdByUserIDs = Zotero_DB::columnQuery($sql2, $itemIDs, $shardID); } else { $sql2 .= "libraryID=?"; $createdByUserIDs = Zotero_DB::columnQuery($sql2, $libraryID, $shardID); } // Populate temp table with usernames if ($createdByUserIDs) { $toAdd = array(); foreach ($createdByUserIDs as $createdByUserID) { $toAdd[] = array($createdByUserID, Zotero_Users::getUsername($createdByUserID)); } $sql2 = "INSERT IGNORE INTO tmpCreatedByUsers{$rnd} VALUES "; Zotero_DB::bulkInsert($sql2, $toAdd, 50, false, $shardID); // Join temp table to query $sql .= "JOIN groupItems GI ON (GI.itemID=I.itemID)\n\t\t\t\t\t\t\t\t\tJOIN tmpCreatedByUsers{$rnd} TCBU ON (TCBU.userID=GI.createdByUserID) "; } } break; } } $sql .= "WHERE I.libraryID=? "; if ($onlyTopLevel) { $sql .= "AND INo.sourceItemID IS NULL AND IA.sourceItemID IS NULL "; } if (!$includeTrashed) { $sql .= "AND DI.itemID IS NULL "; } // Search on title and creators if (!empty($params['q'])) { $sql .= "AND ("; $sql .= "IDT.value LIKE ? "; $sqlParams[] = '%' . $params['q'] . '%'; $sql .= "OR title LIKE ? "; $sqlParams[] = '%' . $params['q'] . '%'; $sql .= "OR TRIM(CONCAT(firstName, ' ', lastName)) LIKE ?"; $sqlParams[] = '%' . $params['q'] . '%'; $sql .= ") "; } // Search on itemType if (!empty($params['itemType'])) { $itemTypes = Zotero_API::getSearchParamValues($params, 'itemType'); if ($itemTypes) { if (sizeOf($itemTypes) > 1) { throw new Exception("Cannot specify 'itemType' more than once", Z_ERROR_INVALID_INPUT); } $itemTypes = $itemTypes[0]; $itemTypeIDs = array(); foreach ($itemTypes['values'] as $itemType) { $itemTypeID = Zotero_ItemTypes::getID($itemType); if (!$itemTypeID) { throw new Exception("Invalid itemType '{$itemType}'", Z_ERROR_INVALID_INPUT); } $itemTypeIDs[] = $itemTypeID; } $sql .= "AND I.itemTypeID " . ($itemTypes['negation'] ? "NOT " : "") . "IN (" . implode(',', array_fill(0, sizeOf($itemTypeIDs), '?')) . ") "; $sqlParams = array_merge($sqlParams, $itemTypeIDs); } } // Tags // // ?tag=foo // ?tag=foo bar // phrase // ?tag=-foo // negation // ?tag=\-foo // literal hyphen (only for first character) // ?tag=foo&tag=bar // AND // ?tag=foo&tagType=0 // ?tag=foo bar || bar&tagType=0 $tagSets = Zotero_API::getSearchParamValues($params, 'tag'); if ($tagSets) { $sql2 = "SELECT itemID FROM items WHERE 1 "; $sqlParams2 = array(); if ($tagSets) { foreach ($tagSets as $set) { $positives = array(); $negatives = array(); $tagIDs = array(); foreach ($set['values'] as $tag) { $ids = Zotero_Tags::getIDs($libraryID, $tag); if (!$ids) { $ids = array(0); } $tagIDs = array_merge($tagIDs, $ids); } $tagIDs = array_unique($tagIDs); if ($set['negation']) { $negatives = array_merge($negatives, $tagIDs); } else { $positives = array_merge($positives, $tagIDs); } if ($positives) { $sql2 .= "AND itemID IN (SELECT itemID FROM items JOIN itemTags USING (itemID)\n\t\t\t\t\t\t\t\tWHERE tagID IN (" . implode(',', array_fill(0, sizeOf($positives), '?')) . ")) "; $sqlParams2 = array_merge($sqlParams2, $positives); } if ($negatives) { $sql2 .= "AND itemID NOT IN (SELECT itemID FROM items JOIN itemTags USING (itemID)\n\t\t\t\t\t\t\t\tWHERE tagID IN (" . implode(',', array_fill(0, sizeOf($negatives), '?')) . ")) "; $sqlParams2 = array_merge($sqlParams2, $negatives); } } } $tagItems = Zotero_DB::columnQuery($sql2, $sqlParams2, $shardID); // No matches if (!$tagItems) { return $results; } // Combine with passed keys if ($itemIDs) { $itemIDs = array_intersect($itemIDs, $tagItems); // None of the tag matches match the passed keys if (!$itemIDs) { return $results; } } else { $itemIDs = $tagItems; } } if ($itemIDs) { $sql .= "AND I.itemID IN (" . implode(', ', array_fill(0, sizeOf($itemIDs), '?')) . ") "; $sqlParams = array_merge($sqlParams, $itemIDs); } if ($keys) { $sql .= "AND `key` IN (" . implode(', ', array_fill(0, sizeOf($keys), '?')) . ") "; $sqlParams = array_merge($sqlParams, $keys); } $sql .= "ORDER BY "; if (!empty($params['order'])) { switch ($params['order']) { case 'dateAdded': case 'dateModified': case 'serverDateModified': $orderSQL = "I." . $params['order']; break; case 'itemType': $orderSQL = "TITN.itemTypeName"; break; case 'title': $orderSQL = "IFNULL(COALESCE(sortTitle, IDT.value, INo.title), '')"; break; case 'creator': $orderSQL = "ISF.creatorSummary"; break; // TODO: generic base field mapping-aware sorting // TODO: generic base field mapping-aware sorting case 'date': $orderSQL = "IDD.value"; break; case 'addedBy': if ($isGroup && $createdByUserIDs) { $orderSQL = "TCBU.username"; } else { $orderSQL = "1"; } break; default: $fieldID = Zotero_ItemFields::getID($params['order']); if (!$fieldID) { throw new Exception("Invalid order field '" . $params['order'] . "'"); } $orderSQL = "(SELECT value FROM itemData WHERE itemID=I.itemID AND fieldID=?)"; if (!$params['emptyFirst']) { $sqlParams[] = $fieldID; } $sqlParams[] = $fieldID; } if (!empty($params['sort'])) { $dir = $params['sort']; } else { $dir = "ASC"; } if (!$params['emptyFirst']) { $sql .= "IFNULL({$orderSQL}, '') = '' {$dir}, "; } $sql .= $orderSQL; $sql .= " {$dir}, "; } $sql .= "I.itemID " . (!empty($params['sort']) ? $params['sort'] : "ASC") . " "; if (!empty($params['limit'])) { $sql .= "LIMIT ?, ?"; $sqlParams[] = $params['start'] ? $params['start'] : 0; $sqlParams[] = $params['limit']; } $itemIDs = Zotero_DB::columnQuery($sql, $sqlParams, $shardID); $results['total'] = Zotero_DB::valueQuery("SELECT FOUND_ROWS()", false, $shardID); if ($itemIDs) { if ($asKeys) { $results['keys'] = $itemIDs; } else { $results['items'] = Zotero_Items::get($libraryID, $itemIDs); } } if (!empty($deleteTempTable['tmpCreatedByUsers'])) { $sql = "DROP TEMPORARY TABLE IF EXISTS tmpCreatedByUsers{$rnd}"; Zotero_DB::query($sql, false, $shardID); } if (!empty($deleteTempTable['tmpItemTypeNames'])) { $sql = "DROP TEMPORARY TABLE IF EXISTS tmpItemTypeNames{$rnd}"; Zotero_DB::query($sql, false, $shardID); } return $results; }
public static function getAllAdvanced($libraryID, $params) { $results = array('objects' => array(), 'total' => 0); $sql = "SELECT SQL_CALC_FOUND_ROWS tagID FROM tags "; if (!empty($params['order']) && $params['order'] == 'numItems') { $sql .= " LEFT JOIN itemTags USING (tagID)"; } $sql .= "WHERE libraryID=? "; $sqlParams = array($libraryID); if (!empty($params['q'])) { if (!is_array($params['q'])) { $params['q'] = array($params['q']); } foreach ($params['q'] as $q) { $sql .= "AND name LIKE ? "; $sqlParams[] = "%{$q}%"; } } $tagTypeSets = Zotero_API::getSearchParamValues($params, 'tagType'); if ($tagTypeSets) { $positives = array(); $negatives = array(); foreach ($tagTypeSets as $set) { if ($set['negation']) { $negatives = array_merge($negatives, $set['values']); } else { $positives = array_merge($positives, $set['values']); } } if ($positives) { $sql .= "AND type IN (" . implode(',', array_fill(0, sizeOf($positives), '?')) . ") "; $sqlParams = array_merge($sqlParams, $positives); } if ($negatives) { $sql .= "AND type NOT IN (" . implode(',', array_fill(0, sizeOf($negatives), '?')) . ") "; $sqlParams = array_merge($sqlParams, $negatives); } } if (!empty($params['order'])) { $order = $params['order']; if ($order == 'title') { // Force a case-insensitive sort $sql .= "ORDER BY name COLLATE utf8_unicode_ci "; } else { if ($order == 'numItems') { $sql .= "GROUP BY tags.tagID ORDER BY COUNT(tags.tagID)"; } else { $sql .= "ORDER BY {$order} "; } } if (!empty($params['sort'])) { $sql .= " " . $params['sort'] . " "; } } if (!empty($params['limit'])) { $sql .= "LIMIT ?, ?"; $sqlParams[] = $params['start'] ? $params['start'] : 0; $sqlParams[] = $params['limit']; } $shardID = Zotero_Shards::getByLibraryID($libraryID); $ids = Zotero_DB::columnQuery($sql, $sqlParams, $shardID); if ($ids) { $results['total'] = Zotero_DB::valueQuery("SELECT FOUND_ROWS()", false, $shardID); $tags = array(); foreach ($ids as $id) { $tags[] = Zotero_Tags::get($libraryID, $id); } $results['objects'] = $tags; } return $results; }
public function items() { // Check for general library access if (!$this->permissions->canAccess($this->objectLibraryID)) { $this->e403(); } if ($this->isWriteMethod()) { // Check for library write access if (!$this->permissions->canWrite($this->objectLibraryID)) { $this->e403("Write access denied"); } // Make sure library hasn't been modified if (!$this->singleObject) { $libraryTimestampChecked = $this->checkLibraryIfUnmodifiedSinceVersion(); } // We don't update the library version in file mode, because currently // to avoid conflicts in the client the timestamp can't change // when the client updates file metadata if (!$this->fileMode) { Zotero_Libraries::updateVersionAndTimestamp($this->objectLibraryID); } } $itemIDs = array(); $itemKeys = array(); $results = array(); $title = ""; // // Single item // if ($this->singleObject) { if ($this->fileMode) { if ($this->fileView) { $this->allowMethods(array('HEAD', 'GET', 'POST')); } else { $this->allowMethods(array('HEAD', 'GET', 'PUT', 'POST', 'PATCH')); } } else { $this->allowMethods(array('HEAD', 'GET', 'PUT', 'PATCH', 'DELETE')); } if (!$this->objectLibraryID || !Zotero_ID::isValidKey($this->objectKey)) { $this->e404(); } $item = Zotero_Items::getByLibraryAndKey($this->objectLibraryID, $this->objectKey); if ($item) { // If no access to the note, don't show that it exists if ($item->isNote() && !$this->permissions->canAccess($this->objectLibraryID, 'notes')) { $this->e404(); } // Make sure URL libraryID matches item libraryID if ($this->objectLibraryID != $item->libraryID) { $this->e404("Item does not exist"); } // File access mode if ($this->fileMode) { $this->_handleFileRequest($item); } if ($this->scopeObject) { switch ($this->scopeObject) { // Remove item from collection case 'collections': $this->allowMethods(array('DELETE')); $collection = Zotero_Collections::getByLibraryAndKey($this->objectLibraryID, $this->scopeObjectKey); if (!$collection) { $this->e404("Collection not found"); } if (!$collection->hasItem($item->id)) { $this->e404("Item not found in collection"); } $collection->removeItem($item->id); $this->e204(); default: $this->e400(); } } } else { // Possibly temporary workaround to block unnecessary full syncs if ($this->fileMode && $this->httpAuth && $this->method == 'POST') { // If > 2 requests for missing file, trigger a full sync via 404 $cacheKey = "apiMissingFile_" . $this->objectLibraryID . "_" . $this->objectKey; $set = Z_Core::$MC->get($cacheKey); if (!$set) { Z_Core::$MC->set($cacheKey, 1, 86400); } else { if ($set < 2) { Z_Core::$MC->increment($cacheKey); } else { Z_Core::$MC->delete($cacheKey); $this->e404("A file sync error occurred. Please sync again."); } } $this->e500("A file sync error occurred. Please sync again."); } } if ($this->isWriteMethod()) { $item = $this->handleObjectWrite('item', $item ? $item : null); if ($this->apiVersion < 2 && ($this->method == 'PUT' || $this->method == 'PATCH')) { $this->queryParams['format'] = 'atom'; $this->queryParams['content'] = ['json']; } } if (!$item) { $this->e404("Item does not exist"); } $this->libraryVersion = $item->version; if ($this->method == 'HEAD') { $this->end(); } // Display item switch ($this->queryParams['format']) { case 'atom': $this->responseXML = Zotero_Items::convertItemToAtom($item, $this->queryParams, $this->permissions); break; case 'bib': echo Zotero_Cite::getBibliographyFromCitationServer(array($item), $this->queryParams); break; case 'csljson': $json = Zotero_Cite::getJSONFromItems(array($item), true); echo Zotero_Utilities::formatJSON($json); break; case 'json': $json = $item->toResponseJSON($this->queryParams, $this->permissions); echo Zotero_Utilities::formatJSON($json); break; default: $export = Zotero_Translate::doExport(array($item), $this->queryParams['format']); $this->queryParams['format'] = null; header("Content-Type: " . $export['mimeType']); echo $export['body']; break; } } else { $this->allowMethods(array('HEAD', 'GET', 'POST', 'DELETE')); $this->libraryVersion = Zotero_Libraries::getUpdatedVersion($this->objectLibraryID); $includeTrashed = $this->queryParams['includeTrashed']; if ($this->scopeObject) { $this->allowMethods(array('GET', 'POST')); switch ($this->scopeObject) { case 'collections': // TEMP if (Zotero_ID::isValidKey($this->scopeObjectKey)) { $collection = Zotero_Collections::getByLibraryAndKey($this->objectLibraryID, $this->scopeObjectKey); } else { $collection = false; } if (!$collection) { // If old collectionID, redirect if ($this->method == 'GET' && Zotero_Utilities::isPosInt($this->scopeObjectKey)) { $collection = Zotero_Collections::get($this->objectLibraryID, $this->scopeObjectKey); if ($collection) { $qs = !empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : ''; $base = Zotero_API::getCollectionURI($collection); $this->redirect($base . "/items" . $qs, 301); } } $this->e404("Collection not found"); } // Add items to collection if ($this->method == 'POST') { $itemKeys = explode(' ', $this->body); $itemIDs = array(); foreach ($itemKeys as $key) { try { $item = Zotero_Items::getByLibraryAndKey($this->objectLibraryID, $key); } catch (Exception $e) { if ($e->getCode() == Z_ERROR_OBJECT_LIBRARY_MISMATCH) { $item = false; } else { throw $e; } } if (!$item) { throw new Exception("Item '{$key}' not found in library", Z_ERROR_INVALID_INPUT); } if ($item->getSource()) { throw new Exception("Child items cannot be added to collections directly", Z_ERROR_INVALID_INPUT); } $itemIDs[] = $item->id; } $collection->addItems($itemIDs); $this->e204(); } if ($this->subset == 'top' || $this->apiVersion < 2) { $title = "Top-Level Items in Collection ‘" . $collection->name . "’"; $itemIDs = $collection->getItems(); } else { $title = "Items in Collection ‘" . $collection->name . "’"; $itemIDs = $collection->getItems(true); } break; case 'tags': if ($this->apiVersion >= 2) { $this->e404(); } $this->allowMethods(array('GET')); $tagIDs = Zotero_Tags::getIDs($this->objectLibraryID, $this->scopeObjectName); if (!$tagIDs) { $this->e404("Tag not found"); } foreach ($tagIDs as $tagID) { $tag = new Zotero_Tag(); $tag->libraryID = $this->objectLibraryID; $tag->id = $tagID; // Use a real tag name, in case case differs if (!$title) { $title = "Items of Tag ‘" . $tag->name . "’"; } $itemKeys = array_merge($itemKeys, $tag->getLinkedItems(true)); } $itemKeys = array_unique($itemKeys); break; default: $this->e404(); } } else { // Top-level items if ($this->subset == 'top') { $this->allowMethods(array('GET')); $title = "Top-Level Items"; $results = Zotero_Items::search($this->objectLibraryID, true, $this->queryParams, $includeTrashed, $this->permissions); } else { if ($this->subset == 'trash') { $this->allowMethods(array('GET')); $title = "Deleted Items"; $this->queryParams['trashedItemsOnly'] = true; $includeTrashed = true; $results = Zotero_Items::search($this->objectLibraryID, false, $this->queryParams, $includeTrashed, $this->permissions); } else { if ($this->subset == 'children') { $item = Zotero_Items::getByLibraryAndKey($this->objectLibraryID, $this->objectKey); if (!$item) { $this->e404("Item not found"); } if ($item->isAttachment()) { $this->e400("/children cannot be called on attachment items"); } if ($item->isNote()) { $this->e400("/children cannot be called on note items"); } if ($item->getSource()) { $this->e400("/children cannot be called on child items"); } // Create new child items if ($this->method == 'POST') { if ($this->apiVersion >= 2) { $this->allowMethods(array('GET')); } Zotero_DB::beginTransaction(); $obj = $this->jsonDecode($this->body); $results = Zotero_Items::updateMultipleFromJSON($obj, $this->queryParams, $this->objectLibraryID, $this->userID, $this->permissions, $libraryTimestampChecked ? 0 : 1, $item); Zotero_DB::commit(); if ($cacheKey = $this->getWriteTokenCacheKey()) { Z_Core::$MC->set($cacheKey, true, $this->writeTokenCacheTime); } $uri = Zotero_API::getItemsURI($this->objectLibraryID); $keys = array_merge(get_object_vars($results['success']), get_object_vars($results['unchanged'])); $queryString = "itemKey=" . urlencode(implode(",", $keys)) . "&format=atom&content=json&order=itemKeyList&sort=asc"; if ($this->apiKey) { $queryString .= "&key=" . $this->apiKey; } $uri .= "?" . $queryString; $this->queryParams = Zotero_API::parseQueryParams($queryString, $this->action, false); $this->responseCode = 201; $title = "Items"; $results = Zotero_Items::search($this->objectLibraryID, false, $this->queryParams, $includeTrashed, $this->permissions); } else { $title = "Child Items of ‘" . $item->getDisplayTitle() . "’"; $notes = $item->getNotes(); $attachments = $item->getAttachments(); $itemIDs = array_merge($notes, $attachments); } } else { // Create new items if ($this->method == 'POST') { $this->queryParams['format'] = 'writereport'; $obj = $this->jsonDecode($this->body); // Server-side translation if (isset($obj->url)) { if ($this->apiVersion == 1) { Zotero_DB::beginTransaction(); } $token = $this->getTranslationToken($obj); $results = Zotero_Items::addFromURL($obj, $this->queryParams, $this->objectLibraryID, $this->userID, $this->permissions, $token); if ($this->apiVersion == 1) { Zotero_DB::commit(); } // Multiple choices if ($results instanceof stdClass) { $this->queryParams['format'] = null; header("Content-Type: application/json"); if ($this->queryParams['v'] >= 2) { echo Zotero_Utilities::formatJSON(['url' => $obj->url, 'token' => $token, 'items' => $results->select]); } else { echo Zotero_Utilities::formatJSON($results->select); } $this->e300(); } else { if (is_int($results)) { switch ($results) { case 501: $this->e501("No translators found for URL"); break; default: $this->e500("Error translating URL"); } } else { if ($this->apiVersion == 1) { $uri = Zotero_API::getItemsURI($this->objectLibraryID); $keys = array_merge(get_object_vars($results['success']), get_object_vars($results['unchanged'])); $queryString = "itemKey=" . urlencode(implode(",", $keys)) . "&format=atom&content=json&order=itemKeyList&sort=asc"; if ($this->apiKey) { $queryString .= "&key=" . $this->apiKey; } $uri .= "?" . $queryString; $this->queryParams = Zotero_API::parseQueryParams($queryString, $this->action, false); $this->responseCode = 201; $title = "Items"; $results = Zotero_Items::search($this->objectLibraryID, false, $this->queryParams, $includeTrashed, $this->permissions); } } } // Otherwise return write status report } else { if ($this->apiVersion < 2) { Zotero_DB::beginTransaction(); } $results = Zotero_Items::updateMultipleFromJSON($obj, $this->queryParams, $this->objectLibraryID, $this->userID, $this->permissions, $libraryTimestampChecked ? 0 : 1, null); if ($this->apiVersion < 2) { Zotero_DB::commit(); $uri = Zotero_API::getItemsURI($this->objectLibraryID); $keys = array_merge(get_object_vars($results['success']), get_object_vars($results['unchanged'])); $queryString = "itemKey=" . urlencode(implode(",", $keys)) . "&format=atom&content=json&order=itemKeyList&sort=asc"; if ($this->apiKey) { $queryString .= "&key=" . $this->apiKey; } $uri .= "?" . $queryString; $this->queryParams = Zotero_API::parseQueryParams($queryString, $this->action, false); $this->responseCode = 201; $title = "Items"; $results = Zotero_Items::search($this->objectLibraryID, false, $this->queryParams, $includeTrashed, $this->permissions); } } if ($cacheKey = $this->getWriteTokenCacheKey()) { Z_Core::$MC->set($cacheKey, true, $this->writeTokenCacheTime); } } else { if ($this->method == 'DELETE') { Zotero_DB::beginTransaction(); foreach ($this->queryParams['itemKey'] as $itemKey) { Zotero_Items::delete($this->objectLibraryID, $itemKey); } Zotero_DB::commit(); $this->e204(); } else { $title = "Items"; $results = Zotero_Items::search($this->objectLibraryID, false, $this->queryParams, $includeTrashed, $this->permissions); } } } } } } if ($itemIDs || $itemKeys) { if ($itemIDs) { $this->queryParams['itemIDs'] = $itemIDs; } if ($itemKeys) { $this->queryParams['itemKey'] = $itemKeys; } $results = Zotero_Items::search($this->objectLibraryID, false, $this->queryParams, $includeTrashed, $this->permissions); } if ($this->queryParams['format'] == 'bib') { $maxBibItems = Zotero_API::MAX_BIBLIOGRAPHY_ITEMS; if ($results['total'] > $maxBibItems) { $this->e413("Cannot generate bibliography with more than {$maxBibItems} items"); } } $this->generateMultiResponse($results, $title); } $this->end(); }
private function loadTags() { if (!$this->id) { return; } Z_Core::debug("Loading tags for item {$this->id}"); if ($this->loaded['tags']) { throw new Exception("Tags already loaded for item {$this->id}"); } $sql = "SELECT tagID FROM itemTags JOIN tags USING (tagID) WHERE itemID=?"; $tagIDs = Zotero_DB::columnQuery($sql, $this->id, Zotero_Shards::getByLibraryID($this->libraryID)); $this->tags = []; if ($tagIDs) { foreach ($tagIDs as $tagID) { $this->tags[] = Zotero_Tags::get($this->libraryID, $tagID, true); } } $this->loaded['tags'] = true; }
public function tags() { $this->allowMethods(['HEAD', 'GET', 'DELETE']); if (!$this->permissions->canAccess($this->objectLibraryID)) { $this->e403(); } if ($this->isWriteMethod()) { // Check for library write access if (!$this->permissions->canWrite($this->objectLibraryID)) { $this->e403("Write access denied"); } // Make sure library hasn't been modified $this->checkLibraryIfUnmodifiedSinceVersion(true); Zotero_Libraries::updateVersionAndTimestamp($this->objectLibraryID); } $tagIDs = array(); $results = array(); $name = $this->objectName; $fixedValues = array(); $this->libraryVersion = Zotero_Libraries::getUpdatedVersion($this->objectLibraryID); // Set of tags matching name if ($name && $this->subset != 'tags') { $this->allowMethods(array('GET')); $tagIDs = Zotero_Tags::getIDs($this->objectLibraryID, $name); if (!$tagIDs) { $this->e404(); } $title = "Tags matching ‘" . $name . "’"; } else { $this->allowMethods(array('GET', 'DELETE')); if ($this->scopeObject) { $this->allowMethods(array('GET')); switch ($this->scopeObject) { case 'collections': $collection = Zotero_Collections::getByLibraryAndKey($this->objectLibraryID, $this->scopeObjectKey); if (!$collection) { $this->e404(); } $title = "Tags in Collection ‘" . $collection->name . "’"; $counts = $collection->getTagItemCounts(); $tagIDs = array(); if ($counts) { foreach ($counts as $tagID => $count) { $tagIDs[] = $tagID; $fixedValues[$tagID] = array('numItems' => $count); } } break; case 'items': $item = Zotero_Items::getByLibraryAndKey($this->objectLibraryID, $this->scopeObjectKey); if (!$item) { $this->e404(); } $title = "Tags of '" . $item->getDisplayTitle() . "'"; $tagIDs = $item->getTags(true); break; default: throw new Exception("Invalid tags scope object '{$this->scopeObject}'"); } } else { if ($this->method == 'DELETE') { // Filter for specific tags with "?tag=foo || bar" $tagNames = !empty($this->queryParams['tag']) ? explode(' || ', $this->queryParams['tag']) : array(); Zotero_DB::beginTransaction(); foreach ($tagNames as $tagName) { $tagIDs = Zotero_Tags::getIDs($this->objectLibraryID, $tagName); foreach ($tagIDs as $tagID) { $tag = Zotero_Tags::get($this->objectLibraryID, $tagID, true); Zotero_Tags::delete($this->objectLibraryID, $tag->key); } } Zotero_DB::commit(); $this->e204(); } else { $title = "Tags"; $results = Zotero_Tags::search($this->objectLibraryID, $this->queryParams); } } } if ($tagIDs) { $this->queryParams['tagIDs'] = $tagIDs; $results = Zotero_Tags::search($this->objectLibraryID, $this->queryParams); } $this->generateMultiResponse($results, $title, $fixedValues); $this->end(); }