private static function getNext($table) { $sql = "REPLACE INTO {$table} (stub) VALUES ('a')"; if (Z_Core::probability(2)) { try { Zotero_ID_DB_1::query($sql); $id = Zotero_ID_DB_1::valueQuery("SELECT LAST_INSERT_ID()"); } catch (Exception $e) { Z_Core::logError("Error accessing ID server 1"); Zotero_ID_DB_2::query($sql); $id = Zotero_ID_DB_2::valueQuery("SELECT LAST_INSERT_ID()"); } } else { try { Zotero_ID_DB_2::query($sql); $id = Zotero_ID_DB_2::valueQuery("SELECT LAST_INSERT_ID()"); } catch (Exception $e) { Z_Core::logError("Error accessing ID server 2"); Zotero_ID_DB_1::query($sql); $id = Zotero_ID_DB_1::valueQuery("SELECT LAST_INSERT_ID()"); } } if (!$id || !is_int($id)) { throw new Exception("Invalid id {$id}"); } return $id; }
public static function notifyProcessors($mode, $signal = "NEXT") { $sql = "SELECT INET_NTOA(addr) AS addr, port FROM processorDaemons WHERE mode=?"; $daemons = Zotero_DB::query($sql, array($mode)); if (!$daemons) { Z_Core::logError("No {$mode} processor daemons found"); return; } foreach ($daemons as $daemon) { self::notifyProcessor($mode, $signal, $daemon['addr'], $daemon['port']); } }
public static function getAllAdvanced($userID = false, $params = array(), $permissions = null) { $buffer = 20; $maxTimes = 3; $groups = array(); $start = !empty($params['start']) ? $params['start'] : 0; $limit = !empty($params['limit']) ? $params['limit'] + $buffer : false; $totalResults = null; $times = 0; while (true) { if ($times > 0) { Z_Core::logError('Getting more groups in Zotero_Groups::getAllAdvanced()'); } $sql = "SELECT SQL_CALC_FOUND_ROWS G.groupID, GUO.userID AS ownerUserID FROM groups G\n\t\t\t\t\tJOIN groupUsers GUO ON (G.groupID=GUO.groupID AND GUO.role='owner') "; $sqlParams = array(); if ($userID) { $sql .= "JOIN groupUsers GUA ON (G.groupID=GUA.groupID) WHERE GUA.userID=? "; $sqlParams[] = $userID; } $paramSQL = ""; $includeEmpty = false; if (!empty($params['q'])) { if (!is_array($params['q'])) { $params['q'] = array($params['q']); } foreach ($params['q'] as $q) { $field = explode(":", $q); if (sizeOf($field) == 2) { switch ($field[0]) { case 'slug': $includeEmpty = true; break; default: throw new Exception("Cannot search by group field '{$field[0]}'", Z_ERROR_INVALID_GROUP_TYPE); } $paramSQL .= "AND " . $field[0]; // If first character is '-', negate $paramSQL .= $field[0][0] == '-' ? '!' : ''; $paramSQL .= "=? "; $sqlParams[] = $field[1]; } else { $paramSQL .= "AND name LIKE ? "; $sqlParams[] = "%{$q}%"; } } } if (!$userID) { if ($includeEmpty) { $sql .= "WHERE 1 "; } else { // Don't include groups that have never had items $sql .= "JOIN libraries L ON (G.libraryID=L.libraryID)\n\t\t\t\t\t\t\tWHERE L.lastUpdated != '0000-00-00 00:00:00' "; } } $sql .= $paramSQL; if (!empty($params['fq'])) { if (!is_array($params['fq'])) { $params['fq'] = array($params['fq']); } foreach ($params['fq'] as $fq) { $facet = explode(":", $fq); if (sizeOf($facet) == 2 && preg_match('/-?GroupType/', $facet[0])) { switch ($facet[1]) { case 'PublicOpen': case 'PublicClosed': case 'Private': break; default: throw new Exception("Invalid group type '{$facet[1]}'", Z_ERROR_INVALID_GROUP_TYPE); } $sql .= "AND type"; // If first character is '-', negate $sql .= $facet[0][0] == '-' ? '!' : ''; $sql .= "=? "; $sqlParams[] = $facet[1]; } } } if (!empty($params['order'])) { $order = $params['order']; if ($order == 'title') { $order = 'name'; } $sql .= "ORDER BY {$order}"; if (!empty($params['sort'])) { $sql .= " " . $params['sort'] . " "; } } // Set limit higher than the actual limit, in case some groups are // removed during access checks // // Actual limiting is done below if ($limit) { $sql .= "LIMIT ?, ?"; $sqlParams[] = $start; $sqlParams[] = $limit; } $rows = Zotero_DB::query($sql, $sqlParams); if (!$rows) { break; } if (!$totalResults) { $foundRows = Zotero_DB::valueQuery("SELECT FOUND_ROWS()"); $totalResults = $foundRows; } // Include only groups with non-banned owners $owners = array(); foreach ($rows as $row) { $owners[] = $row['ownerUserID']; } $owners = Zotero_Users::getValidUsers($owners); $ids = array(); foreach ($rows as $row) { if (!in_array($row['ownerUserID'], $owners)) { $totalResults--; continue; } $ids[] = $row['groupID']; } $batchStartPos = sizeOf($groups); foreach ($ids as $id) { $group = Zotero_Groups::get($id, true); $groups[] = $group; } // Remove groups that can't be accessed if ($permissions) { for ($i = $batchStartPos; $i < sizeOf($groups); $i++) { $libraryID = (int) $groups[$i]->libraryID; // TEMP: casting shouldn't be necessary if (!$permissions->canAccess($libraryID)) { array_splice($groups, $i, 1); $i--; $totalResults--; } } } $times++; if ($times == $maxTimes) { Z_Core::logError('Too many queries in Zotero_Groups::getAllAdvanced()'); break; } if (empty($params['limit'])) { break; } // If we have enough groups to fill the limit, stop if (sizeOf($groups) > $params['limit']) { break; } // If there no more rows, stop if ($start + sizeOf($rows) == $foundRows) { break; } // This shouldn't happen if ($start + sizeOf($rows) > $foundRows) { Z_Core::logError('More rows than $foundRows in Zotero_Groups::getAllAdvanced()'); } $start = $start + sizeOf($rows); // Get number we still need plus the buffer or all remaining, whichever is lower $limit = min($params['limit'] - sizeOf($groups) + $buffer, $foundRows - $start); } // TODO: generate previous start value if (!$groups) { return array('groups' => array(), 'totalResults' => 0); } // Fake limiting -- we can't just use SQL limit because // some groups might be inaccessible if (!empty($params['limit'])) { $groups = array_slice($groups, 0, $params['limit']); } $results = array('groups' => $groups, 'totalResults' => $totalResults); return $results; }
public static function getInstitutionalUserQuota($userID) { // TODO: config $dev = Z_ENV_TESTING_SITE ? "_test" : ""; $databaseName = "zotero_www{$dev}"; // Get maximum institutional quota by e-mail domain $sql = "SELECT IFNULL(MAX(storageQuota), 0) FROM {$databaseName}.users_email\n\t\t\t\tJOIN {$databaseName}.storage_institutions ON (SUBSTRING_INDEX(email, '@', -1)=domain)\n\t\t\t\tWHERE userID=?"; try { $institutionalDomainQuota = Zotero_WWW_DB_2::valueQuery($sql, $userID); } catch (Exception $e) { Z_Core::logError("WARNING: {$e} -- retrying on primary"); $institutionalDomainQuota = Zotero_WWW_DB_1::valueQuery($sql, $userID); } // Get maximum institutional quota by e-mail address $sql = "SELECT IFNULL(MAX(storageQuota), 0) FROM {$databaseName}.users_email\n\t\t\t\tJOIN {$databaseName}.storage_institution_email USING (email)\n\t\t\t\tJOIN {$databaseName}.storage_institutions USING (institutionID)\n\t\t\t\tWHERE userID=?"; try { $institutionalEmailQuota = Zotero_WWW_DB_2::valueQuery($sql, $userID); } catch (Exception $e) { Z_Core::logError("WARNING: {$e} -- retrying on primary"); $institutionalEmailQuota = Zotero_WWW_DB_1::valueQuery($sql, $userID); } $quota = max($institutionalDomainQuota, $institutionalEmailQuota); return $quota ? $quota : false; }
public function items() { if (($this->method == 'POST' || $this->method == 'PUT') && !$this->body) { $this->e400("{$this->method} data not provided"); } $itemIDs = array(); $responseItems = array(); $responseKeys = array(); $totalResults = null; // // Single item // if (($this->objectID || $this->objectKey) && !$this->subset) { if ($this->fileMode) { if ($this->fileView) { $this->allowMethods(array('GET', 'HEAD', 'POST')); } else { $this->allowMethods(array('GET', 'PUT', 'POST', 'HEAD', 'PATCH')); } } else { $this->allowMethods(array('GET', 'PUT', 'DELETE')); } // Check for general library access if (!$this->permissions->canAccess($this->objectLibraryID)) { //var_dump($this->objectLibraryID); //var_dump($this->permissions); $this->e403(); } if ($this->objectKey) { $item = Zotero_Items::getByLibraryAndKey($this->objectLibraryID, $this->objectKey); } else { try { $item = Zotero_Items::get($this->objectLibraryID, $this->objectID); } catch (Exception $e) { if ($e->getCode() == Z_ERROR_OBJECT_LIBRARY_MISMATCH) { $item = false; } else { throw $e; } } } if (!$item) { // Possibly temporary workaround to block unnecessary full syncs if ($this->fileMode && $this->method == 'POST') { // If > 2 requests for missing file, trigger a full sync via 404 $cacheKey = "apiMissingFile_" . $this->objectLibraryID . "_" . ($this->objectKey ? $this->objectKey : $this->objectID); $set = Z_Core::$MC->get($cacheKey); if (!$set) { Z_Core::$MC->set($cacheKey, 1, 86400); } else { if ($set < 2) { Z_Core::$MC->increment($cacheKey); } else { Z_Core::$MC->delete($cacheKey); $this->e404("A file sync error occurred. Please sync again."); } } $this->e500("A file sync error occurred. Please sync again."); } // If we have an id, make sure this isn't really an all-numeric key if ($this->objectID && strlen($this->objectID) == 8 && preg_match('/[0-9]{8}/', $this->objectID)) { $item = Zotero_Items::getByLibraryAndKey($this->objectLibraryID, $this->objectID); if ($item) { $this->objectKey = $this->objectID; unset($this->objectID); } } if (!$item) { $this->e404("Item does not exist"); } } if ($item->isNote() && !$this->permissions->canAccess($this->objectLibraryID, 'notes')) { $this->e403(); } // Make sure URL libraryID matches item libraryID if ($this->objectLibraryID != $item->libraryID) { $this->e404("Item does not exist"); } // File access mode if ($this->fileMode) { $this->_handleFileRequest($item); } // If id, redirect to key URL if ($this->objectID) { $this->allowMethods(array('GET')); $qs = !empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : ''; header("Location: " . Zotero_API::getItemURI($item) . $qs); exit; } if ($this->scopeObject) { switch ($this->scopeObject) { // Remove item from collection case 'collections': $this->allowMethods(array('DELETE')); if (!$this->permissions->canWrite($this->objectLibraryID)) { $this->e403("Write access denied"); } $collection = Zotero_Collections::getByLibraryAndKey($this->objectLibraryID, $this->scopeObjectKey); if (!$collection) { $this->e404("Collection not found"); } if (!$collection->hasItem($item->id)) { $this->e404("Item not found in collection"); } Zotero_DB::beginTransaction(); $timestamp = Zotero_Libraries::updateTimestamps($this->objectLibraryID); Zotero_DB::registerTransactionTimestamp($timestamp); $collection->removeItem($item->id); Zotero_DB::commit(); $this->e204(); default: $this->e400(); } } if ($this->method == 'PUT' || $this->method == 'DELETE') { if (!$this->permissions->canWrite($this->objectLibraryID)) { $this->e403("Write access denied"); } if (!Z_CONFIG::$TESTING_SITE || empty($_GET['skipetag'])) { if (empty($_SERVER['HTTP_IF_MATCH'])) { $this->e400("If-Match header not provided"); } if (!preg_match('/^"?([a-f0-9]{32})"?$/', $_SERVER['HTTP_IF_MATCH'], $matches)) { $this->e400("Invalid ETag in If-Match header"); } if ($item->etag != $matches[1]) { $this->e412("ETag does not match current version of item"); } } // Update existing item if ($this->method == 'PUT') { $obj = $this->jsonDecode($this->body); Zotero_Items::updateFromJSON($item, $obj, false, null, $this->userID); $this->queryParams['format'] = 'atom'; $this->queryParams['content'] = array('json'); if ($cacheKey = $this->getWriteTokenCacheKey()) { Z_Core::$MC->set($cacheKey, true, $this->writeTokenCacheTime); } } else { Zotero_Items::delete($this->objectLibraryID, $this->objectKey, true); try { Zotero_Processors::notifyProcessors('index'); } catch (Exception $e) { Z_Core::logError($e); } $this->e204(); } } // Display item switch ($this->queryParams['format']) { case 'atom': $this->responseXML = Zotero_Items::convertItemToAtom($item, $this->queryParams, $this->apiVersion, $this->permissions); break; case 'bib': echo Zotero_Cite::getBibliographyFromCitationServer(array($item), $this->queryParams['style'], $this->queryParams['css']); exit; case 'csljson': $json = Zotero_Cite::getJSONFromItems(array($item), true); if ($this->queryParams['pprint']) { header("Content-Type: text/plain"); $json = Zotero_Utilities::json_encode_pretty($json); } else { header("Content-Type: application/vnd.citationstyles.csl+json"); $json = json_encode($json); } echo $json; exit; default: $export = Zotero_Translate::doExport(array($item), $this->queryParams['format']); if ($this->queryParams['pprint']) { header("Content-Type: text/plain"); } else { header("Content-Type: " . $export['mimeType']); } echo $export['body']; exit; } } else { $this->allowMethods(array('GET', 'POST')); if (!$this->permissions->canAccess($this->objectLibraryID)) { $this->e403(); } $includeTrashed = false; $formatAsKeys = $this->queryParams['format'] == 'keys'; if ($this->scopeObject) { $this->allowMethods(array('GET', 'POST')); // If id, redirect to key URL if ($this->scopeObjectID) { $this->allowMethods(array('GET')); if (!in_array($this->scopeObject, array("collections", "tags"))) { $this->e400(); } $className = 'Zotero_' . ucwords($this->scopeObject); $obj = call_user_func(array($className, 'get'), $this->objectLibraryID, $this->scopeObjectID); if (!$obj) { $this->e404("Scope " . substr($this->scopeObject, 0, -1) . " not found"); } $base = call_user_func(array('Zotero_API', 'get' . substr(ucwords($this->scopeObject), 0, -1) . 'URI'), $obj); $qs = !empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : ''; header("Location: " . $base . "/items" . $qs); exit; } switch ($this->scopeObject) { case 'collections': $collection = Zotero_Collections::getByLibraryAndKey($this->objectLibraryID, $this->scopeObjectKey); if (!$collection) { $this->e404("Collection not found"); } // Add items to collection if ($this->method == 'POST') { if (!$this->permissions->canWrite($this->objectLibraryID)) { $this->e403("Write access denied"); } Zotero_DB::beginTransaction(); $timestamp = Zotero_Libraries::updateTimestamps($this->objectLibraryID); Zotero_DB::registerTransactionTimestamp($timestamp); $itemKeys = explode(' ', $this->body); $itemIDs = array(); foreach ($itemKeys as $key) { try { $item = Zotero_Items::getByLibraryAndKey($this->objectLibraryID, $key); } catch (Exception $e) { if ($e->getCode() == Z_ERROR_OBJECT_LIBRARY_MISMATCH) { $item = false; } else { throw $e; } } if (!$item) { throw new Exception("Item '{$key}' not found in library", Z_ERROR_INVALID_INPUT); } if ($item->getSource()) { throw new Exception("Child items cannot be added to collections directly", Z_ERROR_INVALID_INPUT); } $itemIDs[] = $item->id; } $collection->addItems($itemIDs); Zotero_DB::commit(); $this->e204(); } $title = "Items in Collection ‘" . $collection->name . "’"; $itemIDs = $collection->getChildItems(); break; case 'tags': $this->allowMethods(array('GET')); $tagIDs = Zotero_Tags::getIDs($this->objectLibraryID, $this->scopeObjectName); if (!$tagIDs) { $this->e404("Tag not found"); } $itemIDs = array(); $title = ''; foreach ($tagIDs as $tagID) { $tag = new Zotero_Tag(); $tag->libraryID = $this->objectLibraryID; $tag->id = $tagID; // Use a real tag name, in case case differs if (!$title) { $title = "Items of Tag ‘" . $tag->name . "’"; } $itemIDs = array_merge($itemIDs, $tag->getLinkedItems(true)); } $itemIDs = array_unique($itemIDs); break; default: throw new Exception("Invalid items scope object '{$this->scopeObject}'"); } } else { // Top-level items if ($this->subset == 'top') { $this->allowMethods(array('GET')); $title = "Top-Level Items"; $results = Zotero_Items::search($this->objectLibraryID, true, $this->queryParams, false, $formatAsKeys); } else { if ($this->subset == 'trash') { $this->allowMethods(array('GET')); $title = "Deleted Items"; $itemIDs = Zotero_Items::getDeleted($this->objectLibraryID, true); $includeTrashed = true; } else { if ($this->subset == 'children') { // If we have an id, make sure this isn't really an all-numeric key if ($this->objectID && strlen($this->objectID) == 8 && preg_match('/[0-9]{8}/', $this->objectID)) { $item = Zotero_Items::getByLibraryAndKey($this->objectLibraryID, $this->objectID); if ($item) { $this->objectKey = $this->objectID; unset($this->objectID); } } // If id, redirect to key URL if ($this->objectID) { $this->allowMethods(array('GET')); $item = Zotero_Items::get($this->objectLibraryID, $this->objectID); if (!$item) { $this->e404("Item not found"); } $qs = !empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : ''; header("Location: " . Zotero_API::getItemURI($item) . '/children' . $qs); exit; } $item = Zotero_Items::getByLibraryAndKey($this->objectLibraryID, $this->objectKey); if (!$item) { $this->e404("Item not found"); } // Create new child items if ($this->method == 'POST') { if (!$this->permissions->canWrite($this->objectLibraryID)) { $this->e403("Write access denied"); } $obj = $this->jsonDecode($this->body); $keys = Zotero_Items::addFromJSON($obj, $this->objectLibraryID, $item, $this->userID); if ($cacheKey = $this->getWriteTokenCacheKey()) { Z_Core::$MC->set($cacheKey, true, $this->writeTokenCacheTime); } $uri = Zotero_API::getItemURI($item) . "/children"; $queryString = "itemKey=" . urlencode(implode(",", $keys)) . "&content=json"; if ($this->apiKey) { $queryString .= "&key=" . $this->apiKey; } $uri .= "?" . $queryString; $this->responseCode = 201; $this->queryParams = Zotero_API::parseQueryParams($queryString, $this->action, false); } // Display items $title = "Child Items of ‘" . $item->getDisplayTitle() . "’"; $notes = $item->getNotes(); $attachments = $item->getAttachments(); $itemIDs = array_merge($notes, $attachments); } else { // Create new items if ($this->method == 'POST') { if (!$this->permissions->canWrite($this->objectLibraryID)) { $this->e403("Write access denied"); } $obj = $this->jsonDecode($this->body); if (isset($obj->url)) { $response = Zotero_Items::addFromURL($obj, $this->objectLibraryID, $this->userID, $this->getTranslationToken()); if ($response instanceof stdClass) { header("Content-Type: application/json"); echo json_encode($response->select); $this->e300(); } else { if (is_int($response)) { switch ($response) { case 501: $this->e501("No translators found for URL"); break; default: $this->e500("Error translating URL"); } } else { $keys = $response; } } } else { $keys = Zotero_Items::addFromJSON($obj, $this->objectLibraryID, null, $this->userID); } if (!$keys) { throw new Exception("No items added"); } if ($cacheKey = $this->getWriteTokenCacheKey()) { Z_Core::$MC->set($cacheKey, true, $this->writeTokenCacheTime); } $uri = Zotero_API::getItemsURI($this->objectLibraryID); $queryString = "itemKey=" . urlencode(implode(",", $keys)) . "&content=json"; if ($this->apiKey) { $queryString .= "&key=" . $this->apiKey; } $uri .= "?" . $queryString; $this->responseCode = 201; $this->queryParams = Zotero_API::parseQueryParams($queryString, $this->action, false); } $title = "Items"; $results = Zotero_Items::search($this->objectLibraryID, false, $this->queryParams, false, $formatAsKeys); } } } if (!empty($results)) { if ($formatAsKeys) { $responseKeys = $results['keys']; } else { $responseItems = $results['items']; } $totalResults = $results['total']; } } if ($this->queryParams['format'] == 'bib') { if (($itemIDs ? sizeOf($itemIDs) : $results['total']) > Zotero_API::$maxBibliographyItems) { $this->e413("Cannot generate bibliography with more than " . Zotero_API::$maxBibliographyItems . " items"); } } if ($itemIDs) { $this->queryParams['itemIDs'] = $itemIDs; $results = Zotero_Items::search($this->objectLibraryID, false, $this->queryParams, $includeTrashed, $formatAsKeys); if ($formatAsKeys) { $responseKeys = $results['keys']; } else { $responseItems = $results['items']; } $totalResults = $results['total']; } else { if (!isset($results)) { if ($formatAsKeys) { $responseKeys = array(); } else { $responseItems = array(); } $totalResults = 0; } } // Remove notes if not user and not public for ($i = 0; $i < sizeOf($responseItems); $i++) { if ($responseItems[$i]->isNote() && !$this->permissions->canAccess($responseItems[$i]->libraryID, 'notes')) { array_splice($responseItems, $i, 1); $totalResults--; $i--; } } switch ($this->queryParams['format']) { case 'atom': $this->responseXML = Zotero_Atom::createAtomFeed($this->getFeedNamePrefix($this->objectLibraryID) . $title, $this->uri, $responseItems, $totalResults, $this->queryParams, $this->apiVersion, $this->permissions); break; case 'bib': echo Zotero_Cite::getBibliographyFromCitationServer($responseItems, $this->queryParams['style'], $this->queryParams['css']); exit; case 'csljson': $json = Zotero_Cite::getJSONFromItems($responseItems, true); if ($this->queryParams['pprint']) { header("Content-Type: text/plain"); $json = Zotero_Utilities::json_encode_pretty($json); } else { header("Content-Type: application/vnd.citationstyles.csl+json"); $json = json_encode($json); } echo $json; exit; case 'keys': if (!$formatAsKeys) { $responseKeys = array(); foreach ($responseItems as $item) { $responseKeys[] = $item->key; } } header("Content-Type: text/plain"); echo implode("\n", $responseKeys) . "\n"; exit; default: $export = Zotero_Translate::doExport($responseItems, $this->queryParams['format']); if ($this->queryParams['pprint']) { header("Content-Type: text/plain"); } else { header("Content-Type: " . $export['mimeType']); } echo $export['body']; exit; } } $this->end(); }
/** * Add sync process and associated locks to database */ private static function addUploadProcess($userID, $libraryIDs, $syncQueueID = null, $syncProcessID = null) { Zotero_DB::beginTransaction(); $syncProcessID = $syncProcessID ? $syncProcessID : Zotero_ID::getBigInt(); $sql = "INSERT INTO syncProcesses (syncProcessID, userID) VALUES (?, ?)"; try { Zotero_DB::query($sql, array($syncProcessID, $userID)); } catch (Exception $e) { $sql = "SELECT CONCAT(syncProcessID,' ',userID,' ',started) FROM syncProcesses WHERE userID=?"; $val = Zotero_DB::valueQuery($sql, $userID); Z_Core::logError($val); } if ($libraryIDs) { $sql = "INSERT INTO syncProcessLocks VALUES "; $sql .= implode(', ', array_fill(0, sizeOf($libraryIDs), '(?,?)')); $params = array(); foreach ($libraryIDs as $libraryID) { $params[] = $syncProcessID; $params[] = $libraryID; } Zotero_DB::query($sql, $params); } // Record the process id in the queue entry, if given if ($syncQueueID) { $sql = "UPDATE syncUploadQueue SET syncProcessID=? WHERE syncUploadQueueID=?"; Zotero_DB::query($sql, array($syncProcessID, $syncQueueID)); } Zotero_DB::commit(); return $syncProcessID; }
public function erase() { if (!$this->loaded) { Z_Core::debug("Not deleting unloaded group {$this->id}"); return; } Zotero_DB::beginTransaction(); $userIDs = self::getUsers(); $this->logGroupLibraryRemoval(); Zotero_Libraries::deleteCachedData($this->libraryID); Zotero_Libraries::clearAllData($this->libraryID); $sql = "DELETE FROM shardLibraries WHERE libraryID=?"; $deleted = Zotero_DB::query($sql, $this->libraryID, Zotero_Shards::getByLibraryID($this->libraryID)); if (!$deleted) { throw new Exception("Group not deleted"); } $sql = "DELETE FROM libraries WHERE libraryID=?"; $deleted = Zotero_DB::query($sql, $this->libraryID); if (!$deleted) { throw new Exception("Group not deleted"); } // Delete key permissions for this library, and then delete any keys // that had no other permissions $sql = "SELECT keyID FROM keyPermissions WHERE libraryID=?"; $keyIDs = Zotero_DB::columnQuery($sql, $this->libraryID); if ($keyIDs) { $sql = "DELETE FROM keyPermissions WHERE libraryID=?"; Zotero_DB::query($sql, $this->libraryID); $sql = "DELETE K FROM `keys` K LEFT JOIN keyPermissions KP USING (keyID)\n\t\t\t\t\tWHERE keyID IN (" . implode(', ', array_fill(0, sizeOf($keyIDs), '?')) . ") AND KP.keyID IS NULL"; Zotero_DB::query($sql, $keyIDs); } // If group is locked by a sync, flag group for a timestamp update // once the sync is done so that the uploading user gets the change try { foreach ($userIDs as $userID) { if ($syncUploadQueueID = Zotero_Sync::getUploadQueueIDByUserID($userID)) { Zotero_Sync::postWriteLog($syncUploadQueueID, 'group', $this->id, 'delete'); } } } catch (Exception $e) { Z_Core::logError($e); } Zotero_Notifier::trigger('delete', 'library', $this->libraryID); Zotero_DB::commit(); $this->erased = true; }
public static function getAllAdvanced($userID = false, $params = array(), $permissions = null) { $buffer = 20; $maxTimes = 3; $groups = array(); $start = !empty($params['start']) ? $params['start'] : 0; $limit = !empty($params['limit']) ? $params['limit'] + $buffer : false; $totalResults = null; $times = 0; while (true) { if ($times > 0) { Z_Core::logError('Getting more groups in Zotero_Groups::getAllAdvanced()'); } $calcFoundRows = !$totalResults; $cacheFoundRows = $calcFoundRows && !$userID; // If we don't yet have a row count and this isn't a user-specific search, // try to get a cached row count. if ($cacheFoundRows) { $foundRowsCacheKey = self::getCacheComponentFromParam($params, 'q') . "," . self::getCacheComponentFromParam($params, 'fq'); $foundRowsTTL = 180; $foundRowsLockTTL = 10; $foundRowsRealTTL = 3600; $obj = Z_Core::$MC->get($foundRowsCacheKey); if ($obj) { $foundRows = $obj['rows']; $exp = $obj['exp']; // If count was found but is past the expiration time, check if another // request is getting the row count, and fetch it if not if ($exp < time()) { if (!Z_Core::$MC->add($foundRowsCacheKey . "Lock", true, $foundRowsLockTTL)) { $calcFoundRows = false; } } else { $calcFoundRows = false; } } } $sql = "SELECT " . ($calcFoundRows ? "SQL_CALC_FOUND_ROWS " : "") . "G.groupID, GUO.userID AS ownerUserID FROM groups G " . "JOIN groupUsers GUO ON (G.groupID=GUO.groupID AND GUO.role='owner') "; $sqlParams = array(); if ($userID) { $sql .= "JOIN groupUsers GUA ON (G.groupID=GUA.groupID) WHERE GUA.userID=? "; $sqlParams[] = $userID; } $paramSQL = ""; $includeEmpty = false; if (!empty($params['q'])) { if (!is_array($params['q'])) { $params['q'] = array($params['q']); } foreach ($params['q'] as $q) { $field = explode(":", $q); if (sizeOf($field) == 2) { switch ($field[0]) { case 'slug': $includeEmpty = true; break; default: throw new Exception("Cannot search by group field '{$field[0]}'", Z_ERROR_INVALID_GROUP_TYPE); } $paramSQL .= "AND " . $field[0]; // If first character is '-', negate $paramSQL .= $field[0][0] == '-' ? '!' : ''; $paramSQL .= "=? "; $sqlParams[] = $field[1]; } else { $paramSQL .= "AND name LIKE ? "; $sqlParams[] = "%{$q}%"; } } } if (!$userID) { if ($includeEmpty) { $sql .= "WHERE 1 "; } else { // Don't include groups that have never had items $sql .= "JOIN libraries L ON (G.libraryID=L.libraryID)\n\t\t\t\t\t\t\tWHERE L.lastUpdated != '0000-00-00 00:00:00' "; } } $sql .= $paramSQL; if (!empty($params['fq'])) { if (!is_array($params['fq'])) { $params['fq'] = array($params['fq']); } foreach ($params['fq'] as $fq) { $facet = explode(":", $fq); if (sizeOf($facet) == 2 && preg_match('/-?GroupType/', $facet[0])) { switch ($facet[1]) { case 'PublicOpen': case 'PublicClosed': case 'Private': break; default: throw new Exception("Invalid group type '{$facet[1]}'", Z_ERROR_INVALID_GROUP_TYPE); } $sql .= "AND type"; // If first character is '-', negate $sql .= $facet[0][0] == '-' ? '!' : ''; $sql .= "=? "; $sqlParams[] = $facet[1]; } } } if (!empty($params['sort'])) { $order = $params['sort']; if ($order == 'title') { $order = 'name'; } $sql .= "ORDER BY {$order}"; if (!empty($params['direction'])) { $sql .= " " . $params['direction'] . " "; } } // Limit is set $buffer higher than the actual limit, in case some groups are // removed during access checks // // Actual limiting is done below if ($limit) { $sql .= "LIMIT ?, ?"; $sqlParams[] = $start; $sqlParams[] = $limit; } $rows = Zotero_DB::query($sql, $sqlParams); if (!$rows) { break; } if (is_null($totalResults)) { if ($calcFoundRows) { $foundRows = Zotero_DB::valueQuery("SELECT FOUND_ROWS()"); // Cache found rows count, and store earlier expiration time so that one // request can trigger a recalculation before cached value expires if ($cacheFoundRows) { Z_Core::$MC->set($foundRowsCacheKey, ['rows' => $foundRows, 'exp' => time() + $foundRowsTTL], $foundRowsRealTTL); } } $totalResults = $foundRows; } // Include only groups with non-banned owners $owners = array(); foreach ($rows as $row) { $owners[] = $row['ownerUserID']; } $owners = Zotero_Users::getValidUsers($owners); $ids = array(); foreach ($rows as $row) { if (!in_array($row['ownerUserID'], $owners)) { $totalResults--; continue; } $ids[] = $row['groupID']; } $batchStartPos = sizeOf($groups); foreach ($ids as $id) { $group = Zotero_Groups::get($id, true); $groups[] = $group; } // Remove groups that can't be accessed if ($permissions) { for ($i = $batchStartPos; $i < sizeOf($groups); $i++) { if (!$permissions->canAccess($groups[$i]->libraryID, 'view')) { array_splice($groups, $i, 1); $i--; $totalResults--; } } } $times++; if ($times == $maxTimes) { Z_Core::logError('Too many queries in Zotero_Groups::getAllAdvanced()'); break; } if (empty($params['limit'])) { break; } // If we have enough groups to fill the limit, stop if (sizeOf($groups) > $params['limit']) { break; } // If there no more rows, stop if ($start + sizeOf($rows) >= $foundRows) { break; } $start = $start + sizeOf($rows); // Get number we still need plus the buffer or all remaining, whichever is lower $limit = min($params['limit'] - sizeOf($groups) + $buffer, $foundRows - $start); } // TODO: generate previous start value if (!$groups) { return array('results' => array(), 'total' => 0); } // Fake limiting -- we can't just use SQL limit because // some groups might be inaccessible if (!empty($params['limit'])) { $groups = array_slice($groups, 0, $params['limit']); } $results = array('results' => $groups, 'total' => $totalResults); return $results; }
public static function doWeb($url, $sessionKey, $items = false) { if (!$sessionKey) { throw new Exception("Session key not provided"); } $servers = Z_CONFIG::$TRANSLATION_SERVERS; // Try servers in a random order shuffle($servers); $cacheKey = 'sessionTranslationServer_' . $sessionKey; $json = ["url" => $url, "sessionid" => $sessionKey]; if ($items) { $json['items'] = $items; // Send session requests to the same node if ($server = Z_Core::$MC->get($cacheKey)) { $servers = [$server]; } else { error_log("WARNING: Server not found for translation session"); } } $json = json_encode($json); foreach ($servers as $server) { $serverURL = "http://{$server}/web"; $start = microtime(true); $ch = curl_init($serverURL); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $json); curl_setopt($ch, CURLOPT_HTTPHEADER, array("Expect:", "Content-Type: application/json")); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 1); curl_setopt($ch, CURLOPT_TIMEOUT, 4); curl_setopt($ch, CURLOPT_HEADER, 0); // do not return HTTP headers curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $response = curl_exec($ch); $time = microtime(true) - $start; $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $mimeType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); if ($code != 200 && $code != 300) { // For explicit errors, trust translation server and bail with response code if ($code == 500 && strpos($response, "An error occurred during translation") !== false) { error_log("Error translating {$url}"); return 500; } else { if ($code == 501) { error_log("No translators found for {$url}"); return 501; } } // If unknown error, log and try another server $response = null; Z_Core::logError("HTTP {$code} from translate server {$server} translating URL"); Z_Core::logError($response); continue; } if (!$response) { $response = ""; } // Remember translation-server node for item selection if ($code == 300) { Z_Core::$MC->set($cacheKey, $server, 600); } break; } if ($response === null) { throw new Exception("Error from translation server"); } $response = json_decode($response); $obj = new stdClass(); // Multiple choices if ($code == 300) { $obj->select = $response; } else { $obj->items = $response; } return $obj; }
public static function authenticate($data) { $salt = Z_CONFIG::$AUTH_SALT; // TODO: config $dev = Z_ENV_TESTING_SITE ? "_test" : ""; $databaseName = "zotero_www{$dev}"; $username = $data['username']; $password = $data['password']; $isEmailAddress = strpos($username, '@') !== false; $cacheKey = 'userAuthHash_' . hash('sha256', $username . $password); $userID = Z_Core::$MC->get($cacheKey); if ($userID) { return $userID; } // Username if (!$isEmailAddress) { $sql = "SELECT userID, username, password AS hash FROM {$databaseName}.users WHERE username=?"; $params = [$username]; } else { $sql = "SELECT userID, username, password AS hash FROM {$databaseName}.users\n\t\t\t WHERE username = ?\n\t\t\t UNION\n\t\t\t SELECT userID, username, password AS hash FROM {$databaseName}.users\n\t\t\t WHERE email = ?\n\t\t\t ORDER BY username = ? DESC"; $params = [$username, $username, $username]; } try { $retry = true; $rows = Zotero_WWW_DB_2::query($sql, $params); if (!$rows) { $retry = false; $rows = Zotero_WWW_DB_1::query($sql, $params); } } catch (Exception $e) { if ($retry) { Z_Core::logError("WARNING: {$e} -- retrying on primary"); $rows = Zotero_WWW_DB_1::query($sql, $params); } } if (!$rows) { return false; } $found = false; foreach ($rows as $row) { // Try bcrypt $found = password_verify($password, $row['hash']); // Try salted SHA1 if (!$found) { $found = sha1($salt . $password) == $row['hash']; } // Try MD5 if (!$found) { $found = md5($password) == $row['hash']; } if ($found) { $foundRow = $row; break; } } if (!$found) { return false; } self::updateUser($foundRow['userID'], $foundRow['username']); Z_Core::$MC->set($cacheKey, $foundRow['userID'], 60); return $foundRow['userID']; }
private static function getUsernameFromWWW($userID) { $sql = "SELECT username FROM users WHERE userID=?"; try { $username = Zotero_WWW_DB_2::valueQuery($sql, $userID); } catch (Exception $e) { Z_Core::logError("WARNING: {$e} -- retrying on primary"); $username = Zotero_WWW_DB_1::valueQuery($sql, $userID); } if (!$username) { throw new Exception("User {$userID} not found", Z_ERROR_USER_NOT_FOUND); } return $username; }
public function set($key, $val, $exptime = 0) { if ($this->disabled) { return false; } if (!$key) { throw new Exception("Memcached key not provided"); } if ($this->queuing) { // If this is already set, mark the previous position for skipping when committing if (isset($this->queueKeyPos[$key])) { $pos = $this->queueKeyPos[$key]; $this->queue[$pos]['skip'] = true; } $this->queue[] = array('op' => 'set', 'key' => $key, 'exp' => $exptime); $this->queueValues[$key] = $val; // Store the position in case we need to clear later $this->queueKeyPos[$key] = sizeOf($this->queue) - 1; return true; } $success = $this->client->set($this->prefix . $key, $val, null, $exptime); if (!$success && !$this->errorLogged) { Z_Core::logError("Setting memcache value failed (key {$key}, value {$val})"); $this->errorLogged = true; } return $success; }
private function getUserPrivacy($userID) { if (isset($this->userPrivacy[$userID])) { return $this->userPrivacy[$userID]; } if (Z_ENV_DEV_SITE) { // Hard-coded test values $privacy = array(); switch ($userID) { case 1: $privacy['library'] = true; $privacy['notes'] = true; break; case 2: $privacy['library'] = false; $privacy['notes'] = false; break; default: throw new Exception("External requests disabled on dev site"); } $this->userPrivacy[$userID] = $privacy; return $privacy; } $sql = "SELECT metaKey, metaValue FROM users_meta WHERE userID=? AND metaKey LIKE 'privacy_publish%'"; try { $rows = Zotero_WWW_DB_2::query($sql, $userID); } catch (Exception $e) { Z_Core::logError("WARNING: {$e} -- retrying on primary"); $rows = Zotero_WWW_DB_1::query($sql, $userID); } $privacy = array('library' => false, 'notes' => false); foreach ($rows as $row) { $privacy[strtolower(substr($row['metaKey'], 15))] = (bool) (int) $row['metaValue']; } $this->userPrivacy[$userID] = $privacy; return $privacy; }
public static function authenticate($data) { $salt = Z_CONFIG::$AUTH_SALT; // TODO: config $dev = Z_ENV_TESTING_SITE ? "_test" : ""; $databaseName = "zotero_www{$dev}"; $username = $data['username']; $password = $data['password']; $isEmailAddress = strpos($username, '@') !== false; $cacheKey = 'userAuthHash_' . sha1($username . $salt . $password); $userID = Z_Core::$MC->get($cacheKey); if ($userID) { return $userID; } // Query the database looking for a salted SHA1 password $passwordSha1 = sha1($salt . $password); if (!$isEmailAddress) { // Try username $sql = "SELECT userID, username FROM {$databaseName}.users\n\t\t\t\t\t WHERE username = ? AND password = ?\n\t\t\t\t\t LIMIT 1"; $params = array($username, $passwordSha1); try { $retry = true; $row = Zotero_WWW_DB_2::rowQuery($sql, $params); if (!$row) { $retry = false; $row = Zotero_WWW_DB_1::rowQuery($sql, $params); } } catch (Exception $e) { if ($retry) { Z_Core::logError("WARNING: {$e} -- retrying on primary"); $row = Zotero_WWW_DB_1::rowQuery($sql, $params); } } } else { // Try both username and e-mail address $sql = "SELECT userID, username FROM {$databaseName}.users\n\t\t\t\t\t WHERE username = ? AND password = ?\n\t\t\t\t\t UNION\n\t\t\t\t\t SELECT userID, username FROM {$databaseName}.users\n\t\t\t\t\t WHERE email = ? AND password = ?\n\t\t\t\t\t ORDER BY username = ? DESC\n\t\t\t\t\t LIMIT 1"; $params = array($username, $passwordSha1, $username, $passwordSha1, $username); try { $retry = true; $row = Zotero_WWW_DB_2::rowQuery($sql, $params); if (!$row) { $retry = false; $row = Zotero_WWW_DB_1::rowQuery($sql, $params); } } catch (Exception $e) { if ($retry) { Z_Core::logError("WARNING: {$e} -- retrying on primary"); $row = Zotero_WWW_DB_1::rowQuery($sql, $params); } } } // If not found, check for an MD5 password if (!$row) { $passwordMd5 = md5($password); if (!$isEmailAddress) { // Try username $sql = "SELECT userID, username FROM {$databaseName}.users\n\t\t\t\t\t\t WHERE username = ? AND password = ? LIMIT 1"; $params = array($username, $passwordMd5); try { $row = Zotero_WWW_DB_2::rowQuery($sql, $params); } catch (Exception $e) { Z_Core::logError("WARNING: {$e} -- retrying on primary"); $row = Zotero_WWW_DB_1::rowQuery($sql, $params); } } else { // Try both username and e-mail address $sql = "SELECT userID, username FROM {$databaseName}.users\n\t\t\t\t\t\t WHERE username = ? AND password = ?\n\t\t\t\t\t\t UNION\n\t\t\t\t\t\t SELECT userID, username FROM {$databaseName}.users\n\t\t\t\t\t\t WHERE email = ? AND password = ?\n\t\t\t\t\t\t ORDER BY username = ? DESC\n\t\t\t\t\t\t LIMIT 1"; $params = array($username, $passwordMd5, $username, $passwordMd5, $username); try { $row = Zotero_WWW_DB_2::rowQuery($sql, $params); } catch (Exception $e) { Z_Core::logError("WARNING: {$e} -- retrying on primary"); $row = Zotero_WWW_DB_1::rowQuery($sql, $params); } } } if (!$row) { return false; } self::updateUser($row['userID'], $row['username']); Z_Core::$MC->set($cacheKey, $row['userID'], 60); return $row['userID']; }
public function set($key, $val, $exptime = 0) { if ($this->disabled) { return false; } if (!$key) { throw new Exception("Memcached key not provided"); } if ($this->queuing) { // If this is already set, mark the previous position for skipping when committing if (isset($this->queueKeyPos[$key])) { $pos = $this->queueKeyPos[$key]; $this->queue[$pos]['skip'] = true; } $this->queue[] = array('op' => 'set', 'key' => $key, 'exp' => $exptime); $this->queueValues[$key] = $val; // Store the position in case we need to clear later $this->queueKeyPos[$key] = sizeOf($this->queue) - 1; return true; } $t = microtime(true); $success = $this->client->set($key, $val, $exptime); $this->requestTime += microtime(true) - $t; if (!$success && !$this->errorLogged) { Z_Core::logError("Setting memcache value failed for key {$key}: " . $this->client->getResultMessage() . " (" . $this->client->getServerByKey($key)['host'] . ")"); $this->errorLogged = true; } return $success; }
private function handleUploadError(Exception $e, $xmldata) { $msg = $e->getMessage(); if ($msg[0] == '=') { $msg = substr($msg, 1); $explicit = true; // TODO: more specific error messages } else { $explicit = false; } switch ($e->getCode()) { case Z_ERROR_TAG_TOO_LONG: case Z_ERROR_COLLECTION_TOO_LONG: break; default: Z_Core::logError($msg); } if (!$explicit && Z_ENV_TESTING_SITE) { switch ($e->getCode()) { case Z_ERROR_COLLECTION_NOT_FOUND: case Z_ERROR_CREATOR_NOT_FOUND: case Z_ERROR_ITEM_NOT_FOUND: case Z_ERROR_TAG_TOO_LONG: case Z_ERROR_LIBRARY_ACCESS_DENIED: case Z_ERROR_TAG_LINKED_ITEM_NOT_FOUND: break; default: throw $e; } $id = 'N/A'; } else { $id = substr(md5(uniqid(rand(), true)), 0, 8); $str = date("D M j G:i:s T Y") . "\n"; $str .= "IP address: " . $_SERVER['REMOTE_ADDR'] . "\n"; if (isset($_SERVER['HTTP_X_ZOTERO_VERSION'])) { $str .= "Version: " . $_SERVER['HTTP_X_ZOTERO_VERSION'] . "\n"; } $str .= $msg; switch ($e->getCode()) { // Don't log uploaded data for some errors case Z_ERROR_TAG_TOO_LONG: case Z_ERROR_FIELD_TOO_LONG: case Z_ERROR_NOTE_TOO_LONG: case Z_ERROR_COLLECTION_TOO_LONG: break; default: $str .= "\n\n" . $xmldata; } if (!file_put_contents(Z_CONFIG::$SYNC_ERROR_PATH . $id, $str)) { error_log("Unable to save error report to " . Z_CONFIG::$SYNC_ERROR_PATH . $id); } } Zotero_DB::rollback(true); switch ($e->getCode()) { case Z_ERROR_LIBRARY_ACCESS_DENIED: preg_match('/[Ll]ibrary ([0-9]+)/', $e->getMessage(), $matches); $libraryID = $matches ? $matches[1] : null; $this->error(400, 'LIBRARY_ACCESS_DENIED', "Cannot make changes to library (Report ID: {$id})", array('libraryID' => $libraryID)); break; case Z_ERROR_ITEM_NOT_FOUND: case Z_ERROR_COLLECTION_NOT_FOUND: case Z_ERROR_CREATOR_NOT_FOUND: error_log($e); $this->error(500, "FULL_SYNC_REQUIRED", "Please perform a full sync in the Sync->Reset pane of the Zotero preferences. (Report ID: {$id})"); break; case Z_ERROR_TAG_TOO_LONG: $message = $e->getMessage(); preg_match("/Tag '(.+)' too long/s", $message, $matches); if ($matches) { $name = $matches[1]; $this->error(400, "TAG_TOO_LONG", "Tag '" . mb_substr($name, 0, 50) . "…' too long", array(), array("tag" => $name)); } break; case Z_ERROR_COLLECTION_TOO_LONG: $message = $e->getMessage(); preg_match("/Collection '(.+)' too long/s", $message, $matches); if ($matches) { $name = $matches[1]; $this->error(400, "COLLECTION_TOO_LONG", "Collection '" . mb_substr($name, 0, 50) . "…' too long", array(), array("collection" => $name)); } break; case Z_ERROR_NOTE_TOO_LONG: preg_match("/Note '(.+)' too long(?: for item '(.+)\\/(.+)')?/s", $msg, $matches); if ($matches) { $name = $matches[1]; $libraryID = false; if (isset($matches[2])) { $libraryID = (int) $matches[2]; $itemKey = $matches[3]; if (Zotero_Libraries::getType($libraryID) == 'group') { $groupID = Zotero_Groups::getGroupIDFromLibraryID($libraryID); $group = Zotero_Groups::get($groupID); $libraryName = $group->name; } else { $libraryName = false; } } else { $itemKey = ''; } $showNoteKey = false; if (isset($_SERVER['HTTP_X_ZOTERO_VERSION'])) { require_once '../model/ToolkitVersionComparator.inc.php'; $showNoteKey = ToolkitVersionComparator::compare($_SERVER['HTTP_X_ZOTERO_VERSION'], "4.0.27") < 0; } if ($showNoteKey) { $this->error(400, "ERROR_PROCESSING_UPLOAD_DATA", "The note '" . mb_substr($name, 0, 50) . "…' in " . ($libraryName === false ? "your library " : "the group '{$libraryName}' ") . "is too long to sync to zotero.org.\n\n" . "Search for the excerpt above or copy and paste " . "'{$itemKey}' into the Zotero search bar. " . "Shorten the note, or delete it and empty the Zotero " . "trash, and then try syncing again."); } else { $this->error(400, "NOTE_TOO_LONG", "The note '" . mb_substr($name, 0, 50) . "…' in " . ($libraryName === false ? "your library " : "the group '{$libraryName}' ") . "is too long to sync to zotero.org.\n\n" . "Shorten the note, or delete it and empty the Zotero " . "trash, and then try syncing again.", [], $libraryID ? ["item" => $libraryID . "/" . $itemKey] : []); } } break; case Z_ERROR_FIELD_TOO_LONG: preg_match("/(.+) field value '(.+)\\.\\.\\.' too long(?: for item '(.+)')?/s", $msg, $matches); if ($matches) { $fieldName = $matches[1]; $value = $matches[2]; if (isset($matches[3])) { $parts = explode("/", $matches[3]); $libraryID = (int) $parts[0]; $itemKey = $parts[1]; if (Zotero_Libraries::getType($libraryID) == 'group') { $groupID = Zotero_Groups::getGroupIDFromLibraryID($libraryID); $group = Zotero_Groups::get($groupID); $libraryName = "the group '" . $group->name . "'"; } else { $libraryName = "your personal library"; } } else { $libraryName = "one of your libraries"; $itemKey = false; } $this->error(400, "ERROR_PROCESSING_UPLOAD_DATA", "The {$fieldName} field value '{$value}…' in {$libraryName} is " . "too long to sync to zotero.org.\n\n" . "Search for the excerpt above " . ($itemKey === false ? "using " : "or copy and paste " . "'{$itemKey}' into ") . "the Zotero search bar. " . "Shorten the field, or delete the item and empty the " . "Zotero trash, and then try syncing again."); } break; case Z_ERROR_ARRAY_SIZE_MISMATCH: $this->error(400, 'DATABASE_TOO_LARGE', "Databases of this size cannot yet be synced. Please check back soon. (Report ID: {$id})"); break; case Z_ERROR_TAG_LINKED_ITEM_NOT_FOUND: $this->error(400, 'WRONG_LIBRARY_TAG_ITEM', "Error processing uploaded data (Report ID: {$id})"); break; case Z_ERROR_SHARD_READ_ONLY: case Z_ERROR_SHARD_UNAVAILABLE: $this->error(503, 'SERVER_ERROR', Z_CONFIG::$MAINTENANCE_MESSAGE); break; } if (strpos($msg, "Lock wait timeout exceeded; try restarting transaction") !== false || strpos($msg, "MySQL error: Deadlock found when trying to get lock; try restarting transaction") !== false) { $this->error(500, 'TIMEOUT', "Sync upload timed out. Please try again in a few minutes. (Report ID: {$id})"); } if (strpos($msg, "Data too long for column 'xmldata'") !== false) { $this->error(400, 'DATABASE_TOO_LARGE', "Databases of this size cannot yet be synced. Please check back soon. (Report ID: {$id})"); } // On certain messages, send 400 to prevent auto-retry if (strpos($msg, " too long") !== false || strpos($msg, "First and last name are empty") !== false) { $this->error(400, 'ERROR_PROCESSING_UPLOAD_DATA', $explicit ? $msg : "Error processing uploaded data (Report ID: {$id})"); } if (preg_match("/Incorrect datetime value: '([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2})' " . "for column 'date(Added|Modified)'/", $msg, $matches)) { if (isset($_SERVER['HTTP_X_ZOTERO_VERSION'])) { require_once '../model/ToolkitVersionComparator.inc.php'; if (ToolkitVersionComparator::compare($_SERVER['HTTP_X_ZOTERO_VERSION'], "2.1rc1") < 0) { $msg = "Invalid timestamp '{$matches[1]}' in uploaded data. Upgrade to Zotero 2.1rc1 when available to fix automatically."; } else { $msg = "Invalid timestamp '{$matches[1]}' in uploaded data. Sync again to correct automatically."; } } else { $msg = "Invalid timestamp '{$matches[1]}' in uploaded data. Upgrade to Zotero 2.1rc1 when available to fix automatically."; } $this->error(400, 'INVALID_TIMESTAMP', $msg); } $this->error(500, 'ERROR_PROCESSING_UPLOAD_DATA', $explicit ? $msg : "Error processing uploaded data (Report ID: {$id})"); }
private static function extractZip($file, $destDir) { $za = new ZipArchive(); $za->open($file); $entries = array(); for ($i = 0, $max = $za->numFiles; $i < $max; $i++) { $stat = $za->statIndex($i); // Skip files not at the top level if ($stat['name'] != basename($stat['name'])) { continue; } // Skip dot files or ztmp (which we use as temp dir) if ($stat['name'][0] == '.' || $stat['name'] == 'ztmp') { continue; } if (preg_match("/%ZB64\$/", $stat['name'])) { $filename = Z_Base64::decode(substr($stat['name'], 0, -5)); $filename = self::decodeRelativeDescriptorString($filename); $za->renameIndex($i, $filename); } else { $filename = $stat['name']; } $entries[] = $filename; } $success = $za->extractTo($destDir, $entries); $za->close(); if (!$success) { Z_Core::logError($za->getStatusString()); } return $success; }
private function handleUploadError(Exception $e, $xmldata) { $msg = $e->getMessage(); if ($msg[0] == '=') { $msg = substr($msg, 1); $explicit = true; // TODO: more specific error messages } else { $explicit = false; } switch ($e->getCode()) { case Z_ERROR_TAG_TOO_LONG: break; default: Z_Core::logError($msg); } if (true || !$explicit) { if (Z_ENV_TESTING_SITE) { switch ($e->getCode()) { case Z_ERROR_COLLECTION_NOT_FOUND: case Z_ERROR_CREATOR_NOT_FOUND: case Z_ERROR_ITEM_NOT_FOUND: case Z_ERROR_TAG_TOO_LONG: case Z_ERROR_LIBRARY_ACCESS_DENIED: case Z_ERROR_TAG_LINKED_ITEM_NOT_FOUND: break; default: throw $e; } $id = 'N/A'; } else { $id = substr(md5(uniqid(rand(), true)), 0, 8); $str = date("D M j G:i:s T Y") . "\n"; $str .= "IP address: " . $_SERVER['REMOTE_ADDR'] . "\n"; if (isset($_SERVER['HTTP_X_ZOTERO_VERSION'])) { $str .= "Version: " . $_SERVER['HTTP_X_ZOTERO_VERSION'] . "\n"; } $str .= $e; switch ($e->getCode()) { // Don't log uploaded data for some errors case Z_ERROR_TAG_TOO_LONG: break; default: $str .= "\n\n" . $xmldata; } file_put_contents(Z_CONFIG::$SYNC_ERROR_PATH . $id, $str); } } Zotero_DB::rollback(true); switch ($e->getCode()) { case Z_ERROR_LIBRARY_ACCESS_DENIED: preg_match('/[Ll]ibrary ([0-9]+)/', $e->getMessage(), $matches); $libraryID = $matches ? $matches[1] : null; $this->error(400, 'LIBRARY_ACCESS_DENIED', "Cannot make changes to library (Report ID: {$id})", array('libraryID' => $libraryID)); break; case Z_ERROR_ITEM_NOT_FOUND: case Z_ERROR_COLLECTION_NOT_FOUND: case Z_ERROR_CREATOR_NOT_FOUND: $this->error(500, "FULL_SYNC_REQUIRED", "Please perform a full sync in the Sync->Reset pane of the Zotero preferences. (Report ID: {$id})"); break; case Z_ERROR_TAG_TOO_LONG: $message = $e->getMessage(); preg_match("/Tag '(.+)' too long/s", $message, $matches); if ($matches) { $name = $matches[1]; $this->error(400, "TAG_TOO_LONG", "Tag '" . mb_substr($name, 0, 50) . "…' too long", array(), array("tag" => $name)); } break; case Z_ERROR_COLLECTION_TOO_LONG: $message = $e->getMessage(); preg_match("/Collection '(.+)' too long/s", $message, $matches); if ($matches) { $name = $matches[1]; $this->error(400, "COLLECTION_TOO_LONG", "Collection '" . mb_substr($name, 0, 50) . "…' too long", array(), array("collection" => $name)); } break; case Z_ERROR_ARRAY_SIZE_MISMATCH: $this->error(400, 'DATABASE_TOO_LARGE', "Databases of this size cannot yet be synced. Please check back soon. (Report ID: {$id})"); break; case Z_ERROR_TAG_LINKED_ITEM_NOT_FOUND: $this->error(400, 'WRONG_LIBRARY_TAG_ITEM', "Error processing uploaded data (Report ID: {$id})"); break; case Z_ERROR_SHARD_READ_ONLY: case Z_ERROR_SHARD_UNAVAILABLE: $this->error(503, 'SERVER_ERROR', Z_CONFIG::$MAINTENANCE_MESSAGE); break; } if (strpos($msg, "Lock wait timeout exceeded; try restarting transaction") !== false || strpos($msg, "MySQL error: Deadlock found when trying to get lock; try restarting transaction") !== false) { $this->error(500, 'TIMEOUT', "Sync upload timed out. Please try again in a few minutes. (Report ID: {$id})"); } if (strpos($msg, "Data too long for column 'xmldata'") !== false) { $this->error(400, 'DATABASE_TOO_LARGE', "Databases of this size cannot yet be synced. Please check back soon. (Report ID: {$id})"); } // On certain messages, send 400 to prevent auto-retry if (strpos($msg, " too long") !== false || strpos($msg, "First and last name are empty") !== false) { $this->error(400, 'ERROR_PROCESSING_UPLOAD_DATA', $explicit ? $msg : "Error processing uploaded data (Report ID: {$id})"); } if (preg_match("/Incorrect datetime value: '([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2})' " . "for column 'date(Added|Modified)'/", $msg, $matches)) { if (isset($_SERVER['HTTP_X_ZOTERO_VERSION'])) { require_once '../model/ToolkitVersionComparator.inc.php'; if (ToolkitVersionComparator::compare($_SERVER['HTTP_X_ZOTERO_VERSION'], "2.1rc1") < 0) { $msg = "Invalid timestamp '{$matches[1]}' in uploaded data. Upgrade to Zotero 2.1rc1 when available to fix automatically."; } else { $msg = "Invalid timestamp '{$matches[1]}' in uploaded data. Sync again to correct automatically."; } } else { $msg = "Invalid timestamp '{$matches[1]}' in uploaded data. Upgrade to Zotero 2.1rc1 when available to fix automatically."; } $this->error(400, 'INVALID_TIMESTAMP', $msg); } $this->error(500, 'ERROR_PROCESSING_UPLOAD_DATA', $explicit ? $msg : "Error processing uploaded data (Report ID: {$id})"); }