public static function getLibraryURI($libraryID) { $libraryType = Zotero_Libraries::getType($libraryID); switch ($libraryType) { case 'user': $id = Zotero_Users::getUserIDFromLibraryID($libraryID); return self::getUserURI($id); case 'group': $id = Zotero_Groups::getGroupIDFromLibraryID($libraryID); $group = Zotero_Groups::get($id); return self::getGroupURI($group); } }
public function init($extra) { $this->startTime = microtime(true); if (!Z_CONFIG::$API_ENABLED) { $this->e503(Z_CONFIG::$MAINTENANCE_MESSAGE); } set_exception_handler(array($this, 'handleException')); // TODO: Throw error on some notices but allow DB/Memcached/etc. failures? //set_error_handler(array($this, 'handleError'), E_ALL | E_USER_ERROR | E_RECOVERABLE_ERROR); set_error_handler(array($this, 'handleError'), E_USER_ERROR | E_RECOVERABLE_ERROR); require_once '../model/Error.inc.php'; // On testing sites, include notifications in headers if (Z_CONFIG::$TESTING_SITE) { Zotero_NotifierObserver::addMessageReceiver(function ($topic, $msg) { $header = "Zotero-Debug-Notifications"; if (!empty($this->headers[$header])) { $notifications = json_decode(base64_decode($this->headers[$header])); } else { $notifications = []; } $notifications[] = $msg; $this->headers[$header] = base64_encode(json_encode($notifications)); }); } register_shutdown_function(array($this, 'checkDBTransactionState')); register_shutdown_function(array($this, 'logTotalRequestTime')); register_shutdown_function(array($this, 'checkForFatalError')); register_shutdown_function(array($this, 'addHeaders')); $this->method = $_SERVER['REQUEST_METHOD']; if (!in_array($this->method, array('HEAD', 'OPTIONS', 'GET', 'PUT', 'POST', 'DELETE', 'PATCH'))) { $this->e501(); } StatsD::increment("api.request.method." . strtolower($this->method), 0.25); // There doesn't seem to be a way for PHP to start processing the request // before the entire body is sent, so an Expect: 100 Continue will, // depending on the client, either fail or cause a delay while the client // waits for the 100 response. To make this explicit, we return an error. if (!empty($_SERVER['HTTP_EXPECT'])) { header("HTTP/1.1 417 Expectation Failed"); die("Expect header is not supported"); } // CORS if (isset($_SERVER['HTTP_ORIGIN'])) { header("Access-Control-Allow-Origin: *"); header("Access-Control-Allow-Methods: HEAD, GET, POST, PUT, PATCH, DELETE"); header("Access-Control-Allow-Headers: Content-Type, If-Match, If-None-Match, If-Modified-Since-Version, If-Unmodified-Since-Version, Zotero-API-Version, Zotero-Write-Token"); header("Access-Control-Expose-Headers: Backoff, ETag, Last-Modified-Version, Link, Retry-After, Total-Results, Zotero-API-Version"); } if ($this->method == 'OPTIONS') { $this->end(); } if (in_array($this->method, array('POST', 'PUT', 'PATCH'))) { $this->ifUnmodifiedSince = isset($_SERVER['HTTP_IF_UNMODIFIED_SINCE']) ? strtotime($_SERVER['HTTP_IF_UNMODIFIED_SINCE']) : false; $this->body = file_get_contents("php://input"); if ($this->body == "" && !in_array($this->action, array('clear', 'laststoragesync', 'removestoragefiles', 'itemContent'))) { $this->e400("{$this->method} data not provided"); } } if ($this->profile) { Zotero_DB::profileStart(); } // If HTTP Basic Auth credentials provided, authenticate if (isset($_SERVER['PHP_AUTH_USER'])) { $username = $_SERVER['PHP_AUTH_USER']; $password = $_SERVER['PHP_AUTH_PW']; if ($username == Z_CONFIG::$API_SUPER_USERNAME && $password == Z_CONFIG::$API_SUPER_PASSWORD) { $this->userID = 0; $this->permissions = new Zotero_Permissions(); $this->permissions->setSuper(); } else { if (!empty($extra['allowHTTP']) || !empty($extra['auth'])) { $userID = Zotero_Users::authenticate('password', array('username' => $username, 'password' => $password)); if (!$userID) { $this->e401('Invalid login'); } $this->httpAuth = true; $this->userID = $userID; $this->grantUserPermissions($userID); } } } if (!isset($this->userID)) { $key = false; // Allow Zotero-API-Key header if (!empty($_SERVER['HTTP_ZOTERO_API_KEY'])) { $key = $_SERVER['HTTP_ZOTERO_API_KEY']; } // Allow ?key=<apikey> if (isset($_GET['key'])) { if (!$key) { $key = $_GET['key']; } else { if ($_GET['key'] !== $key) { $this->e400("Zotero-API-Key header and 'key' parameter differ"); } } } // If neither of the above passed, allow "Authorization: Bearer <apikey>" // // Apache/mod_php doesn't seem to make Authorization available for auth schemes // other than Basic/Digest, so use an Apache-specific method to get the header if (!$key && function_exists('apache_request_headers')) { $headers = apache_request_headers(); if (isset($headers['Authorization'])) { // Look for "Authorization: Bearer" from OAuth 2.0, and ignore everything else if (preg_match('/^bearer/i', $headers['Authorization'], $matches)) { if (preg_match('/^bearer +([a-z0-9]+)$/i', $headers['Authorization'], $matches)) { $key = $matches[1]; } else { $this->e400("Invalid Authorization header format"); } } } } if ($key) { $keyObj = Zotero_Keys::authenticate($key); if (!$keyObj) { $this->e403('Invalid key'); } $this->apiKey = $key; $this->userID = $keyObj->userID; $this->permissions = $keyObj->getPermissions(); // Check Zotero-Write-Token if it exists to make sure // this isn't a duplicate request if ($this->isWriteMethod()) { if ($cacheKey = $this->getWriteTokenCacheKey()) { if (Z_Core::$MC->get($cacheKey)) { $this->e412("Write token already used"); } } } } else { if (!empty($_GET['session']) && ($this->userID = Zotero_Users::getUserIDFromSessionID($_GET['session']))) { // Users who haven't synced may not exist in our DB if (!Zotero_Users::exists($this->userID)) { Zotero_Users::add($this->userID); } $this->grantUserPermissions($this->userID); $this->cookieAuth = true; } else { if (!empty($_GET['auth']) || !empty($extra['auth'])) { $this->e401(); } // Explicit auth request or not a GET request // // /users/<id>/keys is an exception, since the key is embedded in the URL if ($this->method != "GET" && $this->action != 'keys') { $this->e403('An API key is required for write requests.'); } // Anonymous request $this->permissions = new Zotero_Permissions(); $this->permissions->setAnonymous(); } } } $this->uri = Z_CONFIG::$API_BASE_URI . substr($_SERVER["REQUEST_URI"], 1); // Get object user if (isset($this->objectUserID)) { if (!$this->objectUserID) { $this->e400("Invalid user ID", Z_ERROR_INVALID_INPUT); } try { $this->objectLibraryID = Zotero_Users::getLibraryIDFromUserID($this->objectUserID); } catch (Exception $e) { if ($e->getCode() == Z_ERROR_USER_NOT_FOUND) { try { Zotero_Users::addFromWWW($this->objectUserID); } catch (Exception $e) { if ($e->getCode() == Z_ERROR_USER_NOT_FOUND) { $this->e404("User {$this->objectUserID} not found"); } throw $e; } $this->objectLibraryID = Zotero_Users::getLibraryIDFromUserID($this->objectUserID); } else { throw $e; } } // Make sure user isn't banned if (!Zotero_Users::isValidUser($this->objectUserID)) { $this->e404(); } } else { if (isset($this->objectGroupID)) { if (!$this->objectGroupID) { $this->e400("Invalid group ID", Z_ERROR_INVALID_INPUT); } // Make sure group exists $group = Zotero_Groups::get($this->objectGroupID); if (!$group) { $this->e404(); } // Don't show groups owned by banned users if (!Zotero_Users::isValidUser($group->ownerUserID)) { $this->e404(); } $this->objectLibraryID = Zotero_Groups::getLibraryIDFromGroupID($this->objectGroupID); } } $apiVersion = !empty($_SERVER['HTTP_ZOTERO_API_VERSION']) ? (int) $_SERVER['HTTP_ZOTERO_API_VERSION'] : false; // Serve v1 to ZotPad 1.x, at Mikko's request if (!$apiVersion && !empty($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'ZotPad 1') === 0) { $apiVersion = 1; } // For publications URLs (e.g., /users/:userID/publications/items), swap in // objectLibraryID of user's publications library if (!empty($extra['publications'])) { // Query parameters not yet parsed, so check version parameter if ($apiVersion && $apiVersion < 3 || !empty($_REQUEST['v']) && $_REQUEST['v'] < 3 || !empty($_REQUEST['version']) && $_REQUEST['version'] == 1) { $this->e404(); } $userLibraryID = $this->objectLibraryID; $this->objectLibraryID = Zotero_Users::getLibraryIDFromUserID($this->objectUserID, 'publications'); // If one doesn't exist, for write requests create a library if the key // has write permission to the user library. For read requests, just // return a 404. if (!$this->objectLibraryID) { if ($this->isWriteMethod()) { if (!$this->permissions->canAccess($userLibraryID) || !$this->permissions->canWrite($userLibraryID)) { $this->e403(); } $this->objectLibraryID = Zotero_Publications::add($this->objectUserID); } else { $this->objectLibraryID = 0; } } } // Return 409 if target library is locked switch ($this->method) { case 'POST': case 'PUT': case 'DELETE': switch ($this->action) { // Library lock doesn't matter for some admin requests case 'keys': case 'storageadmin': break; default: if ($this->objectLibraryID && Zotero_Libraries::isLocked($this->objectLibraryID)) { $this->e409("Target library is locked"); } break; } } $this->scopeObject = !empty($extra['scopeObject']) ? $extra['scopeObject'] : $this->scopeObject; $this->subset = !empty($extra['subset']) ? $extra['subset'] : $this->subset; $this->fileMode = !empty($extra['file']) ? !empty($_GET['info']) ? 'info' : 'download' : false; $this->fileView = !empty($extra['view']); $this->singleObject = $this->objectKey && !$this->subset; $this->checkLibraryIfModifiedSinceVersion($this->action); // If Accept header includes application/atom+xml, send Atom, as long as there's no 'format' $atomAccepted = false; if (!empty($_SERVER['HTTP_ACCEPT'])) { $accept = preg_split('/\\s*,\\s*/', $_SERVER['HTTP_ACCEPT']); $atomAccepted = in_array('application/atom+xml', $accept); } $this->queryParams = Zotero_API::parseQueryParams($_SERVER['QUERY_STRING'], $this->action, $this->singleObject, $apiVersion, $atomAccepted); // Sorting by Item Type or Added By currently require writing to shard tables, so don't // send those to the read replicas if ($this->queryParams['sort'] == 'itemType' || $this->queryParams['sort'] == 'addedBy') { Zotero_DB::readOnly(false); } $this->apiVersion = $version = $this->queryParams['v']; header("Zotero-API-Version: " . $version); StatsD::increment("api.request.version.v" . $version, 0.25); }
public static function getUserUsage($userID) { $usage = array(); $libraryID = Zotero_Users::getLibraryIDFromUserID($userID); $sql = "SELECT SUM(size) AS bytes FROM storageFileItems\n\t\t\t\tJOIN items USING (itemID) WHERE libraryID=?"; $libraryBytes = Zotero_DB::valueQuery($sql, $libraryID, Zotero_Shards::getByLibraryID($libraryID)); $usage['library'] = round($libraryBytes / 1024 / 1024, 1); $groupBytes = 0; $usage['groups'] = array(); $ownedLibraries = Zotero_Groups::getUserOwnedGroupLibraries($userID); if ($ownedLibraries) { $shardIDs = Zotero_Groups::getUserGroupShards($userID); foreach ($shardIDs as $shardID) { $sql = "SELECT libraryID, SUM(size) AS `bytes` FROM storageFileItems\n\t\t\t\t\t\tJOIN items I USING (itemID)\n\t\t\t\t\t\tWHERE libraryID IN\n\t\t\t\t\t\t(" . implode(', ', array_fill(0, sizeOf($ownedLibraries), '?')) . ")\n\t\t\t\t\t\tGROUP BY libraryID WITH ROLLUP"; $libraries = Zotero_DB::query($sql, $ownedLibraries, $shardID); if ($libraries) { foreach ($libraries as $library) { if ($library['libraryID']) { $usage['groups'][] = array('id' => Zotero_Groups::getGroupIDFromLibraryID($library['libraryID']), 'usage' => round($library['bytes'] / 1024 / 1024, 1)); } else { $groupBytes += $library['bytes']; } } } } } $usage['total'] = round(($libraryBytes + $groupBytes) / 1024 / 1024, 1); return $usage; }
private static function getDeletedObjectIDs($userID, $timestamp, $includeAllUserObjects = false) { /* $sql = "SELECT version FROM version WHERE schema='syncdeletelog'"; $syncLogStart = Zotero_DB::valueQuery($sql); if (!$syncLogStart) { throw ('Sync log start time not found'); } */ /* // Last sync time is before start of log if ($lastSyncDate && new Date($syncLogStart * 1000) > $lastSyncDate) { return -1; } */ // Personal library $shardID = Zotero_Shards::getByUserID($userID); $libraryID = Zotero_Users::getLibraryIDFromUserID($userID); $shardLibraryIDs[$shardID] = array($libraryID); // Group libraries if ($includeAllUserObjects) { $groupIDs = Zotero_Groups::getUserGroups($userID); if ($groupIDs) { // Separate groups into shards for querying foreach ($groupIDs as $groupID) { $libraryID = Zotero_Groups::getLibraryIDFromGroupID($groupID); $shardID = Zotero_Shards::getByLibraryID($libraryID); if (!isset($shardLibraryIDs[$shardID])) { $shardLibraryIDs[$shardID] = array(); } $shardLibraryIDs[$shardID][] = $libraryID; } } } // Send query at each shard $rows = array(); foreach ($shardLibraryIDs as $shardID => $libraryIDs) { $sql = "SELECT libraryID, objectType, id, timestamp\n\t\t\t\t\tFROM syncDeleteLogIDs WHERE libraryID IN (" . implode(', ', array_fill(0, sizeOf($libraryIDs), '?')) . ")"; $params = $libraryIDs; if ($timestamp) { // Send any entries from before these were being properly sent if ($timestamp < 1260778500) { $sql .= " AND (timestamp >= FROM_UNIXTIME(?) OR timestamp BETWEEN 1257968068 AND FROM_UNIXTIME(?))"; $params[] = $timestamp; $params[] = 1260778500; } else { $sql .= " AND timestamp >= FROM_UNIXTIME(?)"; $params[] = $timestamp; } } $sql .= " ORDER BY timestamp"; $shardRows = Zotero_DB::query($sql, $params, $shardID); if ($shardRows) { $rows = array_merge($rows, $shardRows); } } if (!$rows) { return false; } $deletedIDs = array('groups' => array()); foreach ($rows as $row) { $type = $row['objectType'] . 's'; $deletedIDs[$type][] = $row['id']; } return $deletedIDs; }
public static function toJSON($libraryID) { // TODO: cache $libraryType = Zotero_Libraries::getType($libraryID); if ($libraryType == 'user') { $objectUserID = Zotero_Users::getUserIDFromLibraryID($libraryID); $json = ['type' => $libraryType, 'id' => $objectUserID, 'name' => self::getName($libraryID), 'links' => ['alternate' => ['href' => Zotero_URI::getUserURI($objectUserID, true), 'type' => 'text/html']]]; } else { if ($libraryType == 'publications') { $objectUserID = Zotero_Users::getUserIDFromLibraryID($libraryID); $json = ['type' => $libraryType, 'id' => $objectUserID, 'name' => self::getName($libraryID), 'links' => ['alternate' => ['href' => Zotero_URI::getUserURI($objectUserID, true) . "/publications", 'type' => 'text/html']]]; } else { if ($libraryType == 'group') { $objectGroupID = Zotero_Groups::getGroupIDFromLibraryID($libraryID); $group = Zotero_Groups::get($objectGroupID); $json = ['type' => $libraryType, 'id' => $objectGroupID, 'name' => self::getName($libraryID), 'links' => ['alternate' => ['href' => Zotero_URI::getGroupURI($group, true), 'type' => 'text/html']]]; } else { throw new Exception("Invalid library type '{$libraryType}'"); } } } return $json; }
protected function setKeyPermissions($keyObj, $accessElement) { foreach ($accessElement as $accessField => $accessVal) { // 'write' is handled below if ($accessField == 'write') { continue; } // Group library access (<access group="23456"/>) if ($accessField == 'group') { // Grant access to all groups if ($accessVal === 0) { $keyObj->setPermission(0, 'group', true); $keyObj->setPermission(0, 'write', $accessElement['write']); } else { $group = Zotero_Groups::get($accessVal); if (!$group) { $this->e400("Group not found"); } if (!$group->hasUser($keyObj->userID)) { $this->e400("User {$this->id} is not a member of group {$group->id}"); } $keyObj->setPermission($group->libraryID, 'library', true); $keyObj->setPermission($group->libraryID, 'write', $accessElement['write']); } } else { $libraryID = Zotero_Users::getLibraryIDFromUserID($keyObj->userID); $keyObj->setPermission($libraryID, $accessField, $accessVal); $keyObj->setPermission($libraryID, 'write', $accessElement['write']); } } }
private static function getURIObject($objectURI, $type) { $Types = ucwords($type) . 's'; $types = strtolower($Types); $libraryType = null; $baseURI = self::getBaseURI(); // If not found, try global URI if (strpos($objectURI, $baseURI) !== 0) { throw new Exception("Invalid base URI '{$objectURI}'"); } $objectURI = substr($objectURI, strlen($baseURI)); $typeRE = "/^(users|groups)\\/([0-9]+)(?:\\/|\$)/"; if (!preg_match($typeRE, $objectURI, $matches)) { throw new Exception("Invalid library URI '{$objectURI}'"); } $libraryType = substr($matches[1], 0, -1); $id = $matches[2]; $objectURI = preg_replace($typeRE, '', $objectURI); if ($libraryType == 'user') { if (!Zotero_Users::exists($id)) { return false; } $libraryID = Zotero_Users::getLibraryIDFromUserID($id); } else { if ($libraryType == 'group') { if (!Zotero_Groups::get($id)) { return false; } $libraryID = Zotero_Groups::getLibraryIDFromGroupID($id); } else { throw new Exception("Invalid library type {$libraryType}"); } } if ($type === 'library') { return $libraryID; } else { // TODO: objectID-based URI? if (!preg_match($types . "\\/([A-Z0-9]{8})", $objectURI, $matches)) { throw new Exception("Invalid object URI '{$objectURI}'"); } $objectKey = $matches[1]; return call_user_func(array("Zotero_{$Types}", "getByLibraryAndKey"), $libraryID, $objectKey); } }
public static function deleteUser($userID) { if (empty($userID)) { throw new Exception("userID not provided"); } $username = Zotero_Users::getUsername($userID, true); $sql = "SELECT LUM_Role.Name FROM LUM_User JOIN LUM_Role USING (RoleID) WHERE UserID=?"; try { $role = Zotero_WWW_DB_2::valueQuery($sql, $userID); } catch (Exception $e) { Z_Core::logError("WARNING: {$e} -- retrying on primary"); $role = Zotero_WWW_DB_1::valueQuery($sql, $userID); } if ($role != 'Deleted') { throw new Exception("User '{$username}' does not have role 'Deleted'"); } Zotero_DB::beginTransaction(); if (Zotero_Groups::getUserOwnedGroups($userID)) { throw new Exception("Cannot delete user '{$username}' with owned groups"); } // Remove user from any groups they're a member of // // This isn't strictly necessary thanks to foreign key cascades, // but it removes some extra keyPermissions rows $groupIDs = Zotero_Groups::getUserGroups($userID); foreach ($groupIDs as $groupID) { $group = Zotero_Groups::get($groupID, true); $group->removeUser($userID); } // Remove all data Zotero_Users::clearAllData($userID); // Remove user publications library $libraryID = self::getLibraryIDFromUserID($userID, 'publications'); if ($libraryID) { $shardID = Zotero_Shards::getByLibraryID($libraryID); Zotero_DB::query("DELETE FROM shardLibraries WHERE libraryID=?", $libraryID, $shardID); Zotero_DB::query("DELETE FROM libraries WHERE libraryID=?", $libraryID); } // Remove user/library rows $libraryID = self::getLibraryIDFromUserID($userID); $shardID = Zotero_Shards::getByLibraryID($libraryID); Zotero_DB::query("DELETE FROM shardLibraries WHERE libraryID=?", $libraryID, $shardID); Zotero_DB::query("DELETE FROM libraries WHERE libraryID=?", $libraryID); Zotero_DB::commit(); }
/** * Handle S3 request * * Permission-checking provided by items() */ private function _handleFileRequest($item) { if (!$this->permissions->canAccess($this->objectLibraryID, 'files')) { $this->e403(); } $this->allowMethods(array('HEAD', 'GET', 'POST', 'PATCH')); if (!$item->isAttachment()) { $this->e400("Item is not an attachment"); } // File info for 4.0 client sync // // Use of HEAD method was discontinued after 2.0.8/2.1b1 due to // compatibility problems with proxies and security software if ($this->method == 'GET' && $this->fileMode == 'info') { $info = Zotero_Storage::getLocalFileItemInfo($item); if (!$info) { $this->e404(); } StatsD::increment("storage.info", 1); /* header("Last-Modified: " . gmdate('r', $info['uploaded'])); header("Content-Type: " . $info['type']); */ header("Content-Length: " . $info['size']); header("ETag: " . $info['hash']); header("X-Zotero-Filename: " . $info['filename']); header("X-Zotero-Modification-Time: " . $info['mtime']); header("X-Zotero-Compressed: " . ($info['zip'] ? 'Yes' : 'No')); header_remove("X-Powered-By"); $this->end(); } else { if ($this->method == 'GET') { $info = Zotero_Storage::getLocalFileItemInfo($item); if (!$info) { $this->e404(); } // File viewing if ($this->fileView) { $url = Zotero_Attachments::getTemporaryURL($item, !empty($_GET['int'])); if (!$url) { $this->e500(); } StatsD::increment("storage.view", 1); $this->redirect($url); exit; } // File download $url = Zotero_Storage::getDownloadURL($item, 60); if (!$url) { $this->e404(); } // Provide some headers to let 5.0 client skip download header("Zotero-File-Modification-Time: {$info['mtime']}"); header("Zotero-File-MD5: {$info['hash']}"); header("Zotero-File-Size: {$info['size']}"); header("Zotero-File-Compressed: " . ($info['zip'] ? 'Yes' : 'No')); StatsD::increment("storage.download", 1); Zotero_Storage::logDownload($item, $this->userID, IPAddress::getIP()); $this->redirect($url); exit; } else { if ($this->method == 'POST' || $this->method == 'PATCH') { if (!$item->isImportedAttachment()) { $this->e400("Cannot upload file for linked file/URL attachment item"); } $libraryID = $item->libraryID; $type = Zotero_Libraries::getType($libraryID); if ($type == 'group') { $groupID = Zotero_Groups::getGroupIDFromLibraryID($libraryID); $group = Zotero_Groups::get($groupID); if (!$group->userCanEditFiles($this->userID)) { $this->e403("You do not have file editing access"); } } else { $group = null; } // If not the client, require If-Match or If-None-Match if (!$this->httpAuth) { if (empty($_SERVER['HTTP_IF_MATCH']) && empty($_SERVER['HTTP_IF_NONE_MATCH'])) { $this->e428("If-Match/If-None-Match header not provided"); } if (!empty($_SERVER['HTTP_IF_MATCH'])) { if (!preg_match('/^"?([a-f0-9]{32})"?$/', $_SERVER['HTTP_IF_MATCH'], $matches)) { $this->e400("Invalid ETag in If-Match header"); } if (!$item->attachmentStorageHash) { $this->e412("ETag set but file does not exist"); } if ($item->attachmentStorageHash != $matches[1]) { $this->libraryVersion = $item->version; $this->libraryVersionOnFailure = true; $this->e412("ETag does not match current version of file"); } } else { if ($_SERVER['HTTP_IF_NONE_MATCH'] != "*") { $this->e400("Invalid value for If-None-Match header"); } if (Zotero_Storage::getLocalFileItemInfo($item)) { $this->libraryVersion = $item->version; $this->libraryVersionOnFailure = true; $this->e412("If-None-Match: * set but file exists"); } } } // // Upload authorization // if (!isset($_POST['update']) && !isset($_REQUEST['upload'])) { $info = new Zotero_StorageFileInfo(); // Validate upload metadata if (empty($_REQUEST['md5'])) { $this->e400('MD5 hash not provided'); } if (!preg_match('/[abcdefg0-9]{32}/', $_REQUEST['md5'])) { $this->e400('Invalid MD5 hash'); } if (!isset($_REQUEST['filename']) || $_REQUEST['filename'] === "") { $this->e400('Filename not provided'); } // Multi-file upload // // For ZIP files, the filename and hash of the ZIP file are different from those // of the main file. We use the former for S3, and we store the latter in the // upload log to set the attachment metadata with them on file registration. if (!empty($_REQUEST['zipMD5'])) { if (!preg_match('/[abcdefg0-9]{32}/', $_REQUEST['zipMD5'])) { $this->e400('Invalid ZIP MD5 hash'); } if (empty($_REQUEST['zipFilename'])) { $this->e400('ZIP filename not provided'); } $info->zip = true; $info->hash = $_REQUEST['zipMD5']; $info->filename = $_REQUEST['zipFilename']; $info->itemFilename = $_REQUEST['filename']; $info->itemHash = $_REQUEST['md5']; } else { if (!empty($_REQUEST['zipFilename'])) { $this->e400('ZIP MD5 hash not provided'); } else { $info->zip = !empty($_REQUEST['zip']); $info->filename = $_REQUEST['filename']; $info->hash = $_REQUEST['md5']; } } if (empty($_REQUEST['mtime'])) { $this->e400('File modification time not provided'); } $info->mtime = $_REQUEST['mtime']; if (!isset($_REQUEST['filesize'])) { $this->e400('File size not provided'); } $info->size = $_REQUEST['filesize']; if (!is_numeric($info->size)) { $this->e400("Invalid file size"); } $info->contentType = isset($_REQUEST['contentType']) ? $_REQUEST['contentType'] : null; if (!preg_match("/^[a-zA-Z0-9\\-\\/]+\$/", $info->contentType)) { $info->contentType = null; } $info->charset = isset($_REQUEST['charset']) ? $_REQUEST['charset'] : null; if (!preg_match("/^[a-zA-Z0-9\\-]+\$/", $info->charset)) { $info->charset = null; } $contentTypeHeader = $info->contentType . ($info->contentType && $info->charset ? "; charset=" . $info->charset : ""); // Reject file if it would put account over quota if ($group) { $quota = Zotero_Storage::getEffectiveUserQuota($group->ownerUserID); $usage = Zotero_Storage::getUserUsage($group->ownerUserID); } else { $quota = Zotero_Storage::getEffectiveUserQuota($this->objectUserID); $usage = Zotero_Storage::getUserUsage($this->objectUserID); } $total = $usage['total']; $fileSizeMB = round($info->size / 1024 / 1024, 1); if ($total + $fileSizeMB > $quota) { StatsD::increment("storage.upload.quota", 1); $this->e413("File would exceed quota ({$total} + {$fileSizeMB} > {$quota})"); } Zotero_DB::query("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE"); Zotero_DB::beginTransaction(); // See if file exists with this filename $localInfo = Zotero_Storage::getLocalFileInfo($info); if ($localInfo) { $storageFileID = $localInfo['storageFileID']; // Verify file size if ($localInfo['size'] != $info->size) { throw new Exception("Specified file size incorrect for existing file " . $info->hash . "/" . $info->filename . " ({$localInfo['size']} != {$info->size})"); } } else { $oldStorageFileID = Zotero_Storage::getFileByHash($info->hash, $info->zip); if ($oldStorageFileID) { // Verify file size $localInfo = Zotero_Storage::getFileInfoByID($oldStorageFileID); if ($localInfo['size'] != $info->size) { throw new Exception("Specified file size incorrect for duplicated file " . $info->hash . "/" . $info->filename . " ({$localInfo['size']} != {$info->size})"); } // Create new file on S3 with new name $storageFileID = Zotero_Storage::duplicateFile($oldStorageFileID, $info->filename, $info->zip, $contentTypeHeader); } } // If we already have a file, add/update storageFileItems row and stop if (!empty($storageFileID)) { Zotero_Storage::updateFileItemInfo($item, $storageFileID, $info, $this->httpAuth); Zotero_DB::commit(); StatsD::increment("storage.upload.existing", 1); if ($this->httpAuth) { $this->queryParams['format'] = null; header('Content-Type: application/xml'); echo "<exists/>"; } else { $this->queryParams['format'] = null; header('Content-Type: application/json'); $this->libraryVersion = $item->version; echo json_encode(array('exists' => 1)); } $this->end(); } Zotero_DB::commit(); // Add request to upload queue $uploadKey = Zotero_Storage::queueUpload($this->userID, $info); // User over queue limit if (!$uploadKey) { header('Retry-After: ' . Zotero_Storage::$uploadQueueTimeout); if ($this->httpAuth) { $this->e413("Too many queued uploads"); } else { $this->e429("Too many queued uploads"); } } StatsD::increment("storage.upload.new", 1); // Output XML for client requests (which use HTTP Auth) if ($this->httpAuth) { $params = Zotero_Storage::generateUploadPOSTParams($item, $info, true); $this->queryParams['format'] = null; header('Content-Type: application/xml'); $xml = new SimpleXMLElement('<upload/>'); $xml->url = Zotero_Storage::getUploadBaseURL(); $xml->key = $uploadKey; foreach ($params as $key => $val) { $xml->params->{$key} = $val; } echo $xml->asXML(); } else { if (!empty($_REQUEST['params']) && $_REQUEST['params'] == "1") { $params = array("url" => Zotero_Storage::getUploadBaseURL(), "params" => array()); foreach (Zotero_Storage::generateUploadPOSTParams($item, $info) as $key => $val) { $params['params'][$key] = $val; } } else { $params = Zotero_Storage::getUploadPOSTData($item, $info); } $params['uploadKey'] = $uploadKey; $this->queryParams['format'] = null; header('Content-Type: application/json'); echo json_encode($params); } exit; } // // API partial upload and post-upload file registration // if (isset($_REQUEST['upload'])) { $uploadKey = $_REQUEST['upload']; if (!$uploadKey) { $this->e400("Upload key not provided"); } $info = Zotero_Storage::getUploadInfo($uploadKey); if (!$info) { $this->e400("Upload key not found"); } // Partial upload if ($this->method == 'PATCH') { if (empty($_REQUEST['algorithm'])) { throw new Exception("Algorithm not specified", Z_ERROR_INVALID_INPUT); } $storageFileID = Zotero_Storage::patchFile($item, $info, $_REQUEST['algorithm'], $this->body); } else { $remoteInfo = Zotero_Storage::getRemoteFileInfo($info); if (!$remoteInfo) { error_log("Remote file {$info->hash}/{$info->filename} not found"); $this->e400("Remote file not found"); } if ($remoteInfo->size != $info->size) { error_log("Uploaded file size does not match " . "({$remoteInfo->size} != {$info->size}) " . "for file {$info->hash}/{$info->filename}"); } } // Set an automatic shared lock in getLocalFileInfo() to prevent // two simultaneous transactions from adding a file Zotero_DB::query("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE"); Zotero_DB::beginTransaction(); if (!isset($storageFileID)) { // Check if file already exists, which can happen if two identical // files are uploaded simultaneously $fileInfo = Zotero_Storage::getLocalFileInfo($info); if ($fileInfo) { $storageFileID = $fileInfo['storageFileID']; } else { $storageFileID = Zotero_Storage::addFile($info); } } Zotero_Storage::updateFileItemInfo($item, $storageFileID, $info); Zotero_Storage::logUpload($this->userID, $item, $uploadKey, IPAddress::getIP()); Zotero_DB::commit(); header("HTTP/1.1 204 No Content"); header("Last-Modified-Version: " . $item->version); exit; } // // Client post-upload file registration // if (isset($_POST['update'])) { $this->allowMethods(array('POST')); if (empty($_POST['mtime'])) { throw new Exception('File modification time not provided'); } $uploadKey = $_POST['update']; $info = Zotero_Storage::getUploadInfo($uploadKey); if (!$info) { $this->e400("Upload key not found"); } $remoteInfo = Zotero_Storage::getRemoteFileInfo($info); if (!$remoteInfo) { $this->e400("Remote file not found"); } if (!isset($info->size)) { throw new Exception("Size information not available"); } $info->mtime = $_POST['mtime']; // Set an automatic shared lock in getLocalFileInfo() to prevent // two simultaneous transactions from adding a file Zotero_DB::query("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE"); Zotero_DB::beginTransaction(); // Check if file already exists, which can happen if two identical // files are uploaded simultaneously $fileInfo = Zotero_Storage::getLocalFileInfo($info); if ($fileInfo) { $storageFileID = $fileInfo['storageFileID']; } else { $storageFileID = Zotero_Storage::addFile($info); } Zotero_Storage::updateFileItemInfo($item, $storageFileID, $info, true); Zotero_Storage::logUpload($this->userID, $item, $uploadKey, IPAddress::getIP()); Zotero_DB::commit(); header("HTTP/1.1 204 No Content"); exit; } throw new Exception("Invalid request", Z_ERROR_INVALID_INPUT); } } } exit; }
public static function getUserLibraries($userID) { return array_merge(array(Zotero_Users::getLibraryIDFromUserID($userID)), Zotero_Groups::getUserGroupLibraries($userID)); }
public static function getLibraryURI($libraryID) { $libraryType = Zotero_Libraries::getType($libraryID); switch ($libraryType) { case 'user': $id = Zotero_Users::getUserIDFromLibraryID($libraryID); return self::getBaseURI() . "users/{$id}"; case 'group': $id = Zotero_Groups::getGroupIDFromLibraryID($libraryID); return self::getBaseURI() . "groups/{$id}"; } }
public function toJSON() { if (($this->id || $this->key) && !$this->loaded) { $this->load(); } $json = []; $json['key'] = $this->key; $json['userID'] = $this->userID; $json['username'] = Zotero_Users::getUsername($this->userID); $json['name'] = $this->name; if ($this->permissions) { $json['access'] = ['user' => [], 'groups' => []]; foreach ($this->permissions as $libraryID => $p) { // group="all" is stored as libraryID 0 if ($libraryID === 0) { $json['access']['groups']['all']['library'] = true; $json['access']['groups']['all']['write'] = !empty($p['write']); } else { $type = Zotero_Libraries::getType($libraryID); switch ($type) { case 'user': $json['access']['user']['library'] = true; foreach ($p as $permission => $granted) { if ($permission == 'library') { continue; } $json['access']['user'][$permission] = (bool) $granted; } break; case 'group': $groupID = Zotero_Groups::getGroupIDFromLibraryID($libraryID); $json['access']['groups'][$groupID]['library'] = true; $json['access']['groups'][$groupID]['write'] = !empty($p['write']); break; } } } if (sizeOf($json['access']['user']) === 0) { unset($json['access']['user']); } if (sizeOf($json['access']['groups']) === 0) { unset($json['access']['groups']); } } $json['dateAdded'] = Zotero_Date::sqlToISO8601($this->dateAdded); if ($this->lastUsed != '0000-00-00 00:00:00') { $json['lastUsed'] = Zotero_Date::sqlToISO8601($this->lastUsed); } $ips = $this->getRecentIPs(); if ($ips) { $json['recentIPs'] = $ips; } return $json; }
private function getGroupPrivacy($groupID) { if (isset($this->groupPrivacy[$groupID])) { return $this->groupPrivacy[$groupID]; } $group = Zotero_Groups::get($groupID); if (!$group) { throw new Exception("Group {$groupID} doesn't exist"); } $privacy = array(); if ($group->isPublic()) { $privacy['view'] = true; $privacy['library'] = $group->libraryReading == 'all'; $privacy['notes'] = $group->libraryReading == 'all'; } else { $privacy['view'] = false; $privacy['library'] = false; $privacy['notes'] = false; } $this->groupPrivacy[$groupID] = $privacy; return $privacy; }
public function groupUsers() { // For now, only allow root and user access if (!$this->permissions->isSuper()) { $this->e403(); } $groupID = $this->scopeObjectID; $userID = $this->objectID; $group = Zotero_Groups::get($groupID); if (!$group) { $this->e404("Group {$groupID} does not exist"); } // Add multiple users to group if ($this->method == 'POST') { if ($userID) { $this->e400("POST requests cannot end with a userID (did you mean PUT?)"); } // Body can contain multiple <user> blocks, so stuff in root element try { $xml = @new SimpleXMLElement("<root>" . $this->body . "</root>"); } catch (Exception $e) { $this->e400("{$this->method} data is not valid XML"); } $addedUserIDs = array(); Zotero_DB::beginTransaction(); foreach ($xml->user as $user) { $id = (int) $user['id']; $role = (string) $user['role']; if (!$id) { $this->e400("User ID not provided in '" . $user->asXML() . "'"); } if (!$role) { $this->e400("Role not provided in '" . $user->asXML() . "'"); } try { $added = $group->addUser($id, $role); } catch (Exception $e) { if (strpos($e->getMessage(), "Invalid role") === 0) { $this->e400("Invalid role '{$role}' in " . $user->asXML() . "'"); } $this->handleException($e); } if ($added) { $addedUserIDs[] = $id; } } // Response after adding $entries = array(); foreach ($addedUserIDs as $addedUserID) { $entries[] = $group->memberToAtom($addedUserID); } $title = "Users added to group '{$group->name}'"; $this->responseXML = Zotero_Atom::createAtomFeed('groupUsers', $title, $this->uri, $entries, null, $this->queryParams, $this->permissions); Zotero_DB::commit(); $this->end(); } // Add a single user to group if ($this->method == 'PUT') { if (!$userID) { $this->e400("PUT requests must end with a userID (did you mean POST?)"); } try { $user = @new SimpleXMLElement($this->body); } catch (Exception $e) { $this->e400("{$this->method} data is not valid XML"); } $id = (int) $user['id']; $role = (string) $user['role']; // User id is optional, but, if it's there, make sure it matches if ($id && $id != $userID) { $this->e400("User ID {$id} does not match user ID {$userID} from URI"); } if (!$role) { $this->e400("Role not provided in '{$this->body}'"); } Zotero_DB::beginTransaction(); $changedUserIDs = array(); try { if ($role == 'owner') { if ($userID != $group->ownerUserID) { $changedUserIDs[] = $group->ownerUserID; $group->ownerUserID = $userID; $group->save(); $changedUserIDs[] = $userID; } } else { if ($group->hasUser($userID)) { try { $updated = $group->updateUser($userID, $role); } catch (Exception $e) { switch ($e->getCode()) { case Z_ERROR_CANNOT_DELETE_GROUP_OWNER: $this->e400($e->getMessage()); default: $this->handleException($e); } } if ($updated) { $changedUsersIDs[] = $userID; } } else { $added = $group->addUser($userID, $role); if ($added) { $changedUserIDs[] = $userID; } } } } catch (Exception $e) { if (strpos($e->getMessage(), "Invalid role") === 0) { $this->e400("Invalid role '{$role}' in '{$this->body}'"); } $this->handleException($e); } // Response after adding $entries = array(); foreach ($changedUserIDs as $changedUserID) { $entries[] = $group->memberToAtom($changedUserID); } $title = "Users changed in group '{$group->name}'"; $this->responseXML = Zotero_Atom::createAtomFeed('groupUsers', $title, $this->uri, $entries, null, $this->queryParams, $this->permissions); Zotero_DB::commit(); $this->end(); } if ($this->method == 'DELETE') { if (!$userID) { $this->e400("DELETE requests must end with a userID"); } Zotero_DB::beginTransaction(); try { $group->removeUser($userID); } catch (Exception $e) { switch ($e->getCode()) { case Z_ERROR_CANNOT_DELETE_GROUP_OWNER: $this->e400($e->getMessage()); case Z_ERROR_USER_NOT_GROUP_MEMBER: $this->e404($e->getMessage()); default: $this->handleException($e); } } Zotero_DB::commit(); header("HTTP/1.1 204 No Content"); exit; } // Single user if ($userID) { $this->responseXML = $group->memberToAtom($userID); $this->end(); } // Multiple users $title = "Members of '{$group->name}'"; $entries = array(); $memberIDs = array_merge(array($group->ownerUserID), $group->getAdmins(), $group->getMembers()); foreach ($memberIDs as $userID) { $entries[] = $group->memberToAtom($userID); } $totalResults = sizeOf($entries); $this->responseXML = Zotero_Atom::createAtomFeed('groupUsers', $title, $this->uri, $entries, $totalResults, $this->queryParams, $this->permissions); $this->end(); }
/** * Returns shardIDs of all shards storing libraries this user belongs to */ public static function getUserShards($userID) { return array_unique(array_merge(array(self::getByUserID($userID)), Zotero_Groups::getUserGroupShards($userID))); }
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})"); }
public static function getUserOwnedGroupLibraries($userID) { $groups = self::getUserOwnedGroups($userID); $libraries = array(); foreach ($groups as $group) { $libraries[] = Zotero_Groups::getLibraryIDFromGroupID($group); } return $libraries; }
public function save() { if (!$this->loaded) { Z_Core::debug("Not saving unloaded group {$this->id}"); return; } if (empty($this->changed)) { Z_Core::debug("Group {$this->id} has not changed", 4); return; } if (!$this->ownerUserID) { throw new Exception("Cannot save group without owner"); } if (!$this->name) { throw new Exception("Cannot save group without name"); } if (mb_strlen($this->description) > 1024) { throw new Exception("Group description too long", Z_ERROR_GROUP_DESCRIPTION_TOO_LONG); } Zotero_DB::beginTransaction(); $libraryID = $this->libraryID; if (!$libraryID) { $shardID = Zotero_Shards::getNextShard(); $libraryID = Zotero_Libraries::add('group', $shardID); if (!$libraryID) { throw new Exception('libraryID not available after Zotero_Libraries::add()'); } } $fields = array('name', 'slug', 'type', 'description', 'url', 'hasImage'); if ($this->isPublic()) { $existing = Zotero_Groups::publicNameExists($this->name); if ($existing && $existing != $this->id) { throw new Exception("Public group with name '{$this->name}' already exists", Z_ERROR_PUBLIC_GROUP_EXISTS); } } $fields = array_merge($fields, array('libraryEditing', 'libraryReading', 'fileEditing')); $sql = "INSERT INTO groups\n\t\t\t\t\t(groupID, libraryID, " . implode(", ", $fields) . ", dateModified)\n\t\t\t\t\tVALUES (?, ?, " . implode(", ", array_fill(0, sizeOf($fields), "?")) . ", CURRENT_TIMESTAMP)"; $params = array($this->id, $libraryID); foreach ($fields as $field) { if (is_bool($this->{$field})) { $params[] = (int) $this->{$field}; } else { $params[] = $this->{$field}; } } $sql .= " ON DUPLICATE KEY UPDATE "; $q = array(); foreach ($fields as $field) { $q[] = "{$field}=?"; if (is_bool($this->{$field})) { $params[] = (int) $this->{$field}; } else { $params[] = $this->{$field}; } } $sql .= implode(", ", $q) . ", " . "dateModified=CURRENT_TIMESTAMP, " . "version=IF(version = 255, 1, version + 1)"; $insertID = Zotero_DB::query($sql, $params); if (!$this->id) { if (!$insertID) { throw new Exception("Group id not available after INSERT"); } $this->id = $insertID; } if (!$this->libraryID) { $this->libraryID = $libraryID; } // If creating group or changing owner if (!empty($this->changed['ownerUserID'])) { $sql = "SELECT userID FROM groupUsers WHERE groupID=? AND role='owner'"; $currentOwner = Zotero_DB::valueQuery($sql, $this->id); $newOwner = $this->ownerUserID; // Move existing owner out of the way, if there is one if ($currentOwner) { $sql = "UPDATE groupUsers SET role='admin' WHERE groupID=? AND userID=?"; Zotero_DB::query($sql, array($this->id, $currentOwner)); } // Make sure new owner exists in DB if (!Zotero_Users::exists($newOwner)) { Zotero_Users::addFromWWW($newOwner); } // Add new owner to group $sql = "INSERT INTO groupUsers (groupID, userID, role, joined) VALUES\n\t\t\t\t\t(?, ?, 'owner', CURRENT_TIMESTAMP) ON DUPLICATE KEY UPDATE\n\t\t\t\t\trole='owner', lastUpdated=CURRENT_TIMESTAMP"; Zotero_DB::query($sql, array($this->id, $newOwner)); // Delete any record of this user losing access to the group $libraryID = Zotero_Users::getLibraryIDFromUserID($this->ownerUserID); $sql = "DELETE FROM syncDeleteLogIDs WHERE libraryID=? AND objectType='group' AND id=?"; Zotero_DB::query($sql, array($libraryID, $this->id), Zotero_Shards::getByLibraryID($this->libraryID)); // Send library removal notification for all API keys belonging to the former owner // with access to this group $apiKeys = Zotero_Keys::getUserKeysWithLibrary($currentOwner, $this->libraryID); Zotero_Notifier::trigger('remove', 'apikey-library', array_map(function ($key) { return $key->key . "-" . $this->libraryID; }, $apiKeys)); // Send library add notification for all API keys belonging to the new owner // with access to this group $apiKeys = Zotero_Keys::getUserKeysWithLibrary($newOwner, $this->libraryID); Zotero_Notifier::trigger('add', 'apikey-library', array_map(function ($key) { return $key->key . "-" . $this->libraryID; }, $apiKeys)); } // If any of the group's users have a queued upload, flag group for a timestamp // update once the sync is done so that the uploading user gets the change try { $userIDs = self::getUsers(); foreach ($userIDs as $userID) { if ($syncUploadQueueID = Zotero_Sync::getUploadQueueIDByUserID($userID)) { Zotero_Sync::postWriteLog($syncUploadQueueID, 'group', $this->id, 'update'); } } } catch (Exception $e) { Z_Core::logError($e); } Zotero_DB::commit(); $this->load(); return $libraryID; }
/** * Returns user's object ids updated since |timestamp|, keyed by libraryID, * or count of all updated items if $countOnly is true * * @param int $libraryID User ID * @param string $timestamp Unix timestamp of last sync time * @param array $updatedLibraryIDs Libraries with updated data * @return array|int */ public static function getUpdated($userID, $timestamp, $updatedLibraryIDs, $countOnly = false) { $table = self::$table; $id = self::$idColumn; $type = self::$objectType; $types = self::$objectTypePlural; $timestampCol = "serverDateModified"; // All joined groups have to be checked $joinedGroupIDs = Zotero_Groups::getJoined($userID, $timestamp); $joinedLibraryIDs = array(); foreach ($joinedGroupIDs as $groupID) { $joinedLibraryIDs[] = Zotero_Groups::getLibraryIDFromGroupID($groupID); } // Separate libraries into shards for querying $libraryIDs = array_unique(array_merge($joinedLibraryIDs, $updatedLibraryIDs)); $shardLibraryIDs = array(); foreach ($libraryIDs as $libraryID) { $shardID = Zotero_Shards::getByLibraryID($libraryID); if (!isset($shardLibraryIDs[$shardID])) { $shardLibraryIDs[$shardID] = array('updated' => array(), 'joined' => array()); } if (in_array($libraryID, $joinedLibraryIDs)) { $shardLibraryIDs[$shardID]['joined'][] = $libraryID; } else { $shardLibraryIDs[$shardID]['updated'][] = $libraryID; } } if ($countOnly) { $count = 0; $fieldList = "COUNT(*)"; } else { $updatedByLibraryID = array(); $fieldList = "libraryID, {$id} AS id"; } // Send query at each shard foreach ($shardLibraryIDs as $shardID => $libraryIDs) { $sql = "SELECT {$fieldList} FROM {$table} WHERE "; if ($libraryIDs['updated']) { $sql .= "(libraryID IN (" . implode(', ', array_fill(0, sizeOf($libraryIDs['updated']), '?')) . ")"; $params = $libraryIDs['updated']; $sql .= " AND {$timestampCol} >= FROM_UNIXTIME(?))"; $params[] = $timestamp; } if ($libraryIDs['joined']) { if ($libraryIDs['updated']) { $sql .= " OR "; } else { $params = array(); } $sql .= "libraryID IN (" . implode(', ', array_fill(0, sizeOf($libraryIDs['joined']), '?')) . ")"; $params = array_merge($params, $libraryIDs['joined']); } if ($countOnly) { $count += Zotero_DB::valueQuery($sql, $params, $shardID); } else { $rows = Zotero_DB::query($sql, $params, $shardID); if ($rows) { // Separate ids by libraryID foreach ($rows as $row) { $updatedByLibraryID[$row['libraryID']][] = $row['id']; } } } } return $countOnly ? $count : $updatedByLibraryID; }
public static function isEditable($obj) { $type = static::field('object'); // Only enforce for sync controller for now if (empty($GLOBALS['controller']) || !$GLOBALS['controller'] instanceof SyncController) { return true; } // Make sure user has access privileges to delete $userID = $GLOBALS['controller']->userID; if (!$userID) { return true; } $objectLibraryID = $obj->libraryID; $libraryType = Zotero_Libraries::getType($objectLibraryID); switch ($libraryType) { case 'user': if (!empty($GLOBALS['controller']->userLibraryID)) { $userLibraryID = $GLOBALS['controller']->userLibraryID; } else { $userLibraryID = Zotero_Users::getLibraryIDFromUserID($userID); } if ($objectLibraryID != $userLibraryID) { return false; } return true; case 'group': $groupID = Zotero_Groups::getGroupIDFromLibraryID($objectLibraryID); $group = Zotero_Groups::get($groupID); if (!$group->hasUser($userID) || !$group->userCanEdit($userID)) { return false; } if ($type == 'item' && $obj->isImportedAttachment() && !$group->userCanEditFiles($userID)) { return false; } return true; default: throw new Exception("Unsupported library type '{$libraryType}'"); } }
public static function getLibraryURI($libraryID) { $libraryType = Zotero_Libraries::getType($libraryID); switch ($libraryType) { case 'user': $id = Zotero_Users::getUserIDFromLibraryID($libraryID); return self::getBaseURI() . "users/{$id}"; case 'publications': $id = Zotero_Users::getUserIDFromLibraryID($libraryID); return self::getBaseURI() . "users/{$id}/publications"; case 'group': $id = Zotero_Groups::getGroupIDFromLibraryID($libraryID); return self::getBaseURI() . "groups/{$id}"; default: throw new Exception("Invalid library type '{$libraryType}'"); } }
/** * Converts key to a SimpleXMLElement item * * @return SimpleXMLElement Key data as SimpleXML element */ public function toXML() { if (($this->id || $this->key) && !$this->loaded) { $this->load(); } $xml = '<key/>'; $xml = new SimpleXMLElement($xml); $xml['key'] = $this->key; $xml['dateAdded'] = $this->dateAdded; if ($this->lastUsed != '0000-00-00 00:00:00') { $xml['lastUsed'] = $this->lastUsed; } $xml->name = $this->name; if ($this->permissions) { foreach ($this->permissions as $libraryID => $p) { $access = $xml->addChild('access'); // group="all" is stored as libraryID 0 if ($libraryID === 0) { $access['group'] = 'all'; if (!empty($p['write'])) { $access['write'] = 1; } continue; } $type = Zotero_Libraries::getType($libraryID); switch ($type) { case 'user': foreach ($p as $permission => $granted) { $access[$permission] = (int) $granted; } break; case 'group': $access['group'] = Zotero_Groups::getGroupIDFromLibraryID($libraryID); if (!empty($p['write'])) { $access['write'] = 1; } break; } } } $ips = $this->getRecentIPs(); if ($ips) { $xml->recentIPs = implode(' ', $ips); } return $xml; }