urlParser = $urlParser; $this->inputData = $inputData; } /** * @brief Run storage service. * * @return True on successful command parsing, false otherwise. */ public function run() { // Check if given url is valid if (!$this->urlParser->isValid()) { Utils::changeHttpStatus(Utils::STATUS_INVALID_DATA); Utils::writeLog("URL: Invalid URL."); return false; } // Get Mozilla Sync user hash and authenticate user $syncHash = $this->urlParser->getSyncHash(); if (User::authenticateUser($syncHash) === false) { Utils::changeHttpStatus(Utils::STATUS_INVALID_USER); Utils::writeLog("Could not authenticate user " . $syncHash . "."); return false; } // Convert Sync hash to Sync ID $syncId = User::syncHashToSyncId($syncHash); if ($syncId === false) { Utils::changeHttpStatus(Utils::STATUS_INVALID_USER); Utils::writeLog("Could not convert user " . $syncHash . " to Sync ID."); return false; } // Delete old WBO on every run of storage service Storage::deleteOldWbo(); // Map request to functions // Info case: https://server/pathname/version/username/info/ if (($this->urlParser->commandCount() === 2) && ($this->urlParser->getCommand(0) === 'info')) { if (Utils::getRequestMethod() != 'GET') { Utils::changeHttpStatus(Utils::STATUS_NOT_FOUND); Utils::writeLog("URL: Invalid HTTP method " . Utils::getRequestMethod() . " for info."); return false; } switch ($this->urlParser->getCommand(1)) { case 'collections': $this->getInfoCollections($syncId); break; case 'collection_usage': $this->getInfoCollectionUsage($syncId); break; case 'collection_counts': $this->getInfoCollectionCounts($syncId); break; case 'quota': $this->getInfoQuota($syncId); break; default: Utils::changeHttpStatus(Utils::STATUS_NOT_FOUND); Utils::writeLog("URL: Invalid command " . $this->urlParser->getCommand(1) . " for info."); return false; } } // Storage case: https://server/pathname/version/username/storage/ else if (($this->urlParser->commandCount() === 1) && ($this->urlParser->getCommand(0) === 'storage')) { switch (Utils::getRequestMethod()) { case 'DELETE': $this->deleteStorage($syncId); break; default: Utils::changeHttpStatus(Utils::STATUS_NOT_FOUND); Utils::writeLog("URL: Invalid request method " . Utils::getRequestMethod() . " for storage."); return false; } } // Collection case: https://server/pathname/version/username/storage/collection else if (($this->urlParser->commandCount() === 2) && ($this->urlParser->getCommand(0) === 'storage')) { $collectionName = $this->urlParser->getCommand(1); $modifiers = $this->urlParser->getCommandModifiers(); $collectionId = Storage::collectionNameToIndex($syncId, $collectionName); switch (Utils::getRequestMethod()) { case 'GET': $this->getCollection($syncId, $collectionId, $modifiers); break; case 'POST': $this->postCollection($syncId, $collectionId); break; case 'DELETE': $this->deleteCollection($syncId, $collectionId, $modifiers); break; default: Utils::changeHttpStatus(Utils::STATUS_NOT_FOUND); Utils::writeLog("URL: Invalid request method" . Utils::getRequestMethod() . " for collection."); return false; } } // WBO case: https://server/pathname/version/username/storage/collection/id else if (($this->urlParser->commandCount() === 3) && ($this->urlParser->getCommand(0) === 'storage')) { $collectionName = $this->urlParser->getCommand(1); $wboId = $this->urlParser->getCommand(2); $collectionId = Storage::collectionNameToIndex($syncId, $collectionName); switch (Utils::getRequestMethod()) { case 'GET': $this->getWBO($syncId, $collectionId, $wboId); break; case 'PUT': $this->putWBO($syncId, $collectionId, $wboId); break; case 'DELETE': $this->deleteWBO($syncId, $collectionId, $wboId); break; default: Utils::changeHttpStatus(Utils::STATUS_NOT_FOUND); Utils::writeLog("URL: Invalid request method" . Utils::getRequestMethod() . " for WBO."); return false; } } // Invalid request else { Utils::changeHttpStatus(Utils::STATUS_NOT_FOUND); Utils::writeLog("URL: Invalid storage service request. Sent " . ((string) $this->urlParser->commandCount()) . " commands in URL " . Utils::getSyncUrl() . ": " . var_export($this->urlParser->getCommands(), true)); return false; } return true; } /** * @brief Returns a hash of collections associated with the account, along with the last modified timestamp for each collection. * * HTTP request: GET https://server/pathname/version/username/info/collections * * Example: * * HTTP/1.0 200 OK * Server: PasteWSGIServer/0.5 Python/2.6.6 * Date: Sun, 25 Mar 2012 16:29:21 GMT * Content-Type: application/json * Content-Length: 227 * X-Weave-Records: 9 * X-Weave-Timestamp: 1332692961.71 * * {"passwords": 1332607246.46, "tabs": 1332607246.93, "clients": 1332607162.28, * "crypto": 1332607162.21, "forms": 1332607170.80, "meta": 1332607246.96, * "bookmarks": 1332607162.45, "prefs": 1332607246.72, "history": 1332607245.16} * * @param integer $syncId The Sync ID whose info/collections will be * retrieved. * @return bool True on success, false otherwise. */ private function getInfoCollections($syncId) { // Get collections with last modification times $resultArray = Storage::getCollectionModifiedTimes($syncId); if ($resultArray === false) { Utils::writeLog("DB: Could not get info collections for user " . $syncId . "."); return false; } else { OutputData::write($resultArray); return true; } } /** * @brief Returns a hash of collections associated with the account, along * with the data volume used for each (in kB). * * HTTP request: GET https://server/pathname/version/username/info/collection_usage * * Example: * * HTTP/1.0 200 OK * Server: PasteWSGIServer/0.5 Python/2.6.6 * Date: Sun, 25 Mar 2012 16:29:21 GMT * Content-Type: application/json * Content-Length: 227 * X-Weave-Records: 9 * X-Weave-Timestamp: 1332692961.71 * * {"passwords": 258.134, "tabs": 25.258, "clients": 1.525, * "crypto": 0.347, "forms": 119.666, "meta": 0.343, * "bookmarks": 267.791, "prefs": 15.642, "history": 2577.264} * * @param integer $syncId The Sync ID whose info/collection_usage will be * retrieved. * @return bool True on success, false otherwise. */ private function getInfoCollectionUsage($syncId) { // Get collection with sizes $resultArray = Storage::getCollectionSizes($syncId); if ($resultArray === false) { Utils::writeLog("DB: Could not get info collection usage for user " . $syncId . "."); return false; } else { OutputData::write($resultArray); return true; } } /** * @brief Returns a hash of collections associated with the account, along with the total number of items in each collection. * * HTTP request: GET https://server/pathname/version/username/info/collection_counts * * Example: * * HTTP/1.0 200 OK * Server: PasteWSGIServer/0.5 Python/2.6.6 * Date: Sun, 25 Mar 2012 16:29:21 GMT * Content-Type: application/json * Content-Length: 227 * X-Weave-Records: 9 * X-Weave-Timestamp: 1332692961.71 * * {"passwords": 574, "tabs": 2, "clients": 4, * "crypto": 1, "forms": 502, "meta": 1, * "bookmarks": 485, "prefs": 85, "history": 5163} * * @param integer $syncId The Sync ID whose info/collection_counts will be * retrieved. * @return bool True on success, false otherwise. */ private function getInfoCollectionCounts($syncId) { $query = \OCP\DB::prepare('SELECT `name`, (SELECT COUNT(`payload`) FROM `*PREFIX*mozilla_sync_wbo` WHERE `*PREFIX*mozilla_sync_wbo`.`collectionid` = `*PREFIX*mozilla_sync_collections`.`id`) as `counts` FROM `*PREFIX*mozilla_sync_collections` WHERE `userid` = ?'); $result = $query->execute(array($syncId)); if ($result === false) { Utils::writeLogDbError("DB: Could not get info collection counts for user " . $syncId . ".", $query); return false; } $resultArray = array(); while (($row = $result->fetchRow())) { // Skip empty collections if (is_null($row['counts'])) { continue; } $key = $row['name']; $value = $row['counts']; $resultArray[$key] = $value; } OutputData::write($resultArray); return true; } /** * @brief Returns a list containing the user's current usage and quota (in kB). The second value will be null if no quota is defined. * * HTTP request: GET https://server/pathname/version/username/info/quota * * Example: * * HTTP/1.0 200 OK * Server: PasteWSGIServer/0.5 Python/2.6.6 * Date: Sun, 25 Mar 2012 16:29:21 GMT * Content-Type: application/json * Content-Length: 227 * X-Weave-Records: 9 * X-Weave-Timestamp: 1332692961.71 * * { 574, null } * * @param integer $syncId The Sync ID whose info/quota will be fetched. * @return bool True on success, false otherwise. */ private function getInfoQuota($syncId) { $size = User::getUserUsage($syncId); $limit = User::getQuota(); if ($limit === 0) { $limit = null; } OutputData::write(array($size, $limit)); return true; } /** * @brief Checks if user has free space according his usage and the qouta. * * It is possible to restrict the quota of Mozilla Sync to a limit. A zero * limit results in no restriction. The value is zero by default but can be * set on the admin page. * * @param integer $syncId The Sync ID whose quota will be checked. * @return boolean True if the user is below the quota, false otherwise. */ private function checkUserQuota($syncId, $size=0) { $quota = User::getQuota(); $usage = User::getUserUsage($syncId); if ($quota != 0 && ($usage + $size) >= $quota) { Utils::writeLog("User " . $syncId . " reached the sync quota: usage " . $usage. ", size of additional data " . $size . ", quota " . $quota . "."); Utils::sendError(Utils::STATUS_INVALID_DATA, 14); return false; } return true; } /** * @brief Returns a list of the WBO IDs contained in a collection. * * HTTP request: GET https://server/pathname/version/username/storage/collection * * This request has additional optional parameters: * * ids: returns the ids for objects in the collection that are in the provided comma-separated list. * * full: if defined, returns the full WBO, rather than just the id. * * * predecessorid: returns the ids for objects in the collection that are directly preceded by the id given. * Usually only returns one result. * * parentid: returns the ids for objects in the collection that are the children of the parent id given. * * * older: returns only ids for objects in the collection that have been last modified before the date given. * * newer: returns only ids for objects in the collection that have been last modified since the date given. * * index_above: if defined, only returns items with a higher sortindex than the value specified. * * index_below: if defined, only returns items with a lower sortindex than the value specified. * * * limit: sets the maximum number of ids that will be returned. * * offset: skips the first n ids. For use with the limit parameter (required) to paginate through a result set. * * sort: sorts the output. * ‘oldest’ - Orders by modification date (oldest first) * ‘newest’ - Orders by modification date (newest first) * ‘index’ - Orders by the sortindex descending (highest weight first) * * WARNING!! * * In full record mode, data are send in separate arrays, for example: * {"id":"test1","modified":1234} * {"id":"test2","modified":12345} * * In id only mode, identificators are send in one array, for example: * ["qqweeqw","testid","nexttestid"] * * @param integer $syncId The Sync ID whose collection will be fetched. * @param integer $collectionId The ID of the collection to be fetched. * @param array $modifiers Modifiers for the fetching (see above). * @return bool True on success, false otherwise. */ private function getCollection($syncId, $collectionId, &$modifiers) { $queryArgs = array(); // Full or ID modifier $queryFields = ''; if (isset($modifiers['full'])) { $queryFields = '`payload`, `name` AS `id`, `modified`, `sortindex`'; } else { $queryFields = '`name` AS `id`'; } $whereString = 'WHERE `collectionid` = ?'; array_push($queryArgs, $collectionId); // Convert the modifiers to the WHERE string $whereString .= Storage::modifiersToString($modifiers, $queryArgs, $limit, $offset); $query = \OCP\DB::prepare('SELECT ' . $queryFields . ' FROM `*PREFIX*mozilla_sync_wbo` ' . $whereString, $limit, $offset); $result = $query->execute($queryArgs); if ($result === false) { Utils::writeLogDbError("DB: Could not get collection " . $collectionId . " for user " . $syncId . ".", $query); return false; } // Results are sent in an array $resultArray = array(); while (($row = $result->fetchRow())) { if (isset($modifiers['full'])) { // Cast returned values to the correct type $resultArray[] = self::forceTypeCasting($row); } else { $resultArray[] = $row['id']; } } // Set number of elements in header header('X-Weave-Records: ' . count($resultArray)); OutputData::write($resultArray); return true; } /** * @brief Save array of WBO. * * HTTP request: POST https://server/pathname/version/username/storage/collection * * Takes an array of WBOs in the request body and iterates over them, * effectively doing a series of atomic PUTs with the same timestamp. * * example response: * {"failed": {}, "modified": 1341650217.16, "success": ["VQYhVASVcpVI"]} * * @param integer $syncId The Sync user whose WBO will be saved. * @param integer $collectionId The collection this WBO belongs to. * @return bool True on success, false otherwise. */ private function postCollection($syncId, $collectionId) { // Get and verify input data $inputData = $this->getInputData(); if ((!$inputData->isValid()) && (count($inputData->getInputArray()) > 0)) { Utils::changeHttpStatus(Utils::STATUS_INVALID_DATA); Utils::writeLog("URL: Invalid data for posting collection " . $collectionId . " for user " . $syncId . "."); return false; } // Check if user has free space available $size = ((float) strlen(serialize($inputData))/1024.0); // approximate the input data size if(!$this->checkUserQuota($syncId, $size)) { return false; } // Get current time to be stored in DB and returned as header $modifiedTime = Utils::getMozillaTimestamp(); $resultArray["modified"] = $modifiedTime; $successArray = array(); $failedArray = array(); // Iterate through input array and store all WBO in the database for ($i = 0; $i < count($inputData->getInputArray()); $i++) { $result = Storage::saveWBO($syncId, $modifiedTime, $collectionId, $inputData[$i]); if ($result === true) { $successArray[] = $inputData[$i]['id']; } else { $failedArray[] = $inputData[$i]['id']; Utils::writeLog("DB: Failed to post collection " . $collectionId . " for user " . $syncId . ".", \OCP\Util::WARN); } } $resultArray["success"] = $successArray; // The failed field is a hash containing arrays $resultArray["failed"] = (object) $failedArray; // Return modification time in X-Weave-Timestamp header OutputData::write($resultArray, $modifiedTime); return true; } /** * @brief Deletes the collection and all contents. * * HTTP request: DELETE https://server/pathname/version/username/storage/collection * * Additional request parameters may modify the selection of which items to delete @see getCollection * * @param integer $syncId The Sync user whose collection will be deleted. * @param integer $collectionId The ID of the collection to be deleted. * @param array $modifiers Modifiers specifying the collection. * @return bool True on success, false otherwise. */ private function deleteCollection($syncId, $collectionId, &$modifiers) { $queryArgs = array(); $whereString = 'WHERE `collectionid` = ?'; array_push($queryArgs, $collectionId); $whereString .= Storage::modifiersToString($modifiers, $queryArgs, $limit, $offset); // Delete all WBO of a collection $query = \OCP\DB::prepare('DELETE FROM `*PREFIX*mozilla_sync_wbo` ' . $whereString, $limit, $offset); $result = $query->execute($queryArgs); if ($result === false) { Utils::writeLogDbError("DB: Failed to delete WBO for collection " . $collectionId . " for user " . $syncId . ".", $query); return false; } // Check if no new WBO was added in the meantime $query = \OCP\DB::prepare('SELECT 1 FROM `*PREFIX*mozilla_sync_wbo` WHERE `collectionid` = ?'); $result = $query->execute(array($collectionId)); // No WBO found, delete entire collection if ($result->fetchRow() === false) { $query = \OCP\DB::prepare('DELETE FROM `*PREFIX*mozilla_sync_collections` WHERE `id` = ?'); $result = $query->execute(array($collectionId)); if ($result === false) { Utils::writeLogDbError("DB: Failed to delete collection " . $collectionId . " for user " . $syncId . ".", $query); return false; } } OutputData::write(Utils::getMozillaTimestamp()); return true; } /** * @brief Returns the WBO in the collection corresponding to the requested * ID. * * HTTP request: GET https://server/pathname/version/username/storage/collection/id * * @param integer $syncId The Sync user requesting the WBO. * @param integer $collectionId The collection the WBO belongs to. * @param integer $wboId The WBO's ID. * @return bool True on success, false otherwise. */ private function getWBO($syncId, $collectionId, $wboId) { $query = \OCP\DB::prepare('SELECT `sortindex`, `payload`, `name` AS `id`, `modified` FROM `*PREFIX*mozilla_sync_wbo` WHERE `collectionid` = ? AND `name` = ?'); $result = $query->execute(array($collectionId, $wboId)); if ($result === false) { Utils::writeLogDbError("DB: Failed to get WBO " . $wboId . " of collection " . $collectionId . " for user " . $syncId . ".", $query); return false; } $row = $result->fetchRow(); if ($row === false) { Utils::changeHttpStatus(Utils::STATUS_NOT_FOUND); Utils::writeLog("DB: Could not find requested WBO " . $wboId . " of collection " . $collectionId . " for user " . $syncId . ".", \OCP\Util::WARN); return true; } // Cast returned values to the correct type $row = self::forceTypeCasting($row); OutputData::write($row); return true; } /** * @brief Adds the WBO defined in the request body to the collection. * * HTTP request: PUT https://server/pathname/version/username/storage/collection/id * * If the WBO does not contain a payload, it will only update the provided metadata fields on an already defined object. * The server will return the timestamp associated with the modification. * * @param integer $syncId The Sync user the WBO belongs to. * @param integer $collectionId The collection the WBO belongs to. * @param integer $wboId The WBO's ID. * @return bool True on success, false otherwise. */ private function putWBO($syncId, $collectionId, $wboId) { // Get and validate input data $inputData = $this->getInputData(); if ((!$inputData->isValid()) && (count($inputData->getInputArray()) === 1)) { Utils::changeHttpStatus(Utils::STATUS_INVALID_DATA); Utils::writeLog("URL: Invalid input data for putting WBO " . $wboId . " of collection " . $collectionId . " for user " . $syncId . "."); return false; } // Check if user has free space available $size = ((float) strlen(serialize($inputData))/1024.0); // approximate the input data size if(!$this->checkUserQuota($syncId, $size)) { return false; } // Get time to be updated in database and sent as header if (isset($inputData['modified'])) { $modifiedTime = $inputData['modified']; } else { $modifiedTime = Utils::getMozillaTimestamp(); } $result = Storage::saveWBO($syncId, $modifiedTime, $collectionId, $inputData->getInputArray()); if ($result === false) { Utils::writeLog("Failed to save WBO " . $wboId . " of collection " . $collectionId . " for user " . $syncId . "."); return false; } // Return the same modification time in payload and X-Weave-Timestamp header OutputData::write($modifiedTime, $modifiedTime); } /** * @brief Deletes the WBO with the given ID. * * HTTP request: DELETE https://server/pathname/version/username/storage/collection/id * * @param integer $syncId The Sync user whose WBO will be deleted. * @param integer $collectionId The collection ID the WBO belongs to. * @param integer $wboId The WBO's ID. * @return bool True on success, false otherwise. */ private function deleteWBO($syncId, $collectionId, $wboId) { $result = Storage::deleteWBO($syncId, $collectionId, $wboId); if ($result === false) { Utils::writeLog("Failed to delete WBO " . $wboId . " of collection " . $collectionId . " for user " . $syncId . "."); return false; } OutputData::write(Utils::getMozillaTimestamp()); return true; } /** * @brief Deletes all records for the specified user. * * HTTP request: DELETE https://server/pathname/version/username/storage * * Will return a precondition error unless an X-Confirm-Delete header is included. * * All delete requests return the timestamp of the action. * * @param integer $syncId The Sync user whose records will be deleted. * @return bool True on success, false otherwise. */ private function deleteStorage($syncId) { // Only continue if X-Confirm-Delete header is set if (!isset($_SERVER['HTTP_X_CONFIRM_DELETE'])) { Utils::writeLog("Did not send X_CONFIRM_DELETE header when trying to delete all records for user " . $syncId . "."); return false; } $result = Storage::deleteStorage($syncId); if ($result === false) { Utils::writeLog("Failed to delete all records for user " . $syncId . "."); return false; } OutputData::write(Utils::getMozillaTimestamp()); return true; } /** * @brief Casts result rows to the correct type. * * Some implementations (e.g. PHP 5.3 in combination with MySQL 5.5) don't return the * correct type in JSON. To fix this we explicitly cast the values that have been * returned by the database. * * Casts modified to float, sortindex to int. * * @param array $row Row returned from the database. * @return array Row with explicitly casted types. */ public static function forceTypeCasting($row) { // Return modified as float, not string if (!isset($row['modified'])) { unset($row['modified']); } else { $row['modified'] = (float) $row['modified']; } // Return sortindex as int, not string if (!isset($row['sortindex'])) { unset($row['sortindex']); } else { $row['sortindex'] = (int) $row['sortindex']; } return $row; } } /* vim: set ts=4 sw=4 tw=80 noet : */