public function toResponseJSON($requestParams=[], Zotero_Permissions $permissions, $sharedData=null) { $t = microtime(true); if (!$this->loaded['primaryData']) { $this->loadPrimaryData(); } if (!$this->loaded['itemData']) { $this->loadItemData(); } // Uncached stuff or parts of the cache key $version = $this->version; $parent = $this->getSource(); $isRegularItem = !$parent && $this->isRegularItem(); $downloadDetails = $permissions->canAccess($this->libraryID, 'files') ? Zotero_Storage::getDownloadDetails($this) : false; if ($isRegularItem) { $numChildren = $permissions->canAccess($this->libraryID, 'notes') ? $this->numChildren() : $this->numAttachments(); } $libraryType = Zotero_Libraries::getType($this->libraryID); // Any query parameters that have an effect on the output // need to be added here $allowedParams = [ 'include', 'style', 'css', 'linkwrap' ]; $cachedParams = Z_Array::filterKeys($requestParams, $allowedParams); $cacheVersion = 1; $cacheKey = "jsonEntry_" . $this->libraryID . "/" . $this->id . "_" . md5( $version . json_encode($cachedParams) . ($downloadDetails ? 'hasFile' : '') // For groups, include the group WWW URL, which can change . ($libraryType == 'group' ? Zotero_URI::getItemURI($this, true) : '') ) . "_" . $requestParams['v'] // For code-based changes . "_" . $cacheVersion // For data-based changes . (isset(Z_CONFIG::$CACHE_VERSION_RESPONSE_JSON_ITEM) ? "_" . Z_CONFIG::$CACHE_VERSION_RESPONSE_JSON_ITEM : "") // If there's bib content, include the bib cache version . ((in_array('bib', $requestParams['include']) && isset(Z_CONFIG::$CACHE_VERSION_BIB)) ? "_" . Z_CONFIG::$CACHE_VERSION_BIB : ""); $cached = Z_Core::$MC->get($cacheKey); if (false && $cached) { // Make sure numChildren reflects the current permissions if ($isRegularItem) { $json = json_decode($cached); $json['numChildren'] = $numChildren; $cached = json_encode($json); } //StatsD::timing("api.items.itemToResponseJSON.cached", (microtime(true) - $t) * 1000); //StatsD::increment("memcached.items.itemToResponseJSON.hit"); // Skip the cache every 10 times for now, to ensure cache sanity if (!Z_Core::probability(10)) { return $cached; } } $json = [ 'key' => $this->key, 'version' => $version, 'library' => Zotero_Libraries::toJSON($this->libraryID) ]; $json['links'] = [ 'self' => [ 'href' => Zotero_API::getItemURI($this), 'type' => 'application/json' ], 'alternate' => [ 'href' => Zotero_URI::getItemURI($this, true), 'type' => 'text/html' ] ]; if ($parent) { $parentItem = Zotero_Items::get($this->libraryID, $parent); $json['links']['up'] = [ 'href' => Zotero_API::getItemURI($parentItem), 'type' => 'application/json' ]; } // If appropriate permissions and the file is stored in ZFS, get file request link if ($downloadDetails) { $details = $downloadDetails; $type = $this->attachmentMIMEType; if ($type) { $json['links']['enclosure'] = [ 'type' => $type ]; } $json['links']['enclosure']['href'] = $details['url']; if (!empty($details['filename'])) { $json['links']['enclosure']['title'] = $details['filename']; } if (isset($details['size'])) { $json['links']['enclosure']['length'] = $details['size']; } } // 'meta' $json['meta'] = new stdClass; if (Zotero_Libraries::getType($this->libraryID) == 'group') { $createdByUserID = $this->createdByUserID; $lastModifiedByUserID = $this->lastModifiedByUserID; if ($createdByUserID) { $json['meta']->createdByUser = Zotero_Users::toJSON($createdByUserID); } if ($lastModifiedByUserID && $lastModifiedByUserID != $createdByUserID) { $json['meta']->lastModifiedByUser = Zotero_Users::toJSON($lastModifiedByUserID); } } if ($isRegularItem) { $val = $this->getCreatorSummary(); if ($val !== '') { $json['meta']->creatorSummary = $val; } $val = $this->getField('date', true, true, true); if ($val !== '') { $sqlDate = Zotero_Date::multipartToSQL($val); if (substr($sqlDate, 0, 4) !== '0000') { $json['meta']->parsedDate = Zotero_Date::sqlToISO8601($sqlDate); } } $json['meta']->numChildren = $numChildren; } // 'include' $include = $requestParams['include']; foreach ($include as $type) { if ($type == 'html') { $json[$type] = trim($this->toHTML()); } else if ($type == 'citation') { if (isset($sharedData[$type][$this->libraryID . "/" . $this->key])) { $html = $sharedData[$type][$this->libraryID . "/" . $this->key]; } else { if ($sharedData !== null) { //error_log("Citation not found in sharedData -- retrieving individually"); } $html = Zotero_Cite::getCitationFromCiteServer($this, $requestParams); } $json[$type] = $html; } else if ($type == 'bib') { if (isset($sharedData[$type][$this->libraryID . "/" . $this->key])) { $html = $sharedData[$type][$this->libraryID . "/" . $this->key]; } else { if ($sharedData !== null) { //error_log("Bibliography not found in sharedData -- retrieving individually"); } $html = Zotero_Cite::getBibliographyFromCitationServer([$this], $requestParams); // Strip prolog $html = preg_replace('/^<\?xml.+\n/', "", $html); $html = trim($html); } $json[$type] = $html; } else if ($type == 'data') { $json[$type] = $this->toJSON(true, $requestParams, true); } else if ($type == 'csljson') { $json[$type] = $this->toCSLItem(); } else if (in_array($type, Zotero_Translate::$exportFormats)) { $export = Zotero_Translate::doExport([$this], $type); $json[$type] = $export['body']; unset($export); } } // TEMP if ($cached) { $uncached = Zotero_Utilities::formatJSON($json); if ($cached != $uncached) { error_log("Cached JSON item entry does not match"); error_log(" Cached: " . $cached); error_log("Uncached: " . $uncached); //Z_Core::$MC->set($cacheKey, $uncached, 3600); // 1 hour for now } } else { /*Z_Core::$MC->set($cacheKey, $xmlstr, 3600); // 1 hour for now StatsD::timing("api.items.itemToAtom.uncached", (microtime(true) - $t) * 1000); StatsD::increment("memcached.items.itemToAtom.miss");*/ } return $json; }
public static function search($libraryID, $onlyTopLevel = false, $params = array(), $includeTrashed = false, Zotero_Permissions $permissions = null) { $rnd = "_" . uniqid($libraryID . "_"); $results = array('results' => array(), 'total' => 0); // Default empty library if ($libraryID === 0) { return $results; } $shardID = Zotero_Shards::getByLibraryID($libraryID); $includeNotes = true; if ($permissions && !$permissions->canAccess($libraryID, 'notes')) { $includeNotes = false; } // Pass a list of itemIDs, for when the initial search is done via SQL $itemIDs = !empty($params['itemIDs']) ? $params['itemIDs'] : array(); $itemKeys = $params['itemKey']; $titleSort = !empty($params['sort']) && $params['sort'] == 'title'; $parentItemSort = !empty($params['sort']) && in_array($params['sort'], ['itemType', 'dateAdded', 'dateModified', 'serverDateModified', 'addedBy']); $sql = "SELECT SQL_CALC_FOUND_ROWS DISTINCT "; // In /top mode, use the parent item's values for most joins if ($onlyTopLevel) { $itemIDSelector = "COALESCE(IA.sourceItemID, INo.sourceItemID, I.itemID)"; $itemKeySelector = "COALESCE(IP.key, I.key)"; $itemVersionSelector = "COALESCE(IP.version, I.version)"; $itemTypeIDSelector = "COALESCE(IP.itemTypeID, I.itemTypeID)"; } else { $itemIDSelector = "I.itemID"; $itemKeySelector = "I.key"; $itemVersionSelector = "I.version"; $itemTypeIDSelector = "I.itemTypeID"; } if ($params['format'] == 'keys' || $params['format'] == 'versions') { // In /top mode, display the parent item of matching items $sql .= "{$itemKeySelector} AS `key`"; if ($params['format'] == 'versions') { $sql .= ", {$itemVersionSelector} AS version"; } } else { $sql .= "{$itemIDSelector} AS itemID"; } $sql .= " FROM items I "; $sqlParams = array($libraryID); // For /top, we need the parent itemID if ($onlyTopLevel) { $sql .= "LEFT JOIN itemAttachments IA ON (IA.itemID=I.itemID) "; } // For /top, we need the parent itemID; for 'q' we need the note; for sorting by title, // we need the note title if ($onlyTopLevel || !empty($params['q']) || $titleSort) { $sql .= "LEFT JOIN itemNotes INo ON (INo.itemID=I.itemID) "; } // For some /top requests, pull in the parent item's items row if ($onlyTopLevel && ($params['format'] == 'keys' || $params['format'] == 'versions' || $parentItemSort)) { $sql .= "LEFT JOIN items IP ON ({$itemIDSelector}=IP.itemID) "; } // Pull in titles if (!empty($params['q']) || $titleSort) { $titleFieldIDs = array_merge(array(Zotero_ItemFields::getID('title')), Zotero_ItemFields::getTypeFieldsFromBase('title')); $sql .= "LEFT JOIN itemData IDT ON (IDT.itemID=I.itemID AND IDT.fieldID IN " . "(" . implode(',', $titleFieldIDs) . ")) "; } // When sorting by title in /top mode, we need the title of the parent item if ($onlyTopLevel && $titleSort) { $titleSortDataTable = "IDTSort"; $titleSortNoteTable = "INoSort"; $sql .= "LEFT JOIN itemData IDTSort ON (IDTSort.itemID={$itemIDSelector} AND " . "IDTSort.fieldID IN (" . implode(',', $titleFieldIDs) . ")) " . "LEFT JOIN itemNotes INoSort ON (INoSort.itemID={$itemIDSelector}) "; } else { $titleSortDataTable = "IDT"; $titleSortNoteTable = "INo"; } if (!empty($params['q'])) { // Pull in creators $sql .= "LEFT JOIN itemCreators IC ON (IC.itemID=I.itemID) " . "LEFT JOIN creators C ON (C.creatorID=IC.creatorID) "; // Pull in dates $dateFieldIDs = array_merge(array(Zotero_ItemFields::getID('date')), Zotero_ItemFields::getTypeFieldsFromBase('date')); $sql .= "LEFT JOIN itemData IDD ON (IDD.itemID=I.itemID AND IDD.fieldID IN " . "(" . implode(',', $dateFieldIDs) . ")) "; } if ($includeTrashed) { if (!empty($params['trashedItemsOnly'])) { $sql .= "JOIN deletedItems DI ON (DI.itemID=I.itemID) "; } } else { $sql .= "LEFT JOIN deletedItems DI ON (DI.itemID=I.itemID) "; // In /top mode, we don't want to show results for deleted parents or children if ($onlyTopLevel) { $sql .= "LEFT JOIN deletedItems DIP ON (DIP.itemID={$itemIDSelector}) "; } } if (!empty($params['sort'])) { switch ($params['sort']) { case 'title': case 'creator': $sql .= "LEFT JOIN itemSortFields ISF ON (ISF.itemID={$itemIDSelector}) "; break; case 'date': // When sorting by date in /top mode, we need the date of the parent item if ($onlyTopLevel) { $sortTable = "IDDSort"; // Pull in dates $dateFieldIDs = array_merge(array(Zotero_ItemFields::getID('date')), Zotero_ItemFields::getTypeFieldsFromBase('date')); $sql .= "LEFT JOIN itemData IDDSort ON (IDDSort.itemID={$itemIDSelector} AND " . "IDDSort.fieldID IN (" . implode(',', $dateFieldIDs) . ")) "; } else { $sortTable = "IDD"; if (empty($params['q'])) { $dateFieldIDs = array_merge(array(Zotero_ItemFields::getID('date')), Zotero_ItemFields::getTypeFieldsFromBase('date')); $sql .= "LEFT JOIN itemData IDD ON (IDD.itemID=I.itemID AND IDD.fieldID IN (" . implode(',', $dateFieldIDs) . ")) "; } } break; case 'itemType': $locale = 'en-US'; $types = Zotero_ItemTypes::getAll($locale); // TEMP: get localized string // DEBUG: Why is attachment skipped in getAll()? $types[] = array('id' => 14, 'localized' => 'Attachment'); foreach ($types as $type) { $sql2 = "INSERT IGNORE INTO tmpItemTypeNames VALUES (?, ?, ?)"; Zotero_DB::query($sql2, array($type['id'], $locale, $type['localized']), $shardID); } // Join temp table to query $sql .= "JOIN tmpItemTypeNames TITN ON (TITN.itemTypeID={$itemTypeIDSelector}) "; break; case 'addedBy': $isGroup = Zotero_Libraries::getType($libraryID) == 'group'; if ($isGroup) { $sql2 = "SELECT DISTINCT createdByUserID FROM items\n\t\t\t\t\t\t\t\tJOIN groupItems USING (itemID) WHERE\n\t\t\t\t\t\t\t\tcreatedByUserID IS NOT NULL AND "; if ($itemIDs) { $sql2 .= "itemID IN (" . implode(', ', array_fill(0, sizeOf($itemIDs), '?')) . ") "; $createdByUserIDs = Zotero_DB::columnQuery($sql2, $itemIDs, $shardID); } else { $sql2 .= "libraryID=?"; $createdByUserIDs = Zotero_DB::columnQuery($sql2, $libraryID, $shardID); } // Populate temp table with usernames if ($createdByUserIDs) { $toAdd = array(); foreach ($createdByUserIDs as $createdByUserID) { $toAdd[] = array($createdByUserID, Zotero_Users::getUsername($createdByUserID)); } $sql2 = "INSERT IGNORE INTO tmpCreatedByUsers VALUES "; Zotero_DB::bulkInsert($sql2, $toAdd, 50, false, $shardID); // Join temp table to query $sql .= "LEFT JOIN groupItems GI ON (GI.itemID=I.itemID)\n\t\t\t\t\t\t\t\t\tLEFT JOIN tmpCreatedByUsers TCBU ON (TCBU.userID=GI.createdByUserID) "; } } break; } } $sql .= "WHERE I.libraryID=? "; if (!$includeTrashed) { $sql .= "AND DI.itemID IS NULL "; // Hide deleted parents in /top mode if ($onlyTopLevel) { $sql .= "AND DIP.itemID IS NULL "; } } // Search on title, creators, and dates if (!empty($params['q'])) { $sql .= "AND ("; $sql .= "IDT.value LIKE ? "; $sqlParams[] = '%' . $params['q'] . '%'; $sql .= "OR INo.title LIKE ? "; $sqlParams[] = '%' . $params['q'] . '%'; $sql .= "OR TRIM(CONCAT(firstName, ' ', lastName)) LIKE ? "; $sqlParams[] = '%' . $params['q'] . '%'; $sql .= "OR SUBSTR(IDD.value, 1, 4) = ?"; $sqlParams[] = $params['q']; // Full-text search if ($params['qmode'] == 'everything') { $ftKeys = Zotero_FullText::searchInLibrary($libraryID, $params['q']); if ($ftKeys) { $sql .= " OR I.key IN (" . implode(', ', array_fill(0, sizeOf($ftKeys), '?')) . ") "; $sqlParams = array_merge($sqlParams, $ftKeys); } } $sql .= ") "; } // Search on itemType if (!empty($params['itemType'])) { $itemTypes = Zotero_API::getSearchParamValues($params, 'itemType'); if ($itemTypes) { if (sizeOf($itemTypes) > 1) { throw new Exception("Cannot specify 'itemType' more than once", Z_ERROR_INVALID_INPUT); } $itemTypes = $itemTypes[0]; $itemTypeIDs = array(); foreach ($itemTypes['values'] as $itemType) { $itemTypeID = Zotero_ItemTypes::getID($itemType); if (!$itemTypeID) { throw new Exception("Invalid itemType '{$itemType}'", Z_ERROR_INVALID_INPUT); } $itemTypeIDs[] = $itemTypeID; } $sql .= "AND I.itemTypeID " . ($itemTypes['negation'] ? "NOT " : "") . "IN (" . implode(',', array_fill(0, sizeOf($itemTypeIDs), '?')) . ") "; $sqlParams = array_merge($sqlParams, $itemTypeIDs); } } if (!$includeNotes) { $sql .= "AND I.itemTypeID != 1 "; } if (!empty($params['since'])) { $sql .= "AND {$itemVersionSelector} > ? "; $sqlParams[] = $params['since']; } // TEMP: for sync transition if (!empty($params['sincetime']) && $params['sincetime'] != 1) { $sql .= "AND I.serverDateModified >= FROM_UNIXTIME(?) "; $sqlParams[] = $params['sincetime']; } // Tags // // ?tag=foo // ?tag=foo bar // phrase // ?tag=-foo // negation // ?tag=\-foo // literal hyphen (only for first character) // ?tag=foo&tag=bar // AND $tagSets = Zotero_API::getSearchParamValues($params, 'tag'); if ($tagSets) { $sql2 = "SELECT itemID FROM items WHERE libraryID=?\n"; $sqlParams2 = array($libraryID); $positives = array(); $negatives = array(); foreach ($tagSets as $set) { $tagIDs = array(); foreach ($set['values'] as $tag) { $ids = Zotero_Tags::getIDs($libraryID, $tag, true); if (!$ids) { $ids = array(0); } $tagIDs = array_merge($tagIDs, $ids); } $tagIDs = array_unique($tagIDs); $tmpSQL = "SELECT itemID FROM items JOIN itemTags USING (itemID) " . "WHERE tagID IN (" . implode(',', array_fill(0, sizeOf($tagIDs), '?')) . ")"; $ids = Zotero_DB::columnQuery($tmpSQL, $tagIDs, $shardID); if (!$ids) { // If no negative tags, skip this tag set if ($set['negation']) { continue; } // If no positive tags, return no matches return $results; } $ids = $ids ? $ids : array(); $sql2 .= " AND itemID " . ($set['negation'] ? "NOT " : "") . " IN (" . implode(',', array_fill(0, sizeOf($ids), '?')) . ")"; $sqlParams2 = array_merge($sqlParams2, $ids); } $tagItems = Zotero_DB::columnQuery($sql2, $sqlParams2, $shardID); // No matches if (!$tagItems) { return $results; } // Combine with passed ids if ($itemIDs) { $itemIDs = array_intersect($itemIDs, $tagItems); // None of the tag matches match the passed ids if (!$itemIDs) { return $results; } } else { $itemIDs = $tagItems; } } if ($itemIDs) { $sql .= "AND I.itemID IN (" . implode(', ', array_fill(0, sizeOf($itemIDs), '?')) . ") "; $sqlParams = array_merge($sqlParams, $itemIDs); } if ($itemKeys) { $sql .= "AND I.key IN (" . implode(', ', array_fill(0, sizeOf($itemKeys), '?')) . ") "; $sqlParams = array_merge($sqlParams, $itemKeys); } $sql .= "ORDER BY "; if (!empty($params['sort'])) { switch ($params['sort']) { case 'dateAdded': case 'dateModified': case 'serverDateModified': if ($onlyTopLevel) { $orderSQL = "IP." . $params['sort']; } else { $orderSQL = "I." . $params['sort']; } break; case 'itemType': $orderSQL = "TITN.itemTypeName"; /* // Optional method for sorting by localized item type name, which would avoid // the INSERT and JOIN above and allow these requests to use DB read replicas $locale = 'en-US'; $types = Zotero_ItemTypes::getAll($locale); // TEMP: get localized string // DEBUG: Why is attachment skipped in getAll()? $types[] = [ 'id' => 14, 'localized' => 'Attachment' ]; usort($types, function ($a, $b) { return strcasecmp($a['localized'], $b['localized']); }); // Pass order of localized item type names for sorting // e.g., FIELD(14, 12, 14, 26...) for sorting "Attachment" after "Artwork" $orderSQL = "FIELD($itemTypeIDSelector, " . implode(", ", array_map(function ($x) { return $x['id']; }, $types)) . ")"; // If itemTypeID isn't found in passed list (currently only for NSF Reviewer), // sort last $orderSQL = "IFNULL(NULLIF($orderSQL, 0), 99999)"; // All items have types, so no need to check for empty sort values $params['emptyFirst'] = true; */ break; case 'title': $orderSQL = "IFNULL(COALESCE(sortTitle, {$titleSortDataTable}.value, {$titleSortNoteTable}.title), '')"; break; case 'creator': $orderSQL = "ISF.creatorSummary"; break; // TODO: generic base field mapping-aware sorting // TODO: generic base field mapping-aware sorting case 'date': $orderSQL = "{$sortTable}.value"; break; case 'addedBy': if ($isGroup && $createdByUserIDs) { $orderSQL = "TCBU.username"; } else { $orderSQL = ($onlyTopLevel ? "IP" : "I") . ".dateAdded"; } break; case 'itemKeyList': $orderSQL = "FIELD(I.key," . implode(',', array_fill(0, sizeOf($itemKeys), '?')) . ")"; $sqlParams = array_merge($sqlParams, $itemKeys); break; default: $fieldID = Zotero_ItemFields::getID($params['sort']); if (!$fieldID) { throw new Exception("Invalid order field '" . $params['sort'] . "'"); } $orderSQL = "(SELECT value FROM itemData WHERE itemID=I.itemID AND fieldID=?)"; if (!$params['emptyFirst']) { $sqlParams[] = $fieldID; } $sqlParams[] = $fieldID; } if (!empty($params['direction'])) { $dir = $params['direction']; } else { $dir = "ASC"; } if (!$params['emptyFirst']) { $sql .= "IFNULL({$orderSQL}, '') = '' {$dir}, "; } $sql .= $orderSQL . " {$dir}, "; } $sql .= "I.version " . (!empty($params['direction']) ? $params['direction'] : "ASC") . ", I.itemID " . (!empty($params['direction']) ? $params['direction'] : "ASC") . " "; if (!empty($params['limit'])) { $sql .= "LIMIT ?, ?"; $sqlParams[] = $params['start'] ? $params['start'] : 0; $sqlParams[] = $params['limit']; } // Log SQL statement with embedded parameters /*if (true || !empty($_GET['sqldebug'])) { error_log($onlyTopLevel); $debugSQL = ""; $parts = explode("?", $sql); $debugSQLParams = $sqlParams; foreach ($parts as $part) { $val = array_shift($debugSQLParams); $debugSQL .= $part; if (!is_null($val)) { $debugSQL .= is_int($val) ? $val : '"' . $val . '"'; } } error_log($debugSQL . ";"); }*/ if ($params['format'] == 'versions') { $rows = Zotero_DB::query($sql, $sqlParams, $shardID); } else { $rows = Zotero_DB::columnQuery($sql, $sqlParams, $shardID); } $results['total'] = Zotero_DB::valueQuery("SELECT FOUND_ROWS()", false, $shardID); if ($rows) { if ($params['format'] == 'keys') { $results['results'] = $rows; } else { if ($params['format'] == 'versions') { foreach ($rows as $row) { $results['results'][$row['key']] = $row['version']; } } else { $results['results'] = Zotero_Items::get($libraryID, $rows); } } } return $results; }
public function getPermissions() { if ($this->erased) { throw new Exception("Cannot access permissions of deleted key {$this->id}"); } if (($this->id || $this->key) && !$this->loaded) { $this->load(); } $permissions = new Zotero_Permissions($this->userID); foreach ($this->permissions as $libraryID => $p) { foreach ($p as $key => $val) { $permissions->setPermission($libraryID, $key, $val); } } return $permissions; }