Beispiel #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);
     }
 }
Beispiel #2
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);
     }
 }
Beispiel #3
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']);
         }
     }
 }
Beispiel #4
0
 public static function getAllAdvanced($userID = false, $params = array(), $permissions = null)
 {
     $buffer = 20;
     $maxTimes = 3;
     $groups = array();
     $start = !empty($params['start']) ? $params['start'] : 0;
     $limit = !empty($params['limit']) ? $params['limit'] + $buffer : false;
     $totalResults = null;
     $times = 0;
     while (true) {
         if ($times > 0) {
             Z_Core::logError('Getting more groups in Zotero_Groups::getAllAdvanced()');
         }
         $sql = "SELECT SQL_CALC_FOUND_ROWS G.groupID, GUO.userID AS ownerUserID FROM groups G\n\t\t\t\t\tJOIN groupUsers GUO ON (G.groupID=GUO.groupID AND GUO.role='owner') ";
         $sqlParams = array();
         if ($userID) {
             $sql .= "JOIN groupUsers GUA ON (G.groupID=GUA.groupID) WHERE GUA.userID=? ";
             $sqlParams[] = $userID;
         }
         $paramSQL = "";
         $includeEmpty = false;
         if (!empty($params['q'])) {
             if (!is_array($params['q'])) {
                 $params['q'] = array($params['q']);
             }
             foreach ($params['q'] as $q) {
                 $field = explode(":", $q);
                 if (sizeOf($field) == 2) {
                     switch ($field[0]) {
                         case 'slug':
                             $includeEmpty = true;
                             break;
                         default:
                             throw new Exception("Cannot search by group field '{$field[0]}'", Z_ERROR_INVALID_GROUP_TYPE);
                     }
                     $paramSQL .= "AND " . $field[0];
                     // If first character is '-', negate
                     $paramSQL .= $field[0][0] == '-' ? '!' : '';
                     $paramSQL .= "=? ";
                     $sqlParams[] = $field[1];
                 } else {
                     $paramSQL .= "AND name LIKE ? ";
                     $sqlParams[] = "%{$q}%";
                 }
             }
         }
         if (!$userID) {
             if ($includeEmpty) {
                 $sql .= "WHERE 1 ";
             } else {
                 // Don't include groups that have never had items
                 $sql .= "JOIN libraries L ON (G.libraryID=L.libraryID)\n\t\t\t\t\t\t\tWHERE L.lastUpdated != '0000-00-00 00:00:00' ";
             }
         }
         $sql .= $paramSQL;
         if (!empty($params['fq'])) {
             if (!is_array($params['fq'])) {
                 $params['fq'] = array($params['fq']);
             }
             foreach ($params['fq'] as $fq) {
                 $facet = explode(":", $fq);
                 if (sizeOf($facet) == 2 && preg_match('/-?GroupType/', $facet[0])) {
                     switch ($facet[1]) {
                         case 'PublicOpen':
                         case 'PublicClosed':
                         case 'Private':
                             break;
                         default:
                             throw new Exception("Invalid group type '{$facet[1]}'", Z_ERROR_INVALID_GROUP_TYPE);
                     }
                     $sql .= "AND type";
                     // If first character is '-', negate
                     $sql .= $facet[0][0] == '-' ? '!' : '';
                     $sql .= "=? ";
                     $sqlParams[] = $facet[1];
                 }
             }
         }
         if (!empty($params['order'])) {
             $order = $params['order'];
             if ($order == 'title') {
                 $order = 'name';
             }
             $sql .= "ORDER BY {$order}";
             if (!empty($params['sort'])) {
                 $sql .= " " . $params['sort'] . " ";
             }
         }
         // Set limit higher than the actual limit, in case some groups are
         // removed during access checks
         //
         // Actual limiting is done below
         if ($limit) {
             $sql .= "LIMIT ?, ?";
             $sqlParams[] = $start;
             $sqlParams[] = $limit;
         }
         $rows = Zotero_DB::query($sql, $sqlParams);
         if (!$rows) {
             break;
         }
         if (!$totalResults) {
             $foundRows = Zotero_DB::valueQuery("SELECT FOUND_ROWS()");
             $totalResults = $foundRows;
         }
         // Include only groups with non-banned owners
         $owners = array();
         foreach ($rows as $row) {
             $owners[] = $row['ownerUserID'];
         }
         $owners = Zotero_Users::getValidUsers($owners);
         $ids = array();
         foreach ($rows as $row) {
             if (!in_array($row['ownerUserID'], $owners)) {
                 $totalResults--;
                 continue;
             }
             $ids[] = $row['groupID'];
         }
         $batchStartPos = sizeOf($groups);
         foreach ($ids as $id) {
             $group = Zotero_Groups::get($id, true);
             $groups[] = $group;
         }
         // Remove groups that can't be accessed
         if ($permissions) {
             for ($i = $batchStartPos; $i < sizeOf($groups); $i++) {
                 $libraryID = (int) $groups[$i]->libraryID;
                 // TEMP: casting shouldn't be necessary
                 if (!$permissions->canAccess($libraryID)) {
                     array_splice($groups, $i, 1);
                     $i--;
                     $totalResults--;
                 }
             }
         }
         $times++;
         if ($times == $maxTimes) {
             Z_Core::logError('Too many queries in Zotero_Groups::getAllAdvanced()');
             break;
         }
         if (empty($params['limit'])) {
             break;
         }
         // If we have enough groups to fill the limit, stop
         if (sizeOf($groups) > $params['limit']) {
             break;
         }
         // If there no more rows, stop
         if ($start + sizeOf($rows) == $foundRows) {
             break;
         }
         // This shouldn't happen
         if ($start + sizeOf($rows) > $foundRows) {
             Z_Core::logError('More rows than $foundRows in Zotero_Groups::getAllAdvanced()');
         }
         $start = $start + sizeOf($rows);
         // Get number we still need plus the buffer or all remaining, whichever is lower
         $limit = min($params['limit'] - sizeOf($groups) + $buffer, $foundRows - $start);
     }
     // TODO: generate previous start value
     if (!$groups) {
         return array('groups' => array(), 'totalResults' => 0);
     }
     // Fake limiting -- we can't just use SQL limit because
     // some groups might be inaccessible
     if (!empty($params['limit'])) {
         $groups = array_slice($groups, 0, $params['limit']);
     }
     $results = array('groups' => $groups, 'totalResults' => $totalResults);
     return $results;
 }
Beispiel #5
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);
 }
Beispiel #6
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;
 }
 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}'");
     }
 }
Beispiel #8
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})");
 }
Beispiel #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;
 }
Beispiel #10
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();
 }
 public static function getOwner($libraryID)
 {
     $type = self::getType($libraryID);
     switch ($type) {
         case 'user':
             return Zotero_Users::getUserIDFromLibraryID($libraryID);
         case 'group':
             $groupID = Zotero_Groups::getGroupIDFromLibraryID($libraryID);
             $group = Zotero_Groups::get($groupID);
             return $group->ownerUserID;
     }
 }
Beispiel #12
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;
 }
Beispiel #13
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();
 }
Beispiel #14
0
 public static function getAllAdvanced($userID = false, $params = array(), $permissions = null)
 {
     $buffer = 20;
     $maxTimes = 3;
     $groups = array();
     $start = !empty($params['start']) ? $params['start'] : 0;
     $limit = !empty($params['limit']) ? $params['limit'] + $buffer : false;
     $totalResults = null;
     $times = 0;
     while (true) {
         if ($times > 0) {
             Z_Core::logError('Getting more groups in Zotero_Groups::getAllAdvanced()');
         }
         $calcFoundRows = !$totalResults;
         $cacheFoundRows = $calcFoundRows && !$userID;
         // If we don't yet have a row count and this isn't a user-specific search,
         // try to get a cached row count.
         if ($cacheFoundRows) {
             $foundRowsCacheKey = self::getCacheComponentFromParam($params, 'q') . "," . self::getCacheComponentFromParam($params, 'fq');
             $foundRowsTTL = 180;
             $foundRowsLockTTL = 10;
             $foundRowsRealTTL = 3600;
             $obj = Z_Core::$MC->get($foundRowsCacheKey);
             if ($obj) {
                 $foundRows = $obj['rows'];
                 $exp = $obj['exp'];
                 // If count was found but is past the expiration time, check if another
                 // request is getting the row count, and fetch it if not
                 if ($exp < time()) {
                     if (!Z_Core::$MC->add($foundRowsCacheKey . "Lock", true, $foundRowsLockTTL)) {
                         $calcFoundRows = false;
                     }
                 } else {
                     $calcFoundRows = false;
                 }
             }
         }
         $sql = "SELECT " . ($calcFoundRows ? "SQL_CALC_FOUND_ROWS " : "") . "G.groupID, GUO.userID AS ownerUserID FROM groups G " . "JOIN groupUsers GUO ON (G.groupID=GUO.groupID AND GUO.role='owner') ";
         $sqlParams = array();
         if ($userID) {
             $sql .= "JOIN groupUsers GUA ON (G.groupID=GUA.groupID) WHERE GUA.userID=? ";
             $sqlParams[] = $userID;
         }
         $paramSQL = "";
         $includeEmpty = false;
         if (!empty($params['q'])) {
             if (!is_array($params['q'])) {
                 $params['q'] = array($params['q']);
             }
             foreach ($params['q'] as $q) {
                 $field = explode(":", $q);
                 if (sizeOf($field) == 2) {
                     switch ($field[0]) {
                         case 'slug':
                             $includeEmpty = true;
                             break;
                         default:
                             throw new Exception("Cannot search by group field '{$field[0]}'", Z_ERROR_INVALID_GROUP_TYPE);
                     }
                     $paramSQL .= "AND " . $field[0];
                     // If first character is '-', negate
                     $paramSQL .= $field[0][0] == '-' ? '!' : '';
                     $paramSQL .= "=? ";
                     $sqlParams[] = $field[1];
                 } else {
                     $paramSQL .= "AND name LIKE ? ";
                     $sqlParams[] = "%{$q}%";
                 }
             }
         }
         if (!$userID) {
             if ($includeEmpty) {
                 $sql .= "WHERE 1 ";
             } else {
                 // Don't include groups that have never had items
                 $sql .= "JOIN libraries L ON (G.libraryID=L.libraryID)\n\t\t\t\t\t\t\tWHERE L.lastUpdated != '0000-00-00 00:00:00' ";
             }
         }
         $sql .= $paramSQL;
         if (!empty($params['fq'])) {
             if (!is_array($params['fq'])) {
                 $params['fq'] = array($params['fq']);
             }
             foreach ($params['fq'] as $fq) {
                 $facet = explode(":", $fq);
                 if (sizeOf($facet) == 2 && preg_match('/-?GroupType/', $facet[0])) {
                     switch ($facet[1]) {
                         case 'PublicOpen':
                         case 'PublicClosed':
                         case 'Private':
                             break;
                         default:
                             throw new Exception("Invalid group type '{$facet[1]}'", Z_ERROR_INVALID_GROUP_TYPE);
                     }
                     $sql .= "AND type";
                     // If first character is '-', negate
                     $sql .= $facet[0][0] == '-' ? '!' : '';
                     $sql .= "=? ";
                     $sqlParams[] = $facet[1];
                 }
             }
         }
         if (!empty($params['sort'])) {
             $order = $params['sort'];
             if ($order == 'title') {
                 $order = 'name';
             }
             $sql .= "ORDER BY {$order}";
             if (!empty($params['direction'])) {
                 $sql .= " " . $params['direction'] . " ";
             }
         }
         // Limit is set $buffer higher than the actual limit, in case some groups are
         // removed during access checks
         //
         // Actual limiting is done below
         if ($limit) {
             $sql .= "LIMIT ?, ?";
             $sqlParams[] = $start;
             $sqlParams[] = $limit;
         }
         $rows = Zotero_DB::query($sql, $sqlParams);
         if (!$rows) {
             break;
         }
         if (is_null($totalResults)) {
             if ($calcFoundRows) {
                 $foundRows = Zotero_DB::valueQuery("SELECT FOUND_ROWS()");
                 // Cache found rows count, and store earlier expiration time so that one
                 // request can trigger a recalculation before cached value expires
                 if ($cacheFoundRows) {
                     Z_Core::$MC->set($foundRowsCacheKey, ['rows' => $foundRows, 'exp' => time() + $foundRowsTTL], $foundRowsRealTTL);
                 }
             }
             $totalResults = $foundRows;
         }
         // Include only groups with non-banned owners
         $owners = array();
         foreach ($rows as $row) {
             $owners[] = $row['ownerUserID'];
         }
         $owners = Zotero_Users::getValidUsers($owners);
         $ids = array();
         foreach ($rows as $row) {
             if (!in_array($row['ownerUserID'], $owners)) {
                 $totalResults--;
                 continue;
             }
             $ids[] = $row['groupID'];
         }
         $batchStartPos = sizeOf($groups);
         foreach ($ids as $id) {
             $group = Zotero_Groups::get($id, true);
             $groups[] = $group;
         }
         // Remove groups that can't be accessed
         if ($permissions) {
             for ($i = $batchStartPos; $i < sizeOf($groups); $i++) {
                 if (!$permissions->canAccess($groups[$i]->libraryID, 'view')) {
                     array_splice($groups, $i, 1);
                     $i--;
                     $totalResults--;
                 }
             }
         }
         $times++;
         if ($times == $maxTimes) {
             Z_Core::logError('Too many queries in Zotero_Groups::getAllAdvanced()');
             break;
         }
         if (empty($params['limit'])) {
             break;
         }
         // If we have enough groups to fill the limit, stop
         if (sizeOf($groups) > $params['limit']) {
             break;
         }
         // If there no more rows, stop
         if ($start + sizeOf($rows) >= $foundRows) {
             break;
         }
         $start = $start + sizeOf($rows);
         // Get number we still need plus the buffer or all remaining, whichever is lower
         $limit = min($params['limit'] - sizeOf($groups) + $buffer, $foundRows - $start);
     }
     // TODO: generate previous start value
     if (!$groups) {
         return array('results' => array(), 'total' => 0);
     }
     // Fake limiting -- we can't just use SQL limit because
     // some groups might be inaccessible
     if (!empty($params['limit'])) {
         $groups = array_slice($groups, 0, $params['limit']);
     }
     $results = array('results' => $groups, 'total' => $totalResults);
     return $results;
 }