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 deleteBatch($queueURL, $batchEntries) { Z_Core::debug("Deleting " . sizeOf($batchEntries) . " messages from {$queueURL}", 4); $response = self::$sqs->deleteMessageBatch(['QueueUrl' => $queueURL, 'Entries' => $batchEntries]); $response = self::processResponse($response); if (!$response) { return false; } foreach ($response->body->DeleteMessageBatchResult[0]->BatchResultErrorEntry as $error) { error_log("Error deleting SQS message: " . $error->Code . ": " . $error->Message); } return $response->body->DeleteMessageBatchResult[0]->DeleteMessageBatchResultEntry->count(); }
public static function notifyProcessor($mode, $signal, $addr, $port) { switch ($mode) { case 'download': case 'upload': case 'error': break; default: throw new Exception("Invalid processor mode '{$mode}'"); } if (!$addr) { throw new Exception("Host address not provided"); } Z_Core::debug("Notifying {$mode} processor {$addr} with signal {$signal}"); $socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); $success = socket_sendto($socket, $signal, strlen($signal), MSG_EOF, $addr, $port); if (!$success) { $code = socket_last_error($socket); throw new Exception(socket_strerror($code)); } }
public static function strToDate($string) { // Parse 'yesterday'/'today'/'tomorrow' $lc = strtolower($string); if ($lc == 'yesterday' || $lc == self::getString('date.yesterday')) { $string = date("Y-m-d", strtotime('yesterday')); } else { if ($lc == 'today' || $lc == self::getString('date.today')) { $string = date("Y-m-d"); } else { if ($lc == 'tomorrow' || $lc == self::getString('date.tomorrow')) { $string = date("Y-m-d", strtotime('tomorrow')); } } } $date = array(); // skip empty things if (!$string) { return $date; } $string = preg_replace(array("/^\\s+/", "/\\s+\$/", "/\\s+/"), array("", "", " "), $string); // first, directly inspect the string preg_match(self::$slashRE, $string, $m); if ($m && (empty($m[5]) || $m[3] == $m[5] || $m[3] == "年" && $m[5] == "月") && (!empty($m[2]) && !empty($m[4]) && !empty($m[6]) || empty($m[1]) && empty($m[7]))) { // require that either all parts are found, // or else this is the entire date field // figure out date based on parts if (mb_strlen($m[2]) == 3 || mb_strlen($m[2]) == 4 || $m[3] == "年") { // ISO 8601 style date (big endian) $date['year'] = $m[2]; $date['month'] = $m[4]; $date['day'] = $m[6]; } else { // local style date (middle or little endian) $date['year'] = $m[6]; $country = substr(self::$locale, 3); if ($country == "US" || $country == "FM" || $country == "PW" || $country == "PH") { // The Philippines $date['month'] = $m[2]; $date['day'] = $m[4]; } else { $date['month'] = $m[4]; $date['day'] = $m[2]; } } if ($date['year']) { $date['year'] = (int) $date['year']; } if ($date['day']) { $date['day'] = (int) $date['day']; } if ($date['month']) { $date['month'] = (int) $date['month']; if ($date['month'] > 12) { // swap day and month $tmp = $date['day']; $date['day'] = $date['month']; $date['month'] = $tmp; } } if ((empty($date['month']) || $date['month'] <= 12) && (empty($date['day']) || $date['day'] <= 31)) { if (!empty($date['year']) && $date['year'] < 100) { // for two digit years, determine proper $year = date('Y'); $twoDigitYear = date('y'); $century = $year - $twoDigitYear; if ($date['year'] <= $twoDigitYear) { // assume this date is from our century $date['year'] = $century + $date['year']; } else { // assume this date is from the previous century $date['year'] = $century - 100 + $date['year']; } } Z_Core::debug("DATE: retrieved with algorithms: " . json_encode($date)); $date['part'] = $m[1] . $m[7]; } else { // give up; we failed the sanity check Z_Core::debug("DATE: algorithms failed sanity check"); $date = array("part" => $string); } } else { //Zotero.debug("DATE: could not apply algorithms"); $date['part'] = $string; } // couldn't find something with the algorithms; use regexp // YEAR if (empty($date['year'])) { if (preg_match(self::$yearRE, $date['part'], $m)) { $date['year'] = $m[2]; $date['part'] = $m[1] . $m[3]; Z_Core::debug("DATE: got year (" . $date['year'] . ", " . $date['part'] . ")"); } } // MONTH if (empty($date['month'])) { // compile month regular expression $months = array('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'); // If using a non-English bibliography locale, try those too if (self::$locale != 'en-US') { throw new Exception("Unimplemented"); //$months = array_merge($months, .concat(Zotero.$date['month']s.short); } if (!self::$monthRE) { self::$monthRE = "/^(.*)\\b(" . implode("|", $months) . ")[^ ]*(?: (.*)\$|\$)/iu"; } if (preg_match(self::$monthRE, $date['part'], $m)) { // Modulo 12 in case we have multiple languages $date['month'] = array_search(ucwords(strtolower($m[2])), $months) % 12 + 1; $date['part'] = $m[1] . (isset($m[3]) ? $m[3] : ''); Z_Core::debug("DATE: got month (" . $date['month'] . ", " . $date['part'] . ")"); } } // DAY if (empty($date['day'])) { // compile day regular expression if (!self::$dayRE) { $daySuffixes = preg_replace("/, ?/", "|", self::getString("date.daySuffixes")); self::$dayRE = "/\\b([0-9]{1,2})(?:" . $daySuffixes . ")?\\b(.*)/iu"; } if (preg_match(self::$dayRE, $date['part'], $m, PREG_OFFSET_CAPTURE)) { $day = (int) $m[1][0]; // Sanity check if ($day <= 31) { $date['day'] = $day; if ($m[0][1] > 0) { $date['part'] = substr($date['part'], 0, $m[0][1]); if ($m[2][0]) { $date['part'] .= " " . $m[2][0]; } } else { $date['part'] = $m[2][0]; } Z_Core::debug("DATE: got day (" . $date['day'] . ", " . $date['part'] . ")"); } } } // clean up date part if ($date['part']) { $date['part'] = preg_replace(array("/^[^A-Za-z0-9]+/", "/[^A-Za-z0-9]+\$/"), "", $date['part']); } if ($date['part'] === "" || !isset($date['part'])) { unset($date['part']); } return $date; }
/** * Download file from S3, extract it if necessary, and return a temporary URL * pointing to the main file */ public static function getTemporaryURL(Zotero_Item $item, $localOnly = false) { $extURLPrefix = Z_CONFIG::$ATTACHMENT_SERVER_URL; if ($extURLPrefix[strlen($extURLPrefix) - 1] != "/") { $extURLPrefix .= "/"; } $info = Zotero_Storage::getLocalFileItemInfo($item); $storageFileID = $info['storageFileID']; $filename = $info['filename']; $mtime = $info['mtime']; $zip = $info['zip']; $realFilename = preg_replace("/^storage:/", "", $item->attachmentPath); $realFilename = self::decodeRelativeDescriptorString($realFilename); $realEncodedFilename = rawurlencode($realFilename); $docroot = Z_CONFIG::$ATTACHMENT_SERVER_DOCROOT; // Check memcached to see if file is already extracted $key = "attachmentServerString_" . $storageFileID . "_" . $mtime; if ($randomStr = Z_Core::$MC->get($key)) { Z_Core::debug("Got attachment path '{$randomStr}/{$realEncodedFilename}' from memcached"); return $extURLPrefix . "{$randomStr}/{$realEncodedFilename}"; } $localAddr = gethostbyname(gethostname()); // See if this is an attachment host $index = false; $skipHost = false; for ($i = 0, $len = sizeOf(Z_CONFIG::$ATTACHMENT_SERVER_HOSTS); $i < $len; $i++) { $hostAddr = gethostbyname(Z_CONFIG::$ATTACHMENT_SERVER_HOSTS[$i]); if ($hostAddr != $localAddr) { continue; } // Make a HEAD request on the local static port to make sure // this host is actually functional $url = "http://" . Z_CONFIG::$ATTACHMENT_SERVER_HOSTS[$i] . ":" . Z_CONFIG::$ATTACHMENT_SERVER_STATIC_PORT . "/"; Z_Core::debug("Making HEAD request to {$url}"); $ch = curl_init($url); curl_setopt($ch, CURLOPT_NOBODY, 1); curl_setopt($ch, CURLOPT_HTTPHEADER, array("Expect:")); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 1); curl_setopt($ch, CURLOPT_TIMEOUT, 2); curl_setopt($ch, CURLOPT_HEADER, 0); // do not return HTTP headers curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $response = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); if ($code != 200) { $skipHost = Z_CONFIG::$ATTACHMENT_SERVER_HOSTS[$i]; if ($code == 0) { Z_Core::logError("Error connecting to local attachments server"); } else { Z_Core::logError("Local attachments server returned {$code}"); } break; } $index = $i + 1; break; } // If not, make an internal root request to trigger the extraction on // one of them and retrieve the temporary URL if ($index === false) { // Prevent redirect madness if target server doesn't think it's an // attachment server if ($localOnly) { throw new Exception("Internal attachments request hit a non-attachment server"); } $prefix = 'http://' . Z_CONFIG::$API_SUPER_USERNAME . ":" . Z_CONFIG::$API_SUPER_PASSWORD . "@"; $path = Zotero_API::getItemURI($item) . "/file/view?int=1"; $path = preg_replace('/^[^:]+:\\/\\/[^\\/]+/', '', $path); $context = stream_context_create(array('http' => array('follow_location' => 0))); $url = false; $hosts = Z_CONFIG::$ATTACHMENT_SERVER_HOSTS; // Try in random order shuffle($hosts); foreach ($hosts as $host) { // Don't try the local host again if we know it's not working if ($host == $skipHost) { continue; } $intURL = $prefix . $host . ":" . Z_CONFIG::$ATTACHMENT_SERVER_DYNAMIC_PORT . $path; Z_Core::debug("Making GET request to {$host}"); if (file_get_contents($intURL, false, $context) !== false) { foreach ($http_response_header as $header) { if (preg_match('/^Location:\\s*(.+)$/', $header, $matches)) { if (strpos($matches[1], $extURLPrefix) !== 0) { throw new Exception("Redirect location '" . $matches[1] . "'" . " does not begin with {$extURLPrefix}"); } return $matches[1]; } } } } return false; } // If this is an attachment host, do the download/extraction inline // and generate a random number with an embedded host id. // // The reverse proxy routes incoming file requests to the proper hosts // using the embedded id. // // A cron job deletes old attachment directories $randomStr = rand(1000000, 2147483647); // Seventh number is the host id $randomStr = substr($randomStr, 0, 6) . $index . substr($randomStr, 6); // Download file $dir = $docroot . $randomStr . "/"; $downloadDir = $zip ? $dir . "ztmp/" : $dir; Z_Core::debug("Downloading attachment to {$dir}"); if (!mkdir($downloadDir, 0777, true)) { throw new Exception("Unable to create directory '{$downloadDir}'"); } if ($zip) { $response = Zotero_Storage::downloadFile($info, $downloadDir); } else { $response = Zotero_Storage::downloadFile($info, $downloadDir, $realFilename); } if ($response) { if ($zip) { $success = self::extractZip($downloadDir . $info['filename'], $dir); unlink($downloadDir . $info['filename']); rmdir($downloadDir); // Make sure charset is just a string with no spaces or newlines if (preg_match('/^[^\\s]+/', trim($item->attachmentCharset), $matches)) { $charset = $matches[0]; } else { $charset = 'Off'; } file_put_contents($dir . ".htaccess", "AddDefaultCharset " . $charset); } else { $success = true; if (preg_match('/^[^\\s]+/', trim($item->attachmentContentType), $matches)) { $contentType = $matches[0]; $charset = trim($item->attachmentCharset); if (substr($charset, 0, 5) == 'text/' && preg_match('/^[^\\s]+/', $charset, $matches)) { $contentType .= '; ' . $matches[0]; } file_put_contents($dir . ".htaccess", "ForceType " . $contentType); } } } if (!$response || !$success) { return false; } Z_Core::$MC->set($key, $randomStr, self::$cacheTime); return $extURLPrefix . "{$randomStr}/" . $realEncodedFilename; }
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'); }
public function rollback() { if (!$this->queuing) { Z_Core::debug('Transaction not open in Z_MemcachedClientLocal::rollback()'); return; } if (!$this->queue) { return; } $this->queuing = false; $this->queue = array(); $this->queueKeyPos = array(); $this->queueValues = array(); }
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; }
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']); } }
public function save() { if (!$this->libraryID) { trigger_error("Library ID must be set before saving", E_USER_ERROR); } Zotero_DB::beginTransaction(); try { $shardID = Zotero_Shards::getByLibraryID($this->libraryID); $relationID = $this->id ? $this->id : Zotero_ID::get('relations'); Z_Core::debug("Saving relation {$relationID}"); $sql = "INSERT INTO relations\n\t\t\t\t\t(relationID, libraryID, subject, predicate, object, serverDateModified)\n\t\t\t\t\tVALUES (?, ?, ?, ?, ?, ?)"; $timestamp = Zotero_DB::getTransactionTimestamp(); $params = array($relationID, $this->libraryID, $this->subject, $this->predicate, $this->object, $timestamp); $insertID = Zotero_DB::query($sql, $params, $shardID); if (!$this->id) { if (!$insertID) { throw new Exception("Relation id not available after INSERT"); } $this->id = $insertID; } // Remove from delete log if it's there $sql = "DELETE FROM syncDeleteLogKeys WHERE libraryID=? AND objectType='relation' AND `key`=?"; Zotero_DB::query($sql, array($this->libraryID, $this->getKey()), $shardID); Zotero_DB::commit(); } catch (Exception $e) { Zotero_DB::rollback(); throw $e; } return $this->id; }
private function loadChildItems() { Z_Core::debug("Loading child items for collection {$this->id}"); if ($this->childItemsLoaded) { trigger_error("Child items for collection {$this->id} already loaded", E_USER_ERROR); } 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 : array(); $this->childItemsLoaded = true; }
public function updateConditions($conditions) { $this->loadPrimaryData(); $this->loadConditions(); for ($i = 1, $len = sizeOf($conditions); $i <= $len; $i++) { // Compare existing values to new values if (isset($this->conditions[$i])) { if ($this->conditions[$i]['condition'] == $conditions[$i - 1]['condition'] && $this->conditions[$i]['mode'] == $conditions[$i - 1]['mode'] && $this->conditions[$i]['operator'] == $conditions[$i - 1]['operator'] && $this->conditions[$i]['value'] == $conditions[$i - 1]['value']) { continue; } } $this->changed['conditions'] = true; } if (!empty($this->changed['conditions']) || sizeOf($this->conditions) > $conditions) { $this->conditions = $conditions; } else { Z_Core::debug("Conditions have not changed for search {$this->id}"); } }
public function hasChanged() { $changed = array_filter(array_keys($this->changed), function ($dataType) { return $this->changed[$dataType]; }); foreach ($changed as $dataType) { if ($dataType == 'primaryData' && is_array($this->changed['primaryData'])) { foreach ($this->changed['primaryData'] as $field => $val) { Z_Core::debug("{$field} has changed for item {$this->libraryKey}"); } } else { Z_Core::debug("{$dataType} has changed for item {$this->libraryKey}"); } } return !!$changed; }
public function save() { if (!$this->loaded) { Z_Core::debug("Not saving unloaded key {$this->id}"); return; } if (!$this->userID) { throw new Exception("Cannot save key without userID"); } if (!$this->name) { throw new Exception("Cannot save key without name"); } if (strlen($this->name) > 255) { throw new Exception("Key name too long", Z_ERROR_KEY_NAME_TOO_LONG); } Zotero_DB::beginTransaction(); if (!$this->key) { $isNew = true; $this->key = Zotero_Keys::generate(); } else { $isNew = false; } $fields = array('key', 'userID', 'name'); $sql = "INSERT INTO `keys` (keyID, `key`, userID, name) VALUES (?, ?, ?, ?)"; $params = array($this->id); foreach ($fields as $field) { $params[] = $this->{$field}; } $sql .= " ON DUPLICATE KEY UPDATE "; $q = array(); foreach ($fields as $field) { $q[] = "`{$field}`=?"; $params[] = $this->{$field}; } $sql .= implode(", ", $q); $insertID = Zotero_DB::query($sql, $params); if (!$this->id) { if (!$insertID) { throw new Exception("Key id not available after INSERT"); } $this->id = $insertID; } if (!$insertID) { $sql = "SELECT * FROM keyPermissions WHERE keyID=?"; $oldRows = Zotero_DB::query($sql, $this->id); } $oldPermissions = []; $newPermissions = []; $librariesToAdd = []; $librariesToRemove = []; // Massage rows into permissions format if (!$isNew && isset($oldRows)) { foreach ($oldRows as $row) { $oldPermissions[$row['libraryID']][$row['permission']] = !!$row['granted']; } } // Delete existing permissions $sql = "DELETE FROM keyPermissions WHERE keyID=?"; Zotero_DB::query($sql, $this->id); if (isset($this->changed['permissions'])) { foreach ($this->changed['permissions'] as $libraryID => $p) { foreach ($p as $permission => $changed) { $enabled = $this->permissions[$libraryID][$permission]; if (!$enabled) { continue; } $sql = "INSERT INTO keyPermissions VALUES (?, ?, ?, ?)"; // TODO: support negative permissions Zotero_DB::query($sql, array($this->id, $libraryID, $permission, 1)); $newPermissions[$libraryID][$permission] = true; } } } $this->permissions = $newPermissions; // Send notifications for added and removed API key – library pairs if (!$isNew) { $librariesToAdd = $this->permissionsDiff($oldPermissions, $newPermissions, $this->userID); $librariesToRemove = $this->permissionsDiff($newPermissions, $oldPermissions, $this->userID); if ($librariesToAdd) { Zotero_Notifier::trigger('add', 'apikey-library', array_map(function ($libraryID) { return $this->key . "-" . $libraryID; }, array_unique($librariesToAdd))); } if ($librariesToRemove) { Zotero_Notifier::trigger('remove', 'apikey-library', array_map(function ($libraryID) { return $this->key . "-" . $libraryID; }, array_unique($librariesToRemove))); } } Zotero_DB::commit(); $this->load(); return $this->id; }
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'); }
private function load() { $libraryID = $this->libraryID; $name = $this->name; Z_Core::debug("Loading data for setting {$libraryID}/{$name}"); if (!$libraryID) { throw new Exception("Library ID not set"); } if (!$name) { throw new Exception("Name not set"); } $row = Zotero_Settings::getPrimaryDataByKey($libraryID, $name); $this->loaded = true; if (!$row) { return; } foreach ($row as $key => $val) { if ($key == 'value') { $val = json_decode($val); } $this->{$key} = $val; } }
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 static function reset() { Z_Core::debug("Resetting Notifier event queue"); self::$locked = false; self::$queue = array(); self::$inTransaction = false; }
public function updateCondition($searchConditionID, $condition, $mode, $operator, $value, $required) { if ($this->id && !$this->loaded) { $this->load(false); } if (!isset($this->conditions[$searchConditionID])) { trigger_error("Invalid searchConditionID {$searchConditionID}", E_USER_ERROR); } /* if (!Zotero_SearchConditions::hasOperator($condition, $operator)) { trigger_error("Invalid operator $operator", E_USER_ERROR); } */ $existingCondition = $this->conditions[$searchConditionID]; if ($existingCondition['condition'] == $condition && $existingCondition['mode'] == $mode && $existingCondition['operator'] == $operator && $existingCondition['value'] == $value && $existingCondition['required'] == $required) { Z_Core::debug("Condition {$searchConditionID} for search\n\t\t\t\t{$this->id} has not changed"); return; } $this->conditions[$searchConditionID] = array('id' => $searchConditionID, 'condition' => $condition, 'mode' => $mode, 'operator' => $operator, 'value' => $value, 'required' => $required); $this->changed = true; //$this->sql = null; //$this->sqlParams = null; }
*/ $params = (require 'config/routes.inc.php'); if (!$params || !isset($params['controller']) || $params['controller'] == 404) { header("HTTP/1.0 404 Not Found"); include 'errors/404.php'; return; } // Parse variables from router $controllerName = ucwords($params['controller']); $action = !empty($params['action']) ? $params['action'] : lcfirst($controllerName); $directory = !empty($params['directory']) ? $params['directory'] . '/' : ""; $extra = !empty($params['extra']) ? $params['extra'] : array(); // Attempt to load controller $controllerFile = Z_ENV_CONTROLLER_PATH . $directory . $controllerName . 'Controller.php'; Z_Core::debug($_SERVER['REQUEST_METHOD'] . " to " . Z_ENV_SELF); Z_Core::debug("Controller is {$controllerFile}"); if (file_exists($controllerFile)) { require 'mvc/Controller.inc.php'; require $controllerFile; $controllerClass = $controllerName . 'Controller'; $controller = new $controllerClass($controllerName, $action, $params); $controller->init($extra); if (method_exists($controllerClass, $action)) { call_user_func(array($controller, $action)); Z_Core::exitClean(); } else { throw new Exception("Action '{$action}' not found in {$controllerFile}"); } } // If controller not found, load error document header("HTTP/1.0 404 Not Found");
private static function send($message) { $message = json_encode($message, JSON_UNESCAPED_SLASHES); Z_Core::debug("Sending notification: " . $message); foreach (self::$messageReceivers as $receiver) { $receiver(Z_CONFIG::$SNS_TOPIC_STREAM_EVENTS, $message); } }
/** * Store item in internal id-based cache */ public static function cache(Zotero_Item $item) { if (isset(self::$itemsByID[$item->id])) { Z_Core::debug("Item {$item->id} is already cached"); } self::$itemsByID[$item->id] = $item; }
private function setRelatedItems($itemIDs) { if (!$this->loaded['relatedItems']) { $this->loadRelatedItems(); } if (!is_array($itemIDs)) { trigger_error('$itemIDs must be an array', E_USER_ERROR); } $currentIDs = $this->relatedItems; if (!$currentIDs) { $currentIDs = array(); } $oldIDs = array(); // children being kept $newIDs = array(); // new children if (!$itemIDs) { if (!$currentIDs) { Z_Core::debug("No related items added", 4); return false; } } else { /* // Don't bother with this because the DB trigger takes care of it $found = Zotero_Items::get($this->libraryID, $itemIDs); if (sizeOf($found) != sizeOf($itemIDs)) { throw new Exception("Related item(s) not found (" . sizeOf($found) . " != " . sizeOf($itemIDs) . ")"); } */ foreach ($itemIDs as $itemID) { if ($itemID == $this->id) { Z_Core::debug("Can't relate item to itself in Zotero.Item.setRelatedItems()", 2); continue; } if (in_array($itemID, $currentIDs)) { Z_Core::debug("Item {$this->id} is already related to item {$itemID}"); $oldIDs[] = $itemID; continue; } // TODO: check if related on other side (like client)? $newIDs[] = $itemID; } } // Mark as changed if new or removed ids if ($newIDs || sizeOf($oldIDs) != sizeOf($currentIDs)) { if (!$this->changed['relatedItems']) { $this->storePreviousData('relatedItems'); $this->changed['relatedItems'] = true; } } else { Z_Core::debug('Related items not changed', 4); return false; } $this->relatedItems = array_merge($oldIDs, $newIDs); return true; }
protected static function logQuery($sql, $params, $shardID) { Z_Core::debug($sql . ($params ? " (" . (is_scalar($params) ? $params : implode(",", $params)) . ") ({$shardID})" : "")); }
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; }
public function erase() { if (!$this->loaded) { Z_Core::debug("Not deleting unloaded group {$this->id}"); return; } Zotero_DB::beginTransaction(); $userIDs = self::getUsers(); $this->logGroupLibraryRemoval(); Zotero_Libraries::deleteCachedData($this->libraryID); Zotero_Libraries::clearAllData($this->libraryID); $sql = "DELETE FROM shardLibraries WHERE libraryID=?"; $deleted = Zotero_DB::query($sql, $this->libraryID, Zotero_Shards::getByLibraryID($this->libraryID)); if (!$deleted) { throw new Exception("Group not deleted"); } $sql = "DELETE FROM libraries WHERE libraryID=?"; $deleted = Zotero_DB::query($sql, $this->libraryID); if (!$deleted) { throw new Exception("Group not deleted"); } // Delete key permissions for this library, and then delete any keys // that had no other permissions $sql = "SELECT keyID FROM keyPermissions WHERE libraryID=?"; $keyIDs = Zotero_DB::columnQuery($sql, $this->libraryID); if ($keyIDs) { $sql = "DELETE FROM keyPermissions WHERE libraryID=?"; Zotero_DB::query($sql, $this->libraryID); $sql = "DELETE K FROM `keys` K LEFT JOIN keyPermissions KP USING (keyID)\n\t\t\t\t\tWHERE keyID IN (" . implode(', ', array_fill(0, sizeOf($keyIDs), '?')) . ") AND KP.keyID IS NULL"; Zotero_DB::query($sql, $keyIDs); } // If group is locked by a sync, flag group for a timestamp update // once the sync is done so that the uploading user gets the change try { foreach ($userIDs as $userID) { if ($syncUploadQueueID = Zotero_Sync::getUploadQueueIDByUserID($userID)) { Zotero_Sync::postWriteLog($syncUploadQueueID, 'group', $this->id, 'delete'); } } } catch (Exception $e) { Z_Core::logError($e); } Zotero_Notifier::trigger('delete', 'library', $this->libraryID); Zotero_DB::commit(); $this->erased = true; }
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]; } Z_Core::$AWS = new Aws\Sdk($awsConfig); unset($awsConfig); // Elastica Z_Core::$Elastica = new \Elastica\Client(array('connections' => array_map(function ($hostAndPort) { preg_match('/^([^:]+)(:[0-9]+)?$/', $hostAndPort, $matches); return ['host' => $matches[1], 'port' => isset($matches[2]) ? $matches[2] : 9200]; }, Z_CONFIG::$SEARCH_HOSTS))); require 'interfaces/IAuthenticationPlugin.inc.php'; require 'log.inc.php'; Z_Core::$debug = !empty(Z_CONFIG::$DEBUG_LOG); // Load in functions require 'functions/string.inc.php'; require 'functions/array.inc.php';
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; }
/** * Converts a Zotero_Item object to a SimpleXMLElement item * * @param object $item Zotero_Item object * @param array $data * @return SimpleXMLElement Item data as SimpleXML element */ public static function convertItemToXML(Zotero_Item $item, $data = array()) { $t = microtime(true); // Check cache for all items except imported attachments, // which don't have their versions updated when the client // updates their file metadata if (!$item->isImportedAttachment()) { $cacheVersion = 1; $cacheKey = "syncXMLItem_" . $item->libraryID . "/" . $item->id . "_" . $item->version . "_" . md5(json_encode($data)) . "_" . $cacheVersion . (isset(Z_CONFIG::$CACHE_VERSION_SYNC_XML_ITEM) ? "_" . Z_CONFIG::$CACHE_VERSION_SYNC_XML_ITEM : ""); $xmlstr = Z_Core::$MC->get($cacheKey); } else { $cacheKey = false; $xmlstr = false; } if ($xmlstr) { $xml = new SimpleXMLElement($xmlstr); StatsD::timing("api.items.itemToSyncXML.cached", (microtime(true) - $t) * 1000); StatsD::increment("memcached.items.itemToSyncXML.hit"); // Skip the cache every 10 times for now, to ensure cache sanity if (Z_Core::probability(10)) { //$xmlstr = $xml->saveXML(); } else { Z_Core::debug("Using cached sync XML item"); return $xml; } } $xml = new SimpleXMLElement('<item/>'); // Primary fields foreach (self::$primaryFields as $field) { switch ($field) { case 'id': case 'serverDateModified': case 'version': continue 2; case 'itemTypeID': $xmlField = 'itemType'; $xmlValue = Zotero_ItemTypes::getName($item->{$field}); break; default: $xmlField = $field; $xmlValue = $item->{$field}; } $xml[$xmlField] = $xmlValue; } // Item data $itemTypeID = $item->itemTypeID; $fieldIDs = $item->getUsedFields(); foreach ($fieldIDs as $fieldID) { $val = $item->getField($fieldID); if ($val == '') { continue; } $f = $xml->addChild('field', htmlspecialchars($val)); $fieldName = Zotero_ItemFields::getName($fieldID); // Special handling for renamed computerProgram 'version' field if ($itemTypeID == 32 && $fieldName == 'versionNumber') { $fieldName = 'version'; } $f['name'] = htmlspecialchars($fieldName); } // Deleted item flag if ($item->deleted) { $xml['deleted'] = '1'; } if ($item->isNote() || $item->isAttachment()) { $sourceItemID = $item->getSource(); if ($sourceItemID) { $sourceItem = Zotero_Items::get($item->libraryID, $sourceItemID); if (!$sourceItem) { throw new Exception("Source item {$sourceItemID} not found"); } $xml['sourceItem'] = $sourceItem->key; } } // Group modification info $createdByUserID = null; $lastModifiedByUserID = null; switch (Zotero_Libraries::getType($item->libraryID)) { case 'group': $createdByUserID = $item->createdByUserID; $lastModifiedByUserID = $item->lastModifiedByUserID; break; } if ($createdByUserID) { $xml['createdByUserID'] = $createdByUserID; } if ($lastModifiedByUserID) { $xml['lastModifiedByUserID'] = $lastModifiedByUserID; } if ($item->isAttachment()) { $xml['linkMode'] = $item->attachmentLinkMode; $xml['mimeType'] = $item->attachmentMIMEType; if ($item->attachmentCharset) { $xml['charset'] = $item->attachmentCharset; } $storageModTime = $item->attachmentStorageModTime; if ($storageModTime) { $xml['storageModTime'] = $storageModTime; } $storageHash = $item->attachmentStorageHash; if ($storageHash) { $xml['storageHash'] = $storageHash; } // TODO: get from a constant if ($item->attachmentLinkMode != 3) { $xml->addChild('path', htmlspecialchars($item->attachmentPath)); } } // Note if ($item->isNote() || $item->isAttachment()) { // Get htmlspecialchars'ed note $note = $item->getNote(false, true); if ($note !== '') { $xml->addChild('note', $note); } else { if ($item->isNote()) { $xml->addChild('note', ''); } } } // Creators $creators = $item->getCreators(); if ($creators) { foreach ($creators as $index => $creator) { $c = $xml->addChild('creator'); $c['key'] = $creator['ref']->key; $c['creatorType'] = htmlspecialchars(Zotero_CreatorTypes::getName($creator['creatorTypeID'])); $c['index'] = $index; if (empty($data['updatedCreators']) || !in_array($creator['ref']->id, $data['updatedCreators'])) { $cNode = dom_import_simplexml($c); $creatorXML = Zotero_Creators::convertCreatorToXML($creator['ref'], $cNode->ownerDocument); $cNode->appendChild($creatorXML); } } } // Related items $relatedKeys = $item->relatedItems; $keys = array(); foreach ($relatedKeys as $relatedKey) { if (Zotero_Items::getByLibraryAndKey($item->libraryID, $relatedKey)) { $keys[] = $relatedKey; } } if ($keys) { $xml->related = implode(' ', $keys); } if ($xmlstr) { $uncached = $xml->saveXML(); if ($xmlstr != $uncached) { error_log("Cached sync XML item does not match"); error_log(" Cached: " . $xmlstr); error_log("Uncached: " . $uncached); } } else { $xmlstr = $xml->saveXML(); if ($cacheKey) { Z_Core::$MC->set($cacheKey, $xmlstr, 3600); // 1 hour for now } StatsD::timing("api.items.itemToSyncXML.uncached", (microtime(true) - $t) * 1000); StatsD::increment("memcached.items.itemToSyncXML.miss"); } return $xml; }
public function save() { if (!$this->loaded) { Z_Core::debug("Not saving unloaded key {$this->id}"); return; } if (!$this->userID) { throw new Exception("Cannot save key without userID"); } if (!$this->name) { throw new Exception("Cannot save key without name"); } if (strlen($this->name) > 255) { throw new Exception("Key name too long", Z_ERROR_KEY_NAME_TOO_LONG); } Zotero_DB::beginTransaction(); if (!$this->key) { $this->key = Zotero_Keys::generate(); } $fields = array('key', 'userID', 'name'); $sql = "INSERT INTO `keys` (keyID, `key`, userID, name) VALUES (?, ?, ?, ?)"; $params = array($this->id); foreach ($fields as $field) { $params[] = $this->{$field}; } $sql .= " ON DUPLICATE KEY UPDATE "; $q = array(); foreach ($fields as $field) { $q[] = "`{$field}`=?"; $params[] = $this->{$field}; } $sql .= implode(", ", $q); $insertID = Zotero_DB::query($sql, $params); if (!$this->id) { if (!$insertID) { throw new Exception("Key id not available after INSERT"); } $this->id = $insertID; } // Delete existing permissions $sql = "DELETE FROM keyPermissions WHERE keyID=?"; Zotero_DB::query($sql, $this->id); if (isset($this->changed['permissions'])) { foreach ($this->changed['permissions'] as $libraryID => $p) { foreach ($p as $permission => $changed) { $enabled = $this->permissions[$libraryID][$permission]; if (!$enabled) { continue; } $sql = "INSERT INTO keyPermissions VALUES (?, ?, ?, ?)"; // TODO: support negative permissions Zotero_DB::query($sql, array($this->id, $libraryID, $permission, 1)); } } } Zotero_DB::commit(); $this->load(); return $this->id; }