Esempio n. 1
0
 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;
 }
Esempio n. 2
0
 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;
 }
Esempio n. 3
0
 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;
 }
Esempio n. 4
0
 public static function copyLibrary($libraryID, $newShardID, $overrideLock = false)
 {
     $currentShardID = self::getByLibraryID($libraryID);
     if ($currentShardID == $newShardID) {
         throw new Exception("Library {$libraryID} is already on shard {$newShardID}");
     }
     if (!self::shardIsWriteable($newShardID)) {
         throw new Exception("Shard {$newShardID} is not writeable");
     }
     if (!$overrideLock && Zotero_Libraries::isLocked($libraryID)) {
         throw new Exception("Library {$libraryID} is locked");
     }
     // Make sure there's no stale data on the new shard
     if (self::checkForLibrary($libraryID, $newShardID)) {
         throw new Exception("Library {$libraryID} data already exists on shard {$newShardID}");
     }
     Zotero_DB::beginTransaction();
     Zotero_DB::query("SET foreign_key_checks=0", false, $newShardID);
     $tables = array('shardLibraries', 'collections', 'creators', 'items', 'relations', 'savedSearches', 'tags', 'collectionItems', 'deletedItems', 'groupItems', 'itemAttachments', 'itemCreators', 'itemData', 'itemNotes', 'itemRelated', 'itemSortFields', 'itemTags', 'savedSearchConditions', 'storageFileItems', 'syncDeleteLogIDs', 'syncDeleteLogKeys');
     foreach ($tables as $table) {
         if (!$overrideLock && Zotero_Libraries::isLocked($libraryID)) {
             Zotero_DB::rollback();
             throw new Exception("Aborted due to library lock");
         }
         switch ($table) {
             case 'collections':
             case 'creators':
             case 'items':
             case 'relations':
             case 'savedSearches':
             case 'shardLibraries':
             case 'syncDeleteLogIDs':
             case 'syncDeleteLogKeys':
             case 'tags':
                 $sql = "SELECT * FROM {$table} WHERE libraryID=?";
                 break;
             case 'collectionItems':
                 $sql = "SELECT CI.* FROM collectionItems CI\n\t\t\t\t\t\t\tJOIN collections USING (collectionID) WHERE libraryID=?";
                 break;
             case 'deletedItems':
             case 'groupItems':
             case 'itemAttachments':
             case 'itemCreators':
             case 'itemData':
             case 'itemNotes':
             case 'itemRelated':
             case 'itemSortFields':
             case 'itemTags':
             case 'storageFileItems':
                 $sql = "SELECT T.* FROM {$table} T JOIN items USING (itemID) WHERE libraryID=?";
                 break;
             case 'savedSearchConditions':
                 $sql = "SELECT SSC.* FROM savedSearchConditions SSC\n\t\t\t\t\t\t\tJOIN savedSearches USING (searchID) WHERE libraryID=?";
                 break;
         }
         $rows = Zotero_DB::query($sql, $libraryID, $currentShardID);
         if ($rows) {
             $sets = array();
             foreach ($rows as $row) {
                 $sets[] = array_values($row);
             }
             $sql = "INSERT INTO {$table} VALUES ";
             Zotero_DB::bulkInsert($sql, $sets, 50, false, $newShardID);
         }
     }
     Zotero_DB::query("SET foreign_key_checks=1", false, $newShardID);
     if (!$overrideLock && Zotero_Libraries::isLocked($libraryID)) {
         Zotero_DB::rollback();
         throw new Exception("Aborted due to library lock");
     }
     Zotero_DB::commit();
     if (!$overrideLock && Zotero_Libraries::isLocked($libraryID)) {
         self::deleteLibrary($libraryID, $newShardID);
         throw new Exception("Aborted due to library lock");
     }
 }
Esempio n. 5
0
 public function testBulkInsert()
 {
     Zotero_DB::query("CREATE TABLE test (foo INTEGER, foo2 INTEGER)");
     $sql = "INSERT INTO test VALUES ";
     $sets = array(array(1, 2), array(2, 3), array(3, 4), array(4, 5), array(5, 6), array(6, 7));
     // Different maxInsertGroups values
     for ($i = 1; $i < 8; $i++) {
         Zotero_DB::bulkInsert($sql, $sets, $i);
         $rows = Zotero_DB::query("SELECT * FROM test");
         $this->assertEquals(sizeOf($rows), sizeOf($sets));
         $rowVals = array();
         foreach ($rows as $row) {
             $rowVals[] = array($row['foo'], $row['foo2']);
         }
         $this->assertEquals($rowVals, $sets);
         Zotero_DB::query("DELETE FROM test");
     }
     // First val
     $sets2 = array();
     $sets2Comp = array();
     foreach ($sets as $set) {
         $sets2[] = $set[1];
         $sets2Comp[] = array(1, $set[1]);
     }
     Zotero_DB::bulkInsert($sql, $sets2, 2, 1);
     $rows = Zotero_DB::query("SELECT * FROM test");
     $this->assertEquals(sizeOf($rows), sizeOf($sets2Comp));
     $rowVals = array();
     foreach ($rows as $row) {
         $rowVals[] = array($row['foo'], $row['foo2']);
     }
     $this->assertEquals($rowVals, $sets2Comp);
 }