public static function getName($charsetOrCharsetID) { if (isset(self::$charsets[$charsetOrCharsetID])) { return self::$charsets[$charsetOrCharsetID]; } $sql = "(SELECT charset FROM charsets WHERE charsetID=?) UNION\n\t\t\t\t(SELECT charset FROM charsets WHERE charset=?) LIMIT 1"; $charset = Zotero_DB::valueQuery($sql, array($charsetOrCharsetID, $charsetOrCharsetID)); self::$charsets[$charsetOrCharsetID] = $charset; return $charset; }
public function run($id = null) { $this->id = $id; $this->addr = gethostbyname(gethostname()); if (!Z_CONFIG::$PROCESSORS_ENABLED) { $sleep = 20; $this->log("Processors disabled — exiting in {$sleep} seconds"); sleep($sleep); try { $this->notifyProcessor("LOCK" . " " . $id); } catch (Exception $e) { $this->log($e); } return; } $this->log("Starting sync processor"); $startTime = microtime(true); try { $processed = $this->processFromQueue(); if (Zotero_DB::transactionInProgress()) { error_log("WARNING: Transaction still in progress after processing!"); } } catch (Exception $e) { $this->log($e); throw $e; } $duration = microtime(true) - $startTime; $error = false; // Success if ($processed == 1) { $this->log("Process completed in " . round($duration, 2) . " seconds"); $signal = "DONE"; } else { if ($processed == 0) { $this->log("Exiting with no processes found"); $signal = "NONE"; } else { if ($processed == -1) { $this->log("Exiting on lock error"); $signal = "LOCK"; } else { $this->log("Exiting on error"); $signal = "ERROR"; } } } if ($id) { try { $this->notifyProcessor($signal . " " . $id); } catch (Exception $e) { $this->log($e); } } }
public static function add($userID) { Z_Core::debug("Creating publications library for user {$userID}"); Zotero_DB::beginTransaction(); // Use same shard as user library $shardID = Zotero_Shards::getByUserID($userID); $libraryID = Zotero_Libraries::add('publications', $shardID); $sql = "INSERT INTO userPublications (userID, libraryID) VALUES (?, ?)"; Zotero_DB::query($sql, [$userID, $libraryID]); Zotero_DB::commit(); return $libraryID; }
public static function getCreatorsWithData($libraryID, $creator, $sortByItemCountDesc = false) { $sql = "SELECT creatorID FROM creators "; if ($sortByItemCountDesc) { $sql .= "LEFT JOIN itemCreators USING (creatorID) "; } $sql .= "WHERE libraryID=? AND firstName COLLATE utf8_bin = ? " . "AND lastName COLLATE utf8_bin = ? AND fieldMode=?"; if ($sortByItemCountDesc) { $sql .= " ORDER BY IFNULL(COUNT(*), 0) DESC"; } $ids = Zotero_DB::columnQuery($sql, array($libraryID, $creator->firstName, $creator->lastName, $creator->fieldMode), Zotero_Shards::getByLibraryID($libraryID)); return $ids; }
public static function generate() { $tries = 5; while ($tries > 0) { $str = Zotero_Utilities::randomString(24, 'mixed'); $sql = "SELECT COUNT(*) FROM `keys` WHERE `key`=?"; if (Zotero_DB::valueQuery($sql, $str)) { $tries--; continue; } return $str; } throw new Exception("Unique key could not be generated"); }
protected function loadChildItems($reload = false) { if ($this->loaded['childItems'] && !$reload) { return; } Z_Core::debug("Loading child items for collection {$this->id}"); if (!$this->id) { trigger_error('$this->id not set', E_USER_ERROR); } $sql = "SELECT itemID FROM collectionItems WHERE collectionID=?"; $ids = Zotero_DB::columnQuery($sql, $this->id, Zotero_Shards::getByLibraryID($this->libraryID)); $this->childItems = $ids ? $ids : []; $this->loaded['childItems'] = true; $this->clearChanged('childItems'); }
public static function getUserGroupLibraries($userID) { $sql = "SELECT libraryID FROM groupUsers JOIN groups USING (groupID) WHERE userID=?"; $libraryIDs = Zotero_DB::columnQuery($sql, $userID); if (!$libraryIDs) { return array(); } return $libraryIDs; }
private static function loadItems($libraryID, $itemIDs = array()) { $shardID = Zotero_Shards::getByLibraryID($libraryID); $sql = self::getPrimaryDataSQL() . "1"; // TODO: optimize if ($itemIDs) { foreach ($itemIDs as $itemID) { if (!is_int($itemID)) { throw new Exception("Invalid itemID {$itemID}"); } } $sql .= ' AND itemID IN (' . implode(',', array_fill(0, sizeOf($itemIDs), '?')) . ')'; } $stmt = Zotero_DB::getStatement($sql, "loadItems_" . sizeOf($itemIDs), $shardID); $itemRows = Zotero_DB::queryFromStatement($stmt, $itemIDs); $loadedItemIDs = array(); if ($itemRows) { foreach ($itemRows as $row) { if ($row['libraryID'] != $libraryID) { throw new Exception("Item {$itemID} isn't in library {$libraryID}", Z_ERROR_OBJECT_LIBRARY_MISMATCH); } $itemID = $row['id']; $loadedItemIDs[] = $itemID; // Item isn't loaded -- create new object and stuff in array if (!isset(self::$objectCache[$itemID])) { $item = new Zotero_Item(); $item->loadFromRow($row, true); self::$objectCache[$itemID] = $item; } else { self::$objectCache[$itemID]->loadFromRow($row, true); } } } if (!$itemIDs) { // If loading all items, remove old items that no longer exist $ids = array_keys(self::$objectCache); foreach ($ids as $id) { if (!in_array($id, $loadedItemIDs)) { throw new Exception("Unimplemented"); //$this->unload($id); } } } }
/** * Delete data from memcached */ public static function deleteCachedData($libraryID) { $shardID = Zotero_Shards::getByLibraryID($libraryID); // Clear itemID-specific memcache values $sql = "SELECT itemID FROM items WHERE libraryID=?"; $itemIDs = Zotero_DB::columnQuery($sql, $libraryID, $shardID); if ($itemIDs) { $cacheKeys = array("itemCreators", "itemIsDeleted", "itemRelated", "itemUsedFieldIDs", "itemUsedFieldNames"); foreach ($itemIDs as $itemID) { foreach ($cacheKeys as $key) { Z_Core::$MC->delete($key . '_' . $itemID); } } } /*foreach (Zotero_DataObjects::$objectTypes as $type=>$arr) { $className = "Zotero_" . $arr['plural']; call_user_func(array($className, "clearPrimaryDataCache"), $libraryID); }*/ }
private static function load($libraryID, $ids = [], array $options = []) { $loaded = []; if (!$libraryID) { throw new Exception("libraryID must be provided"); } if ($libraryID !== false && !empty(self::$loadedLibraries[$libraryID])) { return $loaded; } $sql = self::getPrimaryDataSQL() . ' AND O.libraryID=?'; $params = [$libraryID]; if ($ids) { $sql .= ' AND O.' . self::$idColumn . ' IN (' . implode(',', $ids) . ')'; } $t = microtime(); $rows = Zotero_DB::query($sql, $params, Zotero_Shards::getByLibraryID($libraryID)); foreach ($rows as $row) { $id = $row['id']; // Existing object -- reload in place if (isset(self::$objectCache[$id])) { self::$objectCache[$id]->loadFromRow($row, true); $obj = self::$objectCache[$id]; } else { $class = "Zotero_" . self::$ObjectType; $obj = new $class(); $obj->loadFromRow($row, true); if (!$options || !$options->noCache) { self::registerObject($obj); } } $loaded[$id] = $obj; } Z_Core::debug("Loaded " . self::$objectTypePlural . " in " . (microtime() - $t) . "ms"); if (!$ids) { self::$loadedLibraries[$libraryID] = true; // If loading all objects, remove cached objects that no longer exist foreach (self::$objectCache as $obj) { if ($libraryID !== false && obj . libraryID !== libraryID) { continue; } if (empty($loaded[$obj->id])) { self::unload($obj->id); } } } return $loaded; }
require 'StatsD.inc.php'; // Use DB read replicas for GET requests if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] == 'GET') { Zotero_DB::readOnly(true); } // Database callbacks Zotero_DB::addCallback("begin", array("Zotero_Notifier", "begin")); Zotero_DB::addCallback("commit", array("Zotero_Notifier", "commit")); Zotero_DB::addCallback("callback", array("Zotero_Notifier", "reset")); Zotero_NotifierObserver::init(); // Memcached require 'Memcached.inc.php'; Z_Core::$MC = new Z_MemcachedClientLocal(Z_CONFIG::$SYNC_DOMAIN, array('disabled' => !Z_CONFIG::$MEMCACHED_ENABLED, 'servers' => Z_CONFIG::$MEMCACHED_SERVERS)); Zotero_DB::addCallback("begin", array(Z_Core::$MC, "begin")); Zotero_DB::addCallback("commit", array(Z_Core::$MC, "commit")); Zotero_DB::addCallback("reset", array(Z_Core::$MC, "reset")); // // Set up AWS service factory // $awsConfig = ['region' => !empty(Z_CONFIG::$AWS_REGION) ? Z_CONFIG::$AWS_REGION : 'us-east-1', 'version' => 'latest', 'signature' => 'v4']; // IAM role authentication if (empty(Z_CONFIG::$AWS_ACCESS_KEY)) { // If APC cache is available, use that to cache temporary credentials if (function_exists('apc_store')) { $cache = new \Doctrine\Common\Cache\ApcCache(); } else { $cache = new \Doctrine\Common\Cache\FilesystemCache(Z_ENV_BASE_PATH . 'tmp/cache'); } $awsConfig['credentials'] = new \Aws\DoctrineCacheAdapter($cache); } else { $awsConfig['credentials'] = ['key' => Z_CONFIG::$AWS_ACCESS_KEY, 'secret' => Z_CONFIG::$AWS_SECRET_KEY];
private static function updateLastAdded($storageFileID) { $sql = "UPDATE storageFiles SET lastAdded=NOW() WHERE storageFileID=?"; Zotero_DB::query($sql, $storageFileID); }
private function loadRelatedItems() { if (!$this->id) { return; } Z_Core::debug("Loading related items for item {$this->id}"); if ($this->loaded['relatedItems']) { trigger_error("Related items for item {$this->id} already loaded", E_USER_ERROR); } if (!$this->loaded['primaryData']) { $this->loadPrimaryData(true); } // TODO: use a prepared statement if (!is_numeric($this->id)) { trigger_error("Invalid itemID '{$this->id}'", E_USER_ERROR); } $cacheKey = $this->getCacheKey("itemRelated"); //$ids = Z_Core::$MC->get($cacheKey); $ids = false; if ($ids === false) { $sql = "SELECT linkedItemID FROM itemRelated WHERE itemID=?"; $stmt = Zotero_DB::getStatement($sql, true, Zotero_Shards::getByLibraryID($this->libraryID)); $ids = Zotero_DB::columnQueryFromStatement($stmt, $this->id); Z_Core::$MC->set($cacheKey, $ids ? $ids : array()); } $this->relatedItems = $ids ? $ids : array(); $this->loaded['relatedItems'] = true; }
private function load() { //Z_Core::debug("Loading data for search $this->id"); if (!$this->libraryID) { throw new Exception("Library ID not set"); } if (!$this->id && !$this->key) { throw new Exception("ID or key not set"); } $shardID = Zotero_Shards::getByLibraryID($this->libraryID); $sql = "SELECT searchID AS id, searchName AS name, dateAdded, dateModified, libraryID, `key`,\n\t\t\t\tMAX(searchConditionID) AS maxSearchConditionID FROM savedSearches\n\t\t\t\tLEFT JOIN savedSearchConditions USING (searchID) WHERE "; if ($this->id) { $sql .= "searchID=?"; $params = $this->id; } else { $sql .= "libraryID=? AND `key`=?"; $params = array($this->libraryID, $this->key); } $sql .= " GROUP BY searchID"; $data = Zotero_DB::rowQuery($sql, $params, $shardID); $this->loaded = true; if (!$data) { return; } foreach ($data as $key => $val) { $this->{$key} = $val; } $sql = "SELECT * FROM savedSearchConditions\n\t\t\t\tWHERE searchID=? ORDER BY searchConditionID"; $conditions = Zotero_DB::query($sql, $this->id, $shardID); foreach ($conditions as $condition) { /* if (!Zotero.SearchConditions.get(condition)){ Zotero.debug("Invalid saved search condition '" + condition + "' -- skipping", 2); continue; } */ $searchConditionID = $condition['searchConditionID']; $this->conditions[$searchConditionID] = array('id' => $searchConditionID, 'condition' => $condition['condition'], 'mode' => $condition['mode'], 'operator' => $condition['operator'], 'value' => $condition['value'], 'required' => $condition['required']); } }
private function end() { if (!$this->responseXML instanceof SimpleXMLElement) { throw new Exception("Response XML not provided"); } if ($this->profile) { Zotero_DB::profileEnd($this->profileShard, false); } if ($this->responseCode) { switch ($this->responseCode) { case 201: header("HTTP/1.1 201 Created"); break; default: throw new Exception("Unsupported response code"); } } else { $updated = (string) $this->responseXML->updated; if ($updated) { $updated = strtotime($updated); $ifModifiedSince = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? $_SERVER['HTTP_IF_MODIFIED_SINCE'] : false; $ifModifiedSince = strtotime($ifModifiedSince); if ($ifModifiedSince >= $updated) { header('HTTP/1.1 304 Not Modified'); exit; } $lastModified = substr(date('r', $updated), 0, -5) . "GMT"; header("Last-Modified: {$lastModified}"); } } $xmlstr = $this->responseXML->asXML(); $doc = new DOMDocument('1.0'); $doc->loadXML($xmlstr); $doc->formatOutput = true; if ($this->queryParams['pprint']) { $ppdoc = new DOMDocument('1.0'); // Zero-width spaces to push <feed> beyond Firefox's // feed auto-detection boundary $comment = $ppdoc->createComment(""); $ppdoc->appendChild($comment); $ppdoc->formatOutput = true; $rootElem = $doc->firstChild; $importedNode = $ppdoc->importNode($rootElem, true); $ppdoc->appendChild($importedNode); $doc = $ppdoc; } $xmlstr = $doc->saveXML(); if ($this->queryParams['pprint']) { header("Content-Type: text/xml"); } else { header("Content-Type: application/atom+xml"); } echo $xmlstr; $this->logRequestTime(); echo ob_get_clean(); exit; }
public static function addCustomType($name) { if (self::getID($name)) { throw new Exception("Item type '{$name}' already exists"); } if (!preg_match('/^[a-z][^\\s0-9]+$/', $name)) { throw new Exception("Invalid item type name '{$name}'"); } // TODO: make sure user hasn't added too many already throw new Exception("Unimplemented"); // TODO: add to cache Zotero_DB::beginTransaction(); $sql = "SELECT NEXT_ID(itemTypeID) FROM itemTypes"; $itemTypeID = Zotero_DB::valueQuery($sql); $sql = "INSERT INTO itemTypes (?, ?, ?)"; Zotero_DB::query($sql, array($itemTypeID, $name, 1)); Zotero_DB::commit(); return $itemTypeID; }
private static function getDeletedObjectIDs($userID, $timestamp, $includeAllUserObjects = false) { /* $sql = "SELECT version FROM version WHERE schema='syncdeletelog'"; $syncLogStart = Zotero_DB::valueQuery($sql); if (!$syncLogStart) { throw ('Sync log start time not found'); } */ /* // Last sync time is before start of log if ($lastSyncDate && new Date($syncLogStart * 1000) > $lastSyncDate) { return -1; } */ // Personal library $shardID = Zotero_Shards::getByUserID($userID); $libraryID = Zotero_Users::getLibraryIDFromUserID($userID); $shardLibraryIDs[$shardID] = array($libraryID); // Group libraries if ($includeAllUserObjects) { $groupIDs = Zotero_Groups::getUserGroups($userID); if ($groupIDs) { // Separate groups into shards for querying foreach ($groupIDs as $groupID) { $libraryID = Zotero_Groups::getLibraryIDFromGroupID($groupID); $shardID = Zotero_Shards::getByLibraryID($libraryID); if (!isset($shardLibraryIDs[$shardID])) { $shardLibraryIDs[$shardID] = array(); } $shardLibraryIDs[$shardID][] = $libraryID; } } } // Send query at each shard $rows = array(); foreach ($shardLibraryIDs as $shardID => $libraryIDs) { $sql = "SELECT libraryID, objectType, id, timestamp\n\t\t\t\t\tFROM syncDeleteLogIDs WHERE libraryID IN (" . implode(', ', array_fill(0, sizeOf($libraryIDs), '?')) . ")"; $params = $libraryIDs; if ($timestamp) { // Send any entries from before these were being properly sent if ($timestamp < 1260778500) { $sql .= " AND (timestamp >= FROM_UNIXTIME(?) OR timestamp BETWEEN 1257968068 AND FROM_UNIXTIME(?))"; $params[] = $timestamp; $params[] = 1260778500; } else { $sql .= " AND timestamp >= FROM_UNIXTIME(?)"; $params[] = $timestamp; } } $sql .= " ORDER BY timestamp"; $shardRows = Zotero_DB::query($sql, $params, $shardID); if ($shardRows) { $rows = array_merge($rows, $shardRows); } } if (!$rows) { return false; } $deletedIDs = array('groups' => array()); foreach ($rows as $row) { $type = $row['objectType'] . 's'; $deletedIDs[$type][] = $row['id']; } return $deletedIDs; }
public static function loadHashes($libraryID) { $sql = "SELECT itemID, hash FROM itemNotes JOIN items USING (itemID) WHERE libraryID=?"; $hashes = Zotero_DB::query($sql, $libraryID, Zotero_Shards::getByLibraryID($libraryID)); if (!$hashes) { return; } if (!isset(self::$hashCache[$libraryID])) { self::$hashCache[$libraryID] = array(); } foreach ($hashes as $hash) { if ($hash['hash']) { self::$hashCache[$libraryID][$hash['itemID']] = $hash['hash']; } } }
public function keys() { $userID = $this->objectUserID; $key = $this->objectName; $this->allowMethods(['GET', 'POST', 'PUT', 'DELETE']); if ($this->method == 'GET') { // Single key if ($key) { $keyObj = Zotero_Keys::getByKey($key); if (!$keyObj) { $this->e404("Key not found"); } // /users/<userID>/keys/<keyID> (deprecated) if ($userID) { // If we have a userID, make sure it matches if ($keyObj->userID != $userID) { $this->e404("Key not found"); } } else { if ($this->apiVersion < 3) { $this->e404(); } } if ($this->apiVersion >= 3) { $json = $keyObj->toJSON(); // If not super-user, don't include name or recent IP addresses if (!$this->permissions->isSuper()) { unset($json['dateAdded']); unset($json['lastUsed']); unset($json['name']); unset($json['recentIPs']); } header('application/json'); echo Zotero_Utilities::formatJSON($json); } else { $this->responseXML = $keyObj->toXML(); // If not super-user, don't include name or recent IP addresses if (!$this->permissions->isSuper()) { unset($this->responseXML['dateAdded']); unset($this->responseXML['lastUsed']); unset($this->responseXML->name); unset($this->responseXML->recentIPs); } } } else { if (!$this->permissions->isSuper()) { $this->e403(); } $keyObjs = Zotero_Keys::getUserKeys($userID); if ($keyObjs) { if ($this->apiVersion >= 3) { $json = []; foreach ($keyObjs as $keyObj) { $json[] = $keyObj->toJSON(); } echo Zotero_Utilities::formatJSON($json); } else { $xml = new SimpleXMLElement('<keys/>'); $domXML = dom_import_simplexml($xml); foreach ($keyObjs as $keyObj) { $keyXML = $keyObj->toXML(); $domKeyXML = dom_import_simplexml($keyXML); $node = $domXML->ownerDocument->importNode($domKeyXML, true); $domXML->appendChild($node); } $this->responseXML = $xml; } } } } else { if ($this->method == 'DELETE') { if (!$key) { $this->e400("DELETE requests must end with a key"); } Zotero_DB::beginTransaction(); $keyObj = Zotero_Keys::getByKey($key); if (!$keyObj) { $this->e404("Key '{$key}' does not exist"); } $keyObj->erase(); Zotero_DB::commit(); header("HTTP/1.1 204 No Content"); exit; } else { // Require super-user for modifications if (!$this->permissions->isSuper()) { $this->e403(); } if ($this->method == 'POST') { if ($key) { $this->e400("POST requests cannot end with a key (did you mean PUT?)"); } if ($this->apiVersion >= 3) { $json = json_decode($this->body, true); if (!$json) { $this->e400("{$this->method} data is not valid JSON"); } if (!empty($json['key'])) { $this->e400("POST requests cannot contain a key in '" . $this->body . "'"); } $fields = $this->getFieldsFromJSON($json); } else { try { $keyXML = @new SimpleXMLElement($this->body); } catch (Exception $e) { $this->e400("{$this->method} data is not valid XML"); } if (!empty($key['key'])) { $this->e400("POST requests cannot contain a key in '" . $this->body . "'"); } $fields = $this->getFieldsFromKeyXML($keyXML); } Zotero_DB::beginTransaction(); try { $keyObj = new Zotero_Key(); $keyObj->userID = $userID; foreach ($fields as $field => $val) { if ($field == 'access') { foreach ($val as $access) { $this->setKeyPermissions($keyObj, $access); } } else { $keyObj->{$field} = $val; } } $keyObj->save(); } catch (Exception $e) { if ($e->getCode() == Z_ERROR_KEY_NAME_TOO_LONG) { $this->e400($e->getMessage()); } $this->handleException($e); } if ($this->apiVersion >= 3) { header('application/json'); echo Zotero_Utilities::formatJSON($keyObj->toJSON()); } else { $this->responseXML = $keyObj->toXML(); } Zotero_DB::commit(); $url = Zotero_API::getKeyURI($keyObj); $this->responseCode = 201; header("Location: " . $url, false, 201); } else { if ($this->method == 'PUT') { if (!$key) { $this->e400("PUT requests must end with a key (did you mean POST?)"); } if ($this->apiVersion >= 3) { $json = json_decode($this->body, true); if (!$json) { $this->e400("{$this->method} data is not valid JSON"); } $fields = $this->getFieldsFromJSON($json); } else { try { $keyXML = @new SimpleXMLElement($this->body); } catch (Exception $e) { $this->e400("{$this->method} data is not valid XML"); } $fields = $this->getFieldsFromKeyXML($keyXML); } // Key attribute is optional, but, if it's there, make sure it matches if (isset($fields['key']) && $fields['key'] != $key) { $this->e400("Key '{$fields['key']}' does not match key '{$key}' from URI"); } Zotero_DB::beginTransaction(); try { $keyObj = Zotero_Keys::getByKey($key); if (!$keyObj) { $this->e404("Key '{$key}' does not exist"); } foreach ($fields as $field => $val) { if ($field == 'access') { foreach ($val as $access) { $this->setKeyPermissions($keyObj, $access); } } else { $keyObj->{$field} = $val; } } $keyObj->save(); } catch (Exception $e) { if ($e->getCode() == Z_ERROR_KEY_NAME_TOO_LONG) { $this->e400($e->getMessage()); } $this->handleException($e); } if ($this->apiVersion >= 3) { echo Zotero_Utilities::formatJSON($keyObj->toJSON()); } else { $this->responseXML = $keyObj->toXML(); } Zotero_DB::commit(); } } } } if ($this->apiVersion >= 3) { $this->end(); } else { header('Content-Type: application/xml'); $xmlstr = $this->responseXML->asXML(); $doc = new DOMDocument('1.0'); $doc->loadXML($xmlstr); $doc->formatOutput = true; echo $doc->saveXML(); exit; } }
public static function deleteByLibraryMySQL($libraryID) { $sql = "DELETE IFT FROM itemFulltext IFT JOIN items USING (itemID) WHERE libraryID=?"; Zotero_DB::query($sql, $libraryID, Zotero_Shards::getByLibraryID($libraryID)); }
private function loadLinkedItems() { Z_Core::debug("Loading linked items for tag {$this->id}"); if (!$this->id && !$this->key) { $this->linkedItemsLoaded = true; return; } if (!$this->loaded) { $this->load(); } if (!$this->id) { $this->linkedItemsLoaded = true; return; } $sql = "SELECT itemID FROM itemTags WHERE tagID=?"; $stmt = Zotero_DB::getStatement($sql, true, Zotero_Shards::getByLibraryID($this->libraryID)); $ids = Zotero_DB::columnQueryFromStatement($stmt, $this->id); $this->linkedItems = array(); if ($ids) { $this->linkedItems = Zotero_Items::get($this->libraryID, $ids); } $this->linkedItemsLoaded = true; }
public function storageadmin() { if (!$this->permissions->isSuper()) { $this->e404(); } $this->allowMethods(array('GET', 'POST')); Zotero_DB::beginTransaction(); if ($this->method == 'POST') { if (!isset($_POST['quota'])) { $this->e400("Quota not provided"); } // Accept 'unlimited' via API if ($_POST['quota'] == 'unlimited') { $_POST['quota'] = self::UNLIMITED; } if (!isset($_POST['expiration'])) { $this->e400("Expiration not provided"); } if (!is_numeric($_POST['quota']) || $_POST['quota'] < 0) { $this->e400("Invalid quota"); } if (!is_numeric($_POST['expiration'])) { $this->e400("Invalid expiration"); } $halfHourAgo = strtotime("-30 minutes"); if ($_POST['expiration'] != 0 && $_POST['expiration'] < $halfHourAgo) { $this->e400("Expiration is in the past"); } try { Zotero_Storage::setUserValues($this->objectUserID, $_POST['quota'], $_POST['expiration']); } catch (Exception $e) { if ($e->getCode() == Z_ERROR_GROUP_QUOTA_SET_BELOW_USAGE) { $this->e409("Cannot set quota below current usage"); } $this->handleException($e); } } // GET request $xml = new SimpleXMLElement('<storage/>'); $quota = Zotero_Storage::getEffectiveUserQuota($this->objectUserID); $xml->quota = $quota; $instQuota = Zotero_Storage::getInstitutionalUserQuota($this->objectUserID); // If personal quota is in effect if (!$instQuota || $quota > $instQuota) { $values = Zotero_Storage::getUserValues($this->objectUserID); if ($values) { $xml->expiration = (int) $values['expiration']; } } // Return 'unlimited' via API if ($quota == self::UNLIMITED) { $xml->quota = 'unlimited'; } $usage = Zotero_Storage::getUserUsage($this->objectUserID); $xml->usage->total = $usage['total']; $xml->usage->library = $usage['library']; foreach ($usage['groups'] as $group) { if (!isset($group['id'])) { throw new Exception("Group id isn't set"); } if (!isset($group['usage'])) { throw new Exception("Group usage isn't set"); } $xmlGroup = $xml->usage->addChild('group', $group['usage']); $xmlGroup['id'] = $group['id']; } Zotero_DB::commit(); header('application/xml'); echo $xml->asXML(); exit; }
public function save() { if (!$this->libraryID) { trigger_error("Library ID must be set before saving", E_USER_ERROR); } Zotero_Creators::editCheck($this); // If empty, move on if ($this->firstName === '' && $this->lastName === '') { throw new Exception('First and last name are empty'); } if ($this->fieldMode == 1 && $this->firstName !== '') { throw new Exception('First name must be empty in single-field mode'); } if (!$this->hasChanged()) { Z_Core::debug("Creator {$this->id} has not changed"); return false; } Zotero_DB::beginTransaction(); try { $creatorID = $this->id ? $this->id : Zotero_ID::get('creators'); $isNew = !$this->id; Z_Core::debug("Saving creator {$this->id}"); $key = $this->key ? $this->key : $this->generateKey(); $timestamp = Zotero_DB::getTransactionTimestamp(); $dateAdded = $this->dateAdded ? $this->dateAdded : $timestamp; $dateModified = $this->changed['dateModified'] ? $this->dateModified : $timestamp; $fields = "firstName=?, lastName=?, fieldMode=?,\n\t\t\t\t\t\tlibraryID=?, `key`=?, dateAdded=?, dateModified=?, serverDateModified=?"; $params = array($this->firstName, $this->lastName, $this->fieldMode, $this->libraryID, $key, $dateAdded, $dateModified, $timestamp); $shardID = Zotero_Shards::getByLibraryID($this->libraryID); try { if ($isNew) { $sql = "INSERT INTO creators SET creatorID=?, {$fields}"; $stmt = Zotero_DB::getStatement($sql, true, $shardID); Zotero_DB::queryFromStatement($stmt, array_merge(array($creatorID), $params)); // Remove from delete log if it's there $sql = "DELETE FROM syncDeleteLogKeys WHERE libraryID=? AND objectType='creator' AND `key`=?"; Zotero_DB::query($sql, array($this->libraryID, $key), $shardID); } else { $sql = "UPDATE creators SET {$fields} WHERE creatorID=?"; $stmt = Zotero_DB::getStatement($sql, true, $shardID); Zotero_DB::queryFromStatement($stmt, array_merge($params, array($creatorID))); } } catch (Exception $e) { if (strpos($e->getMessage(), " too long") !== false) { if (strlen($this->firstName) > 255) { throw new Exception("=First name '" . mb_substr($this->firstName, 0, 50) . "…' too long"); } if (strlen($this->lastName) > 255) { if ($this->fieldMode == 1) { throw new Exception("=Last name '" . mb_substr($this->lastName, 0, 50) . "…' too long"); } else { throw new Exception("=Name '" . mb_substr($this->lastName, 0, 50) . "…' too long"); } } } throw $e; } // The client updates the mod time of associated items here, but // we don't, because either A) this is from syncing, where appropriate // mod times come from the client or B) the change is made through // $item->setCreator(), which updates the mod time. // // If the server started to make other independent creator changes, // linked items would need to be updated. Zotero_DB::commit(); Zotero_Creators::cachePrimaryData(array('id' => $creatorID, 'libraryID' => $this->libraryID, 'key' => $key, 'dateAdded' => $dateAdded, 'dateModified' => $dateModified, 'firstName' => $this->firstName, 'lastName' => $this->lastName, 'fieldMode' => $this->fieldMode)); } catch (Exception $e) { Zotero_DB::rollback(); throw $e; } // If successful, set values in object if (!$this->id) { $this->id = $creatorID; } if (!$this->key) { $this->key = $key; } $this->init(); if ($isNew) { Zotero_Creators::cache($this); Zotero_Creators::cacheLibraryKeyID($this->libraryID, $key, $creatorID); } // TODO: invalidate memcache? return $this->id; }
public function collections() { // Check for general library access if (!$this->permissions->canAccess($this->objectLibraryID)) { $this->e403(); } if ($this->isWriteMethod()) { // Check for library write access if (!$this->permissions->canWrite($this->objectLibraryID)) { $this->e403("Write access denied"); } // Make sure library hasn't been modified if (!$this->singleObject) { $libraryTimestampChecked = $this->checkLibraryIfUnmodifiedSinceVersion(); } Zotero_Libraries::updateVersionAndTimestamp($this->objectLibraryID); } $collectionIDs = array(); $collectionKeys = array(); $results = array(); // Single collection if ($this->singleObject) { $this->allowMethods(['HEAD', 'GET', 'PUT', 'PATCH', 'DELETE']); if (!Zotero_ID::isValidKey($this->objectKey)) { $this->e404(); } $collection = Zotero_Collections::getByLibraryAndKey($this->objectLibraryID, $this->objectKey); if ($this->isWriteMethod()) { $collection = $this->handleObjectWrite('collection', $collection ? $collection : null); $this->queryParams['content'] = ['json']; } if (!$collection) { $this->e404("Collection not found"); } $this->libraryVersion = $collection->version; if ($this->method == 'HEAD') { $this->end(); } switch ($this->queryParams['format']) { case 'atom': $this->responseXML = Zotero_Collections::convertCollectionToAtom($collection, $this->queryParams); break; case 'json': $json = $collection->toResponseJSON($this->queryParams, $this->permissions); echo Zotero_Utilities::formatJSON($json); break; default: throw new Exception("Unexpected format '" . $this->queryParams['format'] . "'"); } } else { $this->allowMethods(['HEAD', 'GET', 'POST', 'DELETE']); $this->libraryVersion = Zotero_Libraries::getUpdatedVersion($this->objectLibraryID); if ($this->scopeObject) { $this->allowMethods(array('GET')); switch ($this->scopeObject) { case 'collections': $collection = Zotero_Collections::getByLibraryAndKey($this->objectLibraryID, $this->scopeObjectKey); if (!$collection) { $this->e404("Collection not found"); } $title = "Child Collections of ‘{$collection->name}'’"; $collectionIDs = $collection->getChildCollections(); break; default: throw new Exception("Invalid collections scope object '{$this->scopeObject}'"); } } else { // Top-level items if ($this->subset == 'top') { $this->allowMethods(array('GET')); $title = "Top-Level Collections"; $results = Zotero_Collections::search($this->objectLibraryID, true, $this->queryParams); } else { // Create a collection if ($this->method == 'POST') { $this->queryParams['format'] = 'writereport'; $obj = $this->jsonDecode($this->body); $results = Zotero_Collections::updateMultipleFromJSON($obj, $this->queryParams, $this->objectLibraryID, $this->userID, $this->permissions, $libraryTimestampChecked ? 0 : 1, null); if ($cacheKey = $this->getWriteTokenCacheKey()) { Z_Core::$MC->set($cacheKey, true, $this->writeTokenCacheTime); } if ($this->apiVersion < 2) { $uri = Zotero_API::getCollectionsURI($this->objectLibraryID); $keys = array_merge(get_object_vars($results['success']), get_object_vars($results['unchanged'])); $queryString = "collectionKey=" . urlencode(implode(",", $keys)) . "&format=atom&content=json&order=collectionKeyList&sort=asc"; if ($this->apiKey) { $queryString .= "&key=" . $this->apiKey; } $uri .= "?" . $queryString; $this->queryParams = Zotero_API::parseQueryParams($queryString, $this->action, true, $this->apiVersion); $title = "Collections"; $results = Zotero_Collections::search($this->objectLibraryID, false, $this->queryParams); } } else { if ($this->method == 'DELETE') { Zotero_DB::beginTransaction(); foreach ($this->queryParams['collectionKey'] as $collectionKey) { Zotero_Collections::delete($this->objectLibraryID, $collectionKey); } Zotero_DB::commit(); $this->e204(); } else { $title = "Collections"; $results = Zotero_Collections::search($this->objectLibraryID, false, $this->queryParams); } } } } if ($collectionIDs) { $this->queryParams['collectionIDs'] = $collectionIDs; $results = Zotero_Collections::search($this->objectLibraryID, false, $this->queryParams); } $options = ['action' => $this->action, 'uri' => $this->uri, 'results' => $results, 'requestParams' => $this->queryParams, 'permissions' => $this->permissions, 'head' => $this->method == 'HEAD']; switch ($this->queryParams['format']) { case 'atom': $this->responseXML = Zotero_API::multiResponse(array_merge($options, ['title' => $this->getFeedNamePrefix($this->objectLibraryID) . $title])); break; case 'json': case 'keys': case 'versions': case 'writereport': Zotero_API::multiResponse($options); break; default: throw new Exception("Unexpected format '" . $this->queryParams['format'] . "'"); } } $this->end(); }
protected function loadRelations($reload = false) { if ($this->loaded['relations'] && !$reload) return; if (!$this->id) { return; } Z_Core::debug("Loading relations for item $this->id"); $this->loadPrimaryData(false, true); $itemURI = Zotero_URI::getItemURI($this); $relations = Zotero_Relations::getByURIs($this->libraryID, $itemURI); $relations = array_map(function ($rel) { return [$rel->predicate, $rel->object]; }, $relations); // Related items are bidirectional, so include any with this item as the object $reverseRelations = Zotero_Relations::getByURIs( $this->libraryID, false, Zotero_Relations::$relatedItemPredicate, $itemURI ); foreach ($reverseRelations as $rel) { $r = [$rel->predicate, $rel->subject]; // Only add if not already added in other direction if (!in_array($r, $relations)) { $relations[] = $r; } } // Also include any owl:sameAs relations with this item as the object // (as sent by client via classic sync) $reverseRelations = Zotero_Relations::getByURIs( $this->libraryID, false, Zotero_Relations::$linkedObjectPredicate, $itemURI ); foreach ($reverseRelations as $rel) { $relations[] = [$rel->predicate, $rel->subject]; } // TEMP: Get old-style related items // // Add related items $sql = "SELECT `key` FROM itemRelated IR " . "JOIN items I ON (IR.linkedItemID=I.itemID) " . "WHERE IR.itemID=?"; $relatedItemKeys = Zotero_DB::columnQuery($sql, $this->id, Zotero_Shards::getByLibraryID($this->libraryID)); if ($relatedItemKeys) { $prefix = Zotero_URI::getLibraryURI($this->libraryID) . "/items/"; $predicate = Zotero_Relations::$relatedItemPredicate; foreach ($relatedItemKeys as $key) { $relations[] = [$predicate, $prefix . $key]; } } // Reverse as well $sql = "SELECT `key` FROM itemRelated IR JOIN items I USING (itemID) WHERE IR.linkedItemID=?"; $reverseRelatedItemKeys = Zotero_DB::columnQuery( $sql, $this->id, Zotero_Shards::getByLibraryID($this->libraryID) ); if ($reverseRelatedItemKeys) { $prefix = Zotero_URI::getLibraryURI($this->libraryID) . "/items/"; $predicate = Zotero_Relations::$relatedItemPredicate; foreach ($reverseRelatedItemKeys as $key) { $relations[] = [$predicate, $prefix . $key]; } } $this->relations = $relations; $this->loaded['relations'] = true; $this->clearChanged('relations'); }
private static function deleteLibrary($libraryID, $shardID) { $sql = "DELETE FROM shardLibraries WHERE libraryID=?"; Zotero_DB::query($sql, $libraryID, $shardID); }
public static function search($libraryID, $params) { $results = array('results' => array(), 'total' => 0); // Default empty library if ($libraryID === 0) { return $results; } $shardID = Zotero_Shards::getByLibraryID($libraryID); $sql = "SELECT SQL_CALC_FOUND_ROWS DISTINCT tagID FROM tags " . "JOIN itemTags USING (tagID) WHERE libraryID=? "; $sqlParams = array($libraryID); // Pass a list of tagIDs, for when the initial search is done via SQL $tagIDs = !empty($params['tagIDs']) ? $params['tagIDs'] : array(); // Filter for specific tags with "?tag=foo || bar" $tagNames = !empty($params['tag']) ? explode(' || ', $params['tag']) : array(); if ($tagIDs) { $sql .= "AND tagID IN (" . implode(', ', array_fill(0, sizeOf($tagIDs), '?')) . ") "; $sqlParams = array_merge($sqlParams, $tagIDs); } if ($tagNames) { $sql .= "AND `name` IN (" . implode(', ', array_fill(0, sizeOf($tagNames), '?')) . ") "; $sqlParams = array_merge($sqlParams, $tagNames); } if (!empty($params['q'])) { if (!is_array($params['q'])) { $params['q'] = array($params['q']); } foreach ($params['q'] as $q) { $sql .= "AND name LIKE ? "; $sqlParams[] = "%{$q}%"; } } $tagTypeSets = Zotero_API::getSearchParamValues($params, 'tagType'); if ($tagTypeSets) { $positives = array(); $negatives = array(); foreach ($tagTypeSets as $set) { if ($set['negation']) { $negatives = array_merge($negatives, $set['values']); } else { $positives = array_merge($positives, $set['values']); } } if ($positives) { $sql .= "AND type IN (" . implode(',', array_fill(0, sizeOf($positives), '?')) . ") "; $sqlParams = array_merge($sqlParams, $positives); } if ($negatives) { $sql .= "AND type NOT IN (" . implode(',', array_fill(0, sizeOf($negatives), '?')) . ") "; $sqlParams = array_merge($sqlParams, $negatives); } } if (!empty($params['since'])) { $sql .= "AND version > ? "; $sqlParams[] = $params['since']; } if (!empty($params['sort'])) { $order = $params['sort']; if ($order == 'title') { // Force a case-insensitive sort $sql .= "ORDER BY name COLLATE utf8_unicode_ci "; } else { if ($order == 'numItems') { $sql .= "GROUP BY tags.tagID ORDER BY COUNT(tags.tagID)"; } else { $sql .= "ORDER BY {$order} "; } } if (!empty($params['direction'])) { $sql .= " " . $params['direction'] . " "; } } if (!empty($params['limit'])) { $sql .= "LIMIT ?, ?"; $sqlParams[] = $params['start'] ? $params['start'] : 0; $sqlParams[] = $params['limit']; } $ids = Zotero_DB::columnQuery($sql, $sqlParams, $shardID); $results['total'] = Zotero_DB::valueQuery("SELECT FOUND_ROWS()", false, $shardID); if ($ids) { $tags = array(); foreach ($ids as $id) { $tags[] = Zotero_Tags::get($libraryID, $id); } $results['results'] = $tags; } return $results; }
public static function delete($libraryID, $key, $updateLibrary = false) { $table = static::field('table'); $id = static::field('id'); $type = static::field('object'); $types = static::field('objects'); if (!$key) { throw new Exception("Invalid key {$key}"); } // Get object (and trigger caching) $obj = static::getByLibraryAndKey($libraryID, $key); if (!$obj) { return; } static::editCheck($obj); Z_Core::debug("Deleting {$type} {$libraryID}/{$key}", 4); $shardID = Zotero_Shards::getByLibraryID($libraryID); Zotero_DB::beginTransaction(); // Needed for API deletes to get propagated via sync if ($updateLibrary) { $timestamp = Zotero_Libraries::updateTimestamps($obj->libraryID); Zotero_DB::registerTransactionTimestamp($timestamp); } // Delete child items if ($type == 'item') { if ($obj->isRegularItem()) { $children = array_merge($obj->getNotes(), $obj->getAttachments()); if ($children) { $children = Zotero_Items::get($libraryID, $children); foreach ($children as $child) { static::delete($child->libraryID, $child->key); } } } } if ($type == 'relation') { // TODO: add key column to relations to speed this up $sql = "DELETE FROM {$table} WHERE libraryID=? AND MD5(CONCAT(subject, '_', predicate, '_', object))=?"; $deleted = Zotero_DB::query($sql, array($libraryID, $key), $shardID); } else { $sql = "DELETE FROM {$table} WHERE libraryID=? AND `key`=?"; $deleted = Zotero_DB::query($sql, array($libraryID, $key), $shardID); } unset(self::$idCache[$type][$libraryID][$key]); static::uncachePrimaryData($libraryID, $key); if ($deleted) { $sql = "INSERT INTO syncDeleteLogKeys (libraryID, objectType, `key`, timestamp)\n\t\t\t\t\t\tVALUES (?, '{$type}', ?, ?) ON DUPLICATE KEY UPDATE timestamp=?"; $timestamp = Zotero_DB::getTransactionTimestamp(); $params = array($libraryID, $key, $timestamp, $timestamp); Zotero_DB::query($sql, $params, $shardID); } Zotero_DB::commit(); }
public function checkDBTransactionState() { if (Zotero_DB::transactionInProgress()) { error_log("Transaction still in progress at request end! " . "[" . $this->method . " " . $_SERVER['REQUEST_URI'] . "]"); } }
private function load() { $libraryID = $this->libraryID; $id = $this->id; $key = $this->key; Z_Core::debug("Loading data for search " . ($id ? $id : $key)); if (!$libraryID) { throw new Exception("Library ID not set"); } if (!$id && !$key) { throw new Exception("ID or key not set"); } $shardID = Zotero_Shards::getByLibraryID($libraryID); $sql = "SELECT searchID AS id, searchName AS name, dateAdded,\n\t\t\t\tdateModified, libraryID, `key`, version\n\t\t\t\tFROM savedSearches WHERE "; if ($id) { $sql .= "searchID=?"; $params = $id; } else { $sql .= "libraryID=? AND `key`=?"; $params = array($libraryID, $key); } $sql .= " GROUP BY searchID"; $data = Zotero_DB::rowQuery($sql, $params, $shardID); $this->loaded = true; if (!$data) { return; } foreach ($data as $key => $val) { $this->{$key} = $val; } $sql = "SELECT * FROM savedSearchConditions\n\t\t\t\tWHERE searchID=? ORDER BY searchConditionID"; $conditions = Zotero_DB::query($sql, $this->id, $shardID); foreach ($conditions as $condition) { $searchConditionID = $condition['searchConditionID']; $this->conditions[$searchConditionID] = array('id' => $searchConditionID, 'condition' => $condition['condition'], 'mode' => $condition['mode'], 'operator' => $condition['operator'], 'value' => $condition['value'], 'required' => $condition['required']); } }