Exemplo n.º 1
0
 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);
     }
 }
Exemplo n.º 2
0
 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);
 }
Exemplo n.º 3
0
 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;
 }
Exemplo n.º 4
0
 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;
 }
Exemplo n.º 5
0
 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;
 }
Exemplo n.º 6
0
 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']);
         }
     }
 }
Exemplo n.º 7
0
 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);
     }
 }
Exemplo n.º 8
0
 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();
 }
Exemplo n.º 9
0
 /**
  * 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;
 }
Exemplo n.º 10
0
 public static function getUserLibraries($userID)
 {
     return array_merge(array(Zotero_Users::getLibraryIDFromUserID($userID)), Zotero_Groups::getUserGroupLibraries($userID));
 }
Exemplo n.º 11
0
 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}";
     }
 }
Exemplo n.º 12
0
 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;
 }
Exemplo n.º 13
0
 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;
 }
Exemplo n.º 14
0
 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();
 }
Exemplo n.º 15
0
 /**
  * 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)));
 }
Exemplo n.º 16
0
 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})");
 }
Exemplo n.º 17
0
 public static function getUserOwnedGroupLibraries($userID)
 {
     $groups = self::getUserOwnedGroups($userID);
     $libraries = array();
     foreach ($groups as $group) {
         $libraries[] = Zotero_Groups::getLibraryIDFromGroupID($group);
     }
     return $libraries;
 }
Exemplo n.º 18
0
 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;
 }
Exemplo n.º 19
0
 /**
  * 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;
 }
Exemplo n.º 20
0
 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}'");
     }
 }
Exemplo n.º 21
0
 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}'");
     }
 }
Exemplo n.º 22
0
 /**
  * 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;
 }