예제 #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;
 }
예제 #2
0
 public static function delete($libraryID, $key)
 {
     $table = self::$table;
     $type = self::$objectType;
     $types = self::$objectTypePlural;
     if (!$key) {
         throw new Exception("Invalid key {$key}");
     }
     // Get object (and trigger caching)
     $obj = self::getByLibraryAndKey($libraryID, $key);
     if (!$obj) {
         return;
     }
     self::editCheck($obj);
     Z_Core::debug("Deleting {$type} {$libraryID}/{$key}", 4);
     $shardID = Zotero_Shards::getByLibraryID($libraryID);
     Zotero_DB::beginTransaction();
     // Delete child items
     if ($type == 'item') {
         if ($obj->isRegularItem()) {
             $children = array_merge($obj->getNotes(), $obj->getAttachments());
             if ($children) {
                 $children = Zotero_Items::get($libraryID, $children);
                 foreach ($children as $child) {
                     self::delete($child->libraryID, $child->key);
                 }
             }
         }
         // Remove relations (except for merge tracker)
         $uri = Zotero_URI::getItemURI($obj);
         Zotero_Relations::eraseByURI($libraryID, $uri, array(Zotero_Relations::$deletedItemPredicate));
     } else {
         if ($type == 'tag') {
             $tagName = $obj->name;
         }
     }
     if ($type == 'item' && $obj->isAttachment()) {
         Zotero_FullText::deleteItemContent($obj);
     }
     $sql = "DELETE FROM {$table} WHERE libraryID=? AND `key`=?";
     $deleted = Zotero_DB::query($sql, array($libraryID, $key), $shardID);
     self::unload($obj->id);
     if ($deleted) {
         $sql = "INSERT INTO syncDeleteLogKeys\n\t\t\t\t\t\t(libraryID, objectType, `key`, timestamp, version)\n\t\t\t\t\t\tVALUES (?, '{$type}', ?, ?, ?)\n\t\t\t\t\t\tON DUPLICATE KEY UPDATE timestamp=?, version=?";
         $timestamp = Zotero_DB::getTransactionTimestamp();
         $version = Zotero_Libraries::getUpdatedVersion($libraryID);
         $params = array($libraryID, $key, $timestamp, $version, $timestamp, $version);
         Zotero_DB::query($sql, $params, $shardID);
         if ($type == 'tag') {
             $sql = "INSERT INTO syncDeleteLogKeys\n\t\t\t\t\t\t\t(libraryID, objectType, `key`, timestamp, version)\n\t\t\t\t\t\t\tVALUES (?, 'tagName', ?, ?, ?)\n\t\t\t\t\t\t\tON DUPLICATE KEY UPDATE timestamp=?, version=?";
             $params = array($libraryID, $tagName, $timestamp, $version, $timestamp, $version);
             Zotero_DB::query($sql, $params, $shardID);
         }
     }
     Zotero_DB::commit();
 }
예제 #3
0
 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;
     }
 }
예제 #4
0
 public function itemContent()
 {
     $this->allowMethods(array('GET', 'PUT'));
     // Check for general library access
     if (!$this->permissions->canAccess($this->objectLibraryID)) {
         $this->e403();
     }
     if (!$this->singleObject) {
         $this->e404();
     }
     if ($this->isWriteMethod()) {
         // Check for library write access
         if (!$this->permissions->canWrite($this->objectLibraryID)) {
             $this->e403("Write access denied");
         }
         Zotero_Libraries::updateVersionAndTimestamp($this->objectLibraryID);
     }
     $item = Zotero_Items::getByLibraryAndKey($this->objectLibraryID, $this->objectKey);
     if (!$item) {
         $this->e404();
     }
     // If no access to the note, don't show that it exists
     if ($item->isNote() && !$this->permissions->canAccess($this->objectLibraryID, 'notes')) {
         $this->e404();
     }
     if (!$item->isAttachment() || Zotero_Attachments::linkModeNumberToName($item->attachmentLinkMode) == 'LINKED_URL') {
         $this->e404();
     }
     if ($this->isWriteMethod()) {
         $this->libraryVersion = Zotero_Libraries::getUpdatedVersion($this->objectLibraryID);
         if ($this->method == 'PUT') {
             $this->requireContentType("application/json");
             $json = json_decode($this->body, true);
             if (!$json) {
                 $this->e400("PUT data is not valid JSON");
             }
             $stats = [];
             foreach (Zotero_FullText::$metadata as $prop) {
                 if (isset($json[$prop])) {
                     $stats[$prop] = $json[$prop];
                 }
             }
             Zotero_FullText::indexItem($item, $json['content'], $stats);
             $this->e204();
         } else {
             $this->e405();
         }
     }
     $data = Zotero_FullText::getItemData($item->libraryID, $item->key);
     if (!$data) {
         $this->e404();
     }
     $this->libraryVersion = $data['version'];
     $json = ["content" => $data['content']];
     foreach (Zotero_FullText::$metadata as $prop) {
         if (!empty($data[$prop])) {
             $json[$prop] = $data[$prop];
         }
     }
     echo Zotero_Utilities::formatJSON($json);
     $this->end();
 }
예제 #5
0
 public static function clearAllData($libraryID)
 {
     if (empty($libraryID)) {
         throw new Exception("libraryID not provided");
     }
     Zotero_DB::beginTransaction();
     $tables = array('collections', 'creators', 'items', 'relations', 'savedSearches', 'tags', 'syncDeleteLogIDs', 'syncDeleteLogKeys', 'settings');
     $shardID = Zotero_Shards::getByLibraryID($libraryID);
     self::deleteCachedData($libraryID);
     // Because of the foreign key constraint on the itemID, delete MySQL full-text rows
     // first, and then clear from Elasticsearch below
     Zotero_FullText::deleteByLibraryMySQL($libraryID);
     foreach ($tables as $table) {
         // Delete notes and attachments first (since they may be child items)
         if ($table == 'items') {
             $sql = "DELETE FROM {$table} WHERE libraryID=? AND itemTypeID IN (1,14)";
             Zotero_DB::query($sql, $libraryID, $shardID);
         }
         $sql = "DELETE FROM {$table} WHERE libraryID=?";
         Zotero_DB::query($sql, $libraryID, $shardID);
     }
     Zotero_FullText::deleteByLibrary($libraryID);
     self::updateVersion($libraryID);
     self::updateTimestamps($libraryID);
     Zotero_Notifier::trigger("clear", "library", $libraryID);
     Zotero_DB::commit();
 }