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'); }
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; }
public function save($full = false) { if (!$this->libraryID) { trigger_error("Library ID must be set before saving", E_USER_ERROR); } Zotero_Tags::editCheck($this); if (!$this->changed) { Z_Core::debug("Tag {$this->id} has not changed"); return false; } $shardID = Zotero_Shards::getByLibraryID($this->libraryID); Zotero_DB::beginTransaction(); try { $tagID = $this->id ? $this->id : Zotero_ID::get('tags'); $isNew = !$this->id; Z_Core::debug("Saving tag {$tagID}"); $key = $this->key ? $this->key : $this->generateKey(); $timestamp = Zotero_DB::getTransactionTimestamp(); $dateAdded = $this->dateAdded ? $this->dateAdded : $timestamp; $dateModified = $this->dateModified ? $this->dateModified : $timestamp; $fields = "name=?, `type`=?, dateAdded=?, dateModified=?,\n\t\t\t\tlibraryID=?, `key`=?, serverDateModified=?"; $params = array($this->name, $this->type ? $this->type : 0, $dateAdded, $dateModified, $this->libraryID, $key, $timestamp); try { if ($isNew) { $sql = "INSERT INTO tags SET tagID=?, {$fields}"; $stmt = Zotero_DB::getStatement($sql, true, $shardID); Zotero_DB::queryFromStatement($stmt, array_merge(array($tagID), $params)); // Remove from delete log if it's there $sql = "DELETE FROM syncDeleteLogKeys WHERE libraryID=? AND objectType='tag' AND `key`=?"; Zotero_DB::query($sql, array($this->libraryID, $key), $shardID); } else { $sql = "UPDATE tags SET {$fields} WHERE tagID=?"; $stmt = Zotero_DB::getStatement($sql, true, Zotero_Shards::getByLibraryID($this->libraryID)); Zotero_DB::queryFromStatement($stmt, array_merge($params, array($tagID))); } } catch (Exception $e) { // If an incoming tag is the same as an existing tag, but with a different key, // then delete the old tag and add its linked items to the new tag if (preg_match("/Duplicate entry .+ for key 'uniqueTags'/", $e->getMessage())) { // GET existing tag $existing = Zotero_Tags::getIDs($this->libraryID, $this->name); if (!$existing) { throw new Exception("Existing tag not found"); } foreach ($existing as $id) { $tag = Zotero_Tags::get($this->libraryID, $id, true); if ($tag->__get('type') == $this->type) { $linked = $tag->getLinkedItems(true); Zotero_Tags::delete($this->libraryID, $tag->key); break; } } // Save again if ($isNew) { $sql = "INSERT INTO tags SET tagID=?, {$fields}"; $stmt = Zotero_DB::getStatement($sql, true, $shardID); Zotero_DB::queryFromStatement($stmt, array_merge(array($tagID), $params)); // Remove from delete log if it's there $sql = "DELETE FROM syncDeleteLogKeys WHERE libraryID=? AND objectType='tag' AND `key`=?"; Zotero_DB::query($sql, array($this->libraryID, $key), $shardID); } else { $sql = "UPDATE tags SET {$fields} WHERE tagID=?"; $stmt = Zotero_DB::getStatement($sql, true, Zotero_Shards::getByLibraryID($this->libraryID)); Zotero_DB::queryFromStatement($stmt, array_merge($params, array($tagID))); } $new = array_unique(array_merge($linked, $this->getLinkedItems(true))); $this->setLinkedItems($new); } else { throw $e; } } // Linked items if ($full || !empty($this->changed['linkedItems'])) { $removed = array(); $newids = array(); $currentIDs = $this->getLinkedItems(true); if (!$currentIDs) { $currentIDs = array(); } if ($full) { $sql = "SELECT itemID FROM itemTags WHERE tagID=?"; $stmt = Zotero_DB::getStatement($sql, true, $shardID); $dbItemIDs = Zotero_DB::columnQueryFromStatement($stmt, $tagID); if ($dbItemIDs) { $removed = array_diff($dbItemIDs, $currentIDs); $newids = array_diff($currentIDs, $dbItemIDs); } else { $newids = $currentIDs; } } else { if ($this->previousData['linkedItems']) { $removed = array_diff($this->previousData['linkedItems'], $currentIDs); $newids = array_diff($currentIDs, $this->previousData['linkedItems']); } else { $newids = $currentIDs; } } if ($removed) { $sql = "DELETE FROM itemTags WHERE tagID=? AND itemID IN ("; $q = array_fill(0, sizeOf($removed), '?'); $sql .= implode(', ', $q) . ")"; Zotero_DB::query($sql, array_merge(array($this->id), $removed), $shardID); } if ($newids) { $newids = array_values($newids); $sql = "INSERT INTO itemTags (tagID, itemID) VALUES "; $maxInsertGroups = 50; Zotero_DB::bulkInsert($sql, $newids, $maxInsertGroups, $tagID, $shardID); } //Zotero.Notifier.trigger('add', 'collection-item', $this->id . '-' . $itemID); } Zotero_DB::commit(); Zotero_Tags::cachePrimaryData(array('id' => $tagID, 'libraryID' => $this->libraryID, 'key' => $key, 'name' => $this->name, 'type' => $this->type ? $this->type : 0, 'dateAdded' => $dateAdded, 'dateModified' => $dateModified)); } catch (Exception $e) { Zotero_DB::rollback(); throw $e; } // If successful, set values in object if (!$this->id) { $this->id = $tagID; } if (!$this->key) { $this->key = $key; } $this->init(); if ($isNew) { Zotero_Tags::cache($this); Zotero_Tags::cacheLibraryKeyID($this->libraryID, $key, $tagID); } return $this->id; }
/** * $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; }
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(); }
/** * 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; }
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; }
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(); }