diff options
author | Eugene Sandulenko | 2016-08-30 13:54:12 +0200 |
---|---|---|
committer | GitHub | 2016-08-30 13:54:12 +0200 |
commit | bfbfbd3e1a8397cc3b1c9eaff6c7754abe3ddc3d (patch) | |
tree | 68851bdc99b78b4b1902944c1df3c3ee5d4f4489 | |
parent | 7df744c291e5fcf5dc7afd3b21189c2c56810f8e (diff) | |
parent | 368f664c813075f8b8cb2c572dce65f60128eda4 (diff) | |
download | scummvm-rg350-bfbfbd3e1a8397cc3b1c9eaff6c7754abe3ddc3d.tar.gz scummvm-rg350-bfbfbd3e1a8397cc3b1c9eaff6c7754abe3ddc3d.tar.bz2 scummvm-rg350-bfbfbd3e1a8397cc3b1c9eaff6c7754abe3ddc3d.zip |
Merge pull request #788 from Tkachov/cloud
ALL: Add Cloud storage support
306 files changed, 25036 insertions, 822 deletions
diff --git a/backends/base-backend.cpp b/backends/base-backend.cpp index 3e95c3e26a..dfb9e284ce 100644 --- a/backends/base-backend.cpp +++ b/backends/base-backend.cpp @@ -39,6 +39,20 @@ void BaseBackend::displayMessageOnOSD(const char *msg) { dialog.runModal(); } +void BaseBackend::copyRectToOSD(const void *buf, int pitch, int x, int y, int w, int h) { + warning("BaseBackend::copyRectToOSD not implemented"); //TODO +} + +void BaseBackend::clearOSD() { + warning("BaseBackend::clearOSD not implemented"); //TODO + //what should I do? Remove all TimedMessageDialogs? +} + +Graphics::PixelFormat BaseBackend::getOSDFormat() { + warning("BaseBackend::getOSDFormat not implemented"); + return Graphics::PixelFormat(); +} + void BaseBackend::initBackend() { // Init Event manager #ifndef DISABLE_DEFAULT_EVENT_MANAGER diff --git a/backends/base-backend.h b/backends/base-backend.h index 598f682b32..2394edaf38 100644 --- a/backends/base-backend.h +++ b/backends/base-backend.h @@ -33,6 +33,9 @@ public: virtual void initBackend(); virtual void displayMessageOnOSD(const char *msg); + virtual void copyRectToOSD(const void *buf, int pitch, int x, int y, int w, int h); + virtual void clearOSD(); + virtual Graphics::PixelFormat getOSDFormat(); virtual void fillScreen(uint32 col); }; diff --git a/backends/cloud/box/boxlistdirectorybyidrequest.cpp b/backends/cloud/box/boxlistdirectorybyidrequest.cpp new file mode 100644 index 0000000000..c013f1eb2a --- /dev/null +++ b/backends/cloud/box/boxlistdirectorybyidrequest.cpp @@ -0,0 +1,195 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/cloud/box/boxlistdirectorybyidrequest.h" +#include "backends/cloud/box/boxstorage.h" +#include "backends/cloud/box/boxtokenrefresher.h" +#include "backends/cloud/iso8601.h" +#include "backends/cloud/storage.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/json.h" + +namespace Cloud { +namespace Box { + +#define BOX_LIST_DIRECTORY_LIMIT 1000 +#define BOX_FOLDERS_API_LINK "https://api.box.com/2.0/folders/%s/items?offset=%u&limit=%u&fields=%s" + +BoxListDirectoryByIdRequest::BoxListDirectoryByIdRequest(BoxStorage *storage, Common::String id, Storage::ListDirectoryCallback cb, Networking::ErrorCallback ecb): + Networking::Request(nullptr, ecb), _requestedId(id), _storage(storage), _listDirectoryCallback(cb), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +BoxListDirectoryByIdRequest::~BoxListDirectoryByIdRequest() { + _ignoreCallback = true; + if (_workingRequest) _workingRequest->finish(); + delete _listDirectoryCallback; +} + +void BoxListDirectoryByIdRequest::start() { + _ignoreCallback = true; + if (_workingRequest) _workingRequest->finish(); + _files.clear(); + _ignoreCallback = false; + + makeRequest(0); +} + +void BoxListDirectoryByIdRequest::makeRequest(uint32 offset) { + Common::String url = Common::String::format( + BOX_FOLDERS_API_LINK, + _requestedId.c_str(), + offset, + BOX_LIST_DIRECTORY_LIMIT, + "id,type,name,size,modified_at" + ); + + Networking::JsonCallback callback = new Common::Callback<BoxListDirectoryByIdRequest, Networking::JsonResponse>(this, &BoxListDirectoryByIdRequest::responseCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<BoxListDirectoryByIdRequest, Networking::ErrorResponse>(this, &BoxListDirectoryByIdRequest::errorCallback); + Networking::CurlJsonRequest *request = new BoxTokenRefresher(_storage, callback, failureCallback, url.c_str()); + request->addHeader("Authorization: Bearer " + _storage->accessToken()); + _workingRequest = ConnMan.addRequest(request); +} + +void BoxListDirectoryByIdRequest::responseCallback(Networking::JsonResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) { + delete response.value; + return; + } + + if (response.request) + _date = response.request->date(); + + Networking::ErrorResponse error(this); + Networking::CurlJsonRequest *rq = (Networking::CurlJsonRequest *)response.request; + if (rq && rq->getNetworkReadStream()) + error.httpResponseCode = rq->getNetworkReadStream()->httpResponseCode(); + + Common::JSONValue *json = response.value; + if (json == nullptr) { + error.response = "Failed to parse JSON, null passed!"; + finishError(error); + return; + } + + if (!json->isObject()) { + error.response = "Passed JSON is not an object!"; + finishError(error); + delete json; + return; + } + + Common::JSONObject responseObject = json->asObject(); + //debug(9, "%s", json->stringify(true).c_str()); + + //TODO: handle error messages passed as JSON + /* + if (responseObject.contains("error") || responseObject.contains("error_summary")) { + warning("Box returned error: %s", responseObject.getVal("error_summary")->asString().c_str()); + error.failed = true; + error.response = json->stringify(); + finishError(error); + delete json; + return; + } + */ + + //check that ALL keys exist AND HAVE RIGHT TYPE to avoid segfaults + if (responseObject.contains("entries")) { + if (!responseObject.getVal("entries")->isArray()) { + error.response = Common::String::format( + "\"entries\" found, but that's not an array!\n%s", + responseObject.getVal("entries")->stringify(true).c_str() + ); + finishError(error); + delete json; + return; + } + + Common::JSONArray items = responseObject.getVal("entries")->asArray(); + for (uint32 i = 0; i < items.size(); ++i) { + if (!Networking::CurlJsonRequest::jsonIsObject(items[i], "BoxListDirectoryByIdRequest")) continue; + + Common::JSONObject item = items[i]->asObject(); + + if (!Networking::CurlJsonRequest::jsonContainsString(item, "id", "BoxListDirectoryByIdRequest")) continue; + if (!Networking::CurlJsonRequest::jsonContainsString(item, "name", "BoxListDirectoryByIdRequest")) continue; + if (!Networking::CurlJsonRequest::jsonContainsString(item, "type", "BoxListDirectoryByIdRequest")) continue; + if (!Networking::CurlJsonRequest::jsonContainsString(item, "modified_at", "BoxListDirectoryByIdRequest")) continue; + if (!Networking::CurlJsonRequest::jsonContainsStringOrIntegerNumber(item, "size", "BoxListDirectoryByIdRequest")) continue; + + Common::String id = item.getVal("id")->asString(); + Common::String name = item.getVal("name")->asString(); + bool isDirectory = (item.getVal("type")->asString() == "folder"); + uint32 size; + if (item.getVal("size")->isString()) { + size = item.getVal("size")->asString().asUint64(); + } else { + size = item.getVal("size")->asIntegerNumber(); + } + uint32 timestamp = ISO8601::convertToTimestamp(item.getVal("modified_at")->asString()); + + //as we list directory by id, we can't determine full path for the file, so we leave it empty + _files.push_back(StorageFile(id, "", name, size, timestamp, isDirectory)); + } + } + + uint32 received = 0; + uint32 totalCount = 0; + if (responseObject.contains("total_count") && responseObject.getVal("total_count")->isIntegerNumber()) + totalCount = responseObject.getVal("total_count")->asIntegerNumber(); + if (responseObject.contains("offset") && responseObject.getVal("offset")->isIntegerNumber()) + received = responseObject.getVal("offset")->asIntegerNumber(); + if (responseObject.contains("limit") && responseObject.getVal("limit")->isIntegerNumber()) + received += responseObject.getVal("limit")->asIntegerNumber(); + bool hasMore = (received < totalCount); + + if (hasMore) makeRequest(received); + else finishListing(_files); + + delete json; +} + +void BoxListDirectoryByIdRequest::errorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) return; + if (error.request) _date = error.request->date(); + finishError(error); +} + +void BoxListDirectoryByIdRequest::handle() {} + +void BoxListDirectoryByIdRequest::restart() { start(); } + +Common::String BoxListDirectoryByIdRequest::date() const { return _date; } + +void BoxListDirectoryByIdRequest::finishListing(Common::Array<StorageFile> &files) { + Request::finishSuccess(); + if (_listDirectoryCallback) (*_listDirectoryCallback)(Storage::ListDirectoryResponse(this, files)); +} + +} // End of namespace Box +} // End of namespace Cloud diff --git a/backends/cloud/box/boxlistdirectorybyidrequest.h b/backends/cloud/box/boxlistdirectorybyidrequest.h new file mode 100644 index 0000000000..13f1ba056c --- /dev/null +++ b/backends/cloud/box/boxlistdirectorybyidrequest.h @@ -0,0 +1,63 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_BOX_BOXLISTDIRECTORYBYIDREQUEST_H +#define BACKENDS_CLOUD_BOX_BOXLISTDIRECTORYBYIDREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/request.h" +#include "common/callback.h" + +namespace Cloud { +namespace Box { + +class BoxStorage; + +class BoxListDirectoryByIdRequest: public Networking::Request { + Common::String _requestedId; + BoxStorage *_storage; + + Storage::ListDirectoryCallback _listDirectoryCallback; + Common::Array<StorageFile> _files; + Request *_workingRequest; + bool _ignoreCallback; + Common::String _date; + + void start(); + void makeRequest(uint32 offset); + void responseCallback(Networking::JsonResponse response); + void errorCallback(Networking::ErrorResponse error); + void finishListing(Common::Array<StorageFile> &files); +public: + BoxListDirectoryByIdRequest(BoxStorage *storage, Common::String id, Storage::ListDirectoryCallback cb, Networking::ErrorCallback ecb); + virtual ~BoxListDirectoryByIdRequest(); + + virtual void handle(); + virtual void restart(); + virtual Common::String date() const; +}; + +} // End of namespace Box +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/box/boxstorage.cpp b/backends/cloud/box/boxstorage.cpp new file mode 100644 index 0000000000..70864679e7 --- /dev/null +++ b/backends/cloud/box/boxstorage.cpp @@ -0,0 +1,345 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ +#define FORBIDDEN_SYMBOL_ALLOW_ALL + +#include "backends/cloud/box/boxstorage.h" +#include "backends/cloud/box/boxlistdirectorybyidrequest.h" +#include "backends/cloud/box/boxtokenrefresher.h" +#include "backends/cloud/box/boxuploadrequest.h" +#include "backends/cloud/cloudmanager.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/config-manager.h" +#include "common/debug.h" +#include "common/json.h" +#include <curl/curl.h> + +#ifdef ENABLE_RELEASE +#include "dists/clouds/cloud_keys.h" +#endif + +namespace Cloud { +namespace Box { + +#define BOX_OAUTH2_TOKEN "https://api.box.com/oauth2/token" +#define BOX_API_FOLDERS "https://api.box.com/2.0/folders" +#define BOX_API_FILES_CONTENT "https://api.box.com/2.0/files/%s/content" +#define BOX_API_USERS_ME "https://api.box.com/2.0/users/me" + +char *BoxStorage::KEY = nullptr; //can't use CloudConfig there yet, loading it on instance creation/auth +char *BoxStorage::SECRET = nullptr; + +void BoxStorage::loadKeyAndSecret() { +#ifdef ENABLE_RELEASE + KEY = RELEASE_BOX_KEY; + SECRET = RELEASE_BOX_SECRET; +#else + Common::String k = ConfMan.get("BOX_KEY", ConfMan.kCloudDomain); + KEY = new char[k.size() + 1]; + memcpy(KEY, k.c_str(), k.size()); + KEY[k.size()] = 0; + + k = ConfMan.get("BOX_SECRET", ConfMan.kCloudDomain); + SECRET = new char[k.size() + 1]; + memcpy(SECRET, k.c_str(), k.size()); + SECRET[k.size()] = 0; +#endif +} + +BoxStorage::BoxStorage(Common::String accessToken, Common::String refreshToken): + _token(accessToken), _refreshToken(refreshToken) {} + +BoxStorage::BoxStorage(Common::String code) { + getAccessToken( + new Common::Callback<BoxStorage, BoolResponse>(this, &BoxStorage::codeFlowComplete), + new Common::Callback<BoxStorage, Networking::ErrorResponse>(this, &BoxStorage::codeFlowFailed), + code + ); +} + +BoxStorage::~BoxStorage() {} + +void BoxStorage::getAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback, Common::String code) { + if (!KEY || !SECRET) + loadKeyAndSecret(); + bool codeFlow = (code != ""); + + if (!codeFlow && _refreshToken == "") { + warning("BoxStorage: no refresh token available to get new access token."); + if (callback) (*callback)(BoolResponse(nullptr, false)); + return; + } + + Networking::JsonCallback innerCallback = new Common::CallbackBridge<BoxStorage, BoolResponse, Networking::JsonResponse>(this, &BoxStorage::tokenRefreshed, callback); + if (errorCallback == nullptr) + errorCallback = getErrorPrintingCallback(); + + Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(innerCallback, errorCallback, BOX_OAUTH2_TOKEN); + if (codeFlow) { + request->addPostField("grant_type=authorization_code"); + request->addPostField("code=" + code); + } else { + request->addPostField("grant_type=refresh_token"); + request->addPostField("refresh_token=" + _refreshToken); + } + request->addPostField("client_id=" + Common::String(KEY)); + request->addPostField("client_secret=" + Common::String(SECRET)); + /* + if (Cloud::CloudManager::couldUseLocalServer()) { + request->addPostField("&redirect_uri=http%3A%2F%2Flocalhost%3A12345"); + } else { + request->addPostField("&redirect_uri=https%3A%2F%2Fwww.scummvm.org/c/code"); + } + */ + addRequest(request); +} + +void BoxStorage::tokenRefreshed(BoolCallback callback, Networking::JsonResponse response) { + Common::JSONValue *json = response.value; + if (!json) { + warning("BoxStorage: got NULL instead of JSON"); + if (callback) + (*callback)(BoolResponse(nullptr, false)); + delete callback; + return; + } + + if (!Networking::CurlJsonRequest::jsonIsObject(json, "BoxStorage")) { + if (callback) + (*callback)(BoolResponse(nullptr, false)); + delete json; + delete callback; + return; + } + + Common::JSONObject result = json->asObject(); + if (!Networking::CurlJsonRequest::jsonContainsString(result, "access_token", "BoxStorage") || + !Networking::CurlJsonRequest::jsonContainsString(result, "refresh_token", "BoxStorage")) { + warning("BoxStorage: bad response, no token passed"); + debug(9, "%s", json->stringify().c_str()); + if (callback) + (*callback)(BoolResponse(nullptr, false)); + } else { + _token = result.getVal("access_token")->asString(); + _refreshToken = result.getVal("refresh_token")->asString(); + CloudMan.save(); //ask CloudManager to save our new refreshToken + if (callback) + (*callback)(BoolResponse(nullptr, true)); + } + delete json; + delete callback; +} + +void BoxStorage::codeFlowComplete(BoolResponse response) { + if (!response.value) { + warning("BoxStorage: failed to get access token through code flow"); + CloudMan.removeStorage(this); + return; + } + + CloudMan.replaceStorage(this, kStorageBoxId); + ConfMan.flushToDisk(); +} + +void BoxStorage::codeFlowFailed(Networking::ErrorResponse error) { + debug(9, "BoxStorage: code flow failed (%s, %ld):", (error.failed ? "failed" : "interrupted"), error.httpResponseCode); + debug(9, "%s", error.response.c_str()); + CloudMan.removeStorage(this); +} + +void BoxStorage::saveConfig(Common::String keyPrefix) { + ConfMan.set(keyPrefix + "access_token", _token, ConfMan.kCloudDomain); + ConfMan.set(keyPrefix + "refresh_token", _refreshToken, ConfMan.kCloudDomain); +} + +Common::String BoxStorage::name() const { + return "Box"; +} + +void BoxStorage::infoInnerCallback(StorageInfoCallback outerCallback, Networking::JsonResponse response) { + Common::JSONValue *json = response.value; + if (!json) { + warning("BoxStorage::infoInnerCallback: NULL passed instead of JSON"); + delete outerCallback; + return; + } + + if (!Networking::CurlJsonRequest::jsonIsObject(json, "BoxStorage::infoInnerCallback")) { + delete json; + delete outerCallback; + return; + } + + Common::JSONObject info = json->asObject(); + + Common::String uid, name, email; + uint64 quotaUsed = 0, quotaAllocated = 0; + + // can check that "type": "user" + // there is also "max_upload_size", "phone" and "avatar_url" + + if (Networking::CurlJsonRequest::jsonContainsString(info, "id", "BoxStorage::infoInnerCallback")) + uid = info.getVal("id")->asString(); + + if (Networking::CurlJsonRequest::jsonContainsString(info, "name", "BoxStorage::infoInnerCallback")) + name = info.getVal("name")->asString(); + + if (Networking::CurlJsonRequest::jsonContainsString(info, "login", "BoxStorage::infoInnerCallback")) + email = info.getVal("login")->asString(); + + if (Networking::CurlJsonRequest::jsonContainsIntegerNumber(info, "space_amount", "BoxStorage::infoInnerCallback")) + quotaAllocated = info.getVal("space_amount")->asIntegerNumber(); + + if (Networking::CurlJsonRequest::jsonContainsIntegerNumber(info, "space_used", "BoxStorage::infoInnerCallback")) + quotaUsed = info.getVal("space_used")->asIntegerNumber(); + + Common::String username = email; + if (username == "") username = name; + if (username == "") username = uid; + CloudMan.setStorageUsername(kStorageBoxId, username); + + if (outerCallback) { + (*outerCallback)(StorageInfoResponse(nullptr, StorageInfo(uid, name, email, quotaUsed, quotaAllocated))); + delete outerCallback; + } + + delete json; +} + +Networking::Request *BoxStorage::listDirectoryById(Common::String id, ListDirectoryCallback callback, Networking::ErrorCallback errorCallback) { + if (!errorCallback) + errorCallback = getErrorPrintingCallback(); + if (!callback) + callback = getPrintFilesCallback(); + return addRequest(new BoxListDirectoryByIdRequest(this, id, callback, errorCallback)); +} + +void BoxStorage::createDirectoryInnerCallback(BoolCallback outerCallback, Networking::JsonResponse response) { + Common::JSONValue *json = response.value; + if (!json) { + warning("BoxStorage::createDirectoryInnerCallback: NULL passed instead of JSON"); + delete outerCallback; + return; + } + + if (outerCallback) { + if (Networking::CurlJsonRequest::jsonIsObject(json, "BoxStorage::createDirectoryInnerCallback")) { + Common::JSONObject info = json->asObject(); + (*outerCallback)(BoolResponse(nullptr, info.contains("id"))); + } else { + (*outerCallback)(BoolResponse(nullptr, false)); + } + delete outerCallback; + } + + delete json; +} + +Networking::Request *BoxStorage::createDirectoryWithParentId(Common::String parentId, Common::String name, BoolCallback callback, Networking::ErrorCallback errorCallback) { + if (!errorCallback) + errorCallback = getErrorPrintingCallback(); + + Common::String url = BOX_API_FOLDERS; + Networking::JsonCallback innerCallback = new Common::CallbackBridge<BoxStorage, BoolResponse, Networking::JsonResponse>(this, &BoxStorage::createDirectoryInnerCallback, callback); + Networking::CurlJsonRequest *request = new BoxTokenRefresher(this, innerCallback, errorCallback, url.c_str()); + request->addHeader("Authorization: Bearer " + accessToken()); + request->addHeader("Content-Type: application/json"); + + Common::JSONObject parentObject; + parentObject.setVal("id", new Common::JSONValue(parentId)); + + Common::JSONObject jsonRequestParameters; + jsonRequestParameters.setVal("name", new Common::JSONValue(name)); + jsonRequestParameters.setVal("parent", new Common::JSONValue(parentObject)); + + Common::JSONValue value(jsonRequestParameters); + request->addPostField(Common::JSON::stringify(&value)); + + return addRequest(request); +} + +Networking::Request *BoxStorage::upload(Common::String remotePath, Common::String localPath, UploadCallback callback, Networking::ErrorCallback errorCallback) { + if (!errorCallback) + errorCallback = getErrorPrintingCallback(); + return addRequest(new BoxUploadRequest(this, remotePath, localPath, callback, errorCallback)); +} + +Networking::Request *BoxStorage::upload(Common::String path, Common::SeekableReadStream *contents, UploadCallback callback, Networking::ErrorCallback errorCallback) { + warning("BoxStorage::upload(ReadStream) not implemented"); + if (errorCallback) + (*errorCallback)(Networking::ErrorResponse(nullptr, false, true, "BoxStorage::upload(ReadStream) not implemented", -1)); + delete callback; + delete errorCallback; + return nullptr; +} + +bool BoxStorage::uploadStreamSupported() { + return false; +} + +Networking::Request *BoxStorage::streamFileById(Common::String id, Networking::NetworkReadStreamCallback callback, Networking::ErrorCallback errorCallback) { + if (callback) { + Common::String url = Common::String::format(BOX_API_FILES_CONTENT, id.c_str()); + Common::String header = "Authorization: Bearer " + _token; + curl_slist *headersList = curl_slist_append(nullptr, header.c_str()); + Networking::NetworkReadStream *stream = new Networking::NetworkReadStream(url.c_str(), headersList, ""); + (*callback)(Networking::NetworkReadStreamResponse(nullptr, stream)); + } + delete callback; + delete errorCallback; + return nullptr; +} + +Networking::Request *BoxStorage::info(StorageInfoCallback callback, Networking::ErrorCallback errorCallback) { + Networking::JsonCallback innerCallback = new Common::CallbackBridge<BoxStorage, StorageInfoResponse, Networking::JsonResponse>(this, &BoxStorage::infoInnerCallback, callback); + Networking::CurlJsonRequest *request = new BoxTokenRefresher(this, innerCallback, errorCallback, BOX_API_USERS_ME); + request->addHeader("Authorization: Bearer " + _token); + return addRequest(request); +} + +Common::String BoxStorage::savesDirectoryPath() { return "scummvm/saves/"; } + +BoxStorage *BoxStorage::loadFromConfig(Common::String keyPrefix) { + loadKeyAndSecret(); + + if (!ConfMan.hasKey(keyPrefix + "access_token", ConfMan.kCloudDomain)) { + warning("BoxStorage: no access_token found"); + return nullptr; + } + + if (!ConfMan.hasKey(keyPrefix + "refresh_token", ConfMan.kCloudDomain)) { + warning("BoxStorage: no refresh_token found"); + return nullptr; + } + + Common::String accessToken = ConfMan.get(keyPrefix + "access_token", ConfMan.kCloudDomain); + Common::String refreshToken = ConfMan.get(keyPrefix + "refresh_token", ConfMan.kCloudDomain); + return new BoxStorage(accessToken, refreshToken); +} + +Common::String BoxStorage::getRootDirectoryId() { + return "0"; +} + +} // End of namespace Box +} // End of namespace Cloud diff --git a/backends/cloud/box/boxstorage.h b/backends/cloud/box/boxstorage.h new file mode 100644 index 0000000000..2dd516d894 --- /dev/null +++ b/backends/cloud/box/boxstorage.h @@ -0,0 +1,116 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_BOX_BOXSTORAGE_H +#define BACKENDS_CLOUD_BOX_BOXSTORAGE_H + +#include "backends/cloud/id/idstorage.h" +#include "backends/networking/curl/curljsonrequest.h" + +namespace Cloud { +namespace Box { + +class BoxStorage: public Id::IdStorage { + static char *KEY, *SECRET; + + static void loadKeyAndSecret(); + + Common::String _token, _refreshToken; + + /** This private constructor is called from loadFromConfig(). */ + BoxStorage(Common::String token, Common::String refreshToken); + + void tokenRefreshed(BoolCallback callback, Networking::JsonResponse response); + void codeFlowComplete(BoolResponse response); + void codeFlowFailed(Networking::ErrorResponse error); + + /** Constructs StorageInfo based on JSON response from cloud. */ + void infoInnerCallback(StorageInfoCallback outerCallback, Networking::JsonResponse json); + + void createDirectoryInnerCallback(BoolCallback outerCallback, Networking::JsonResponse response); +public: + /** This constructor uses OAuth code flow to get tokens. */ + BoxStorage(Common::String code); + virtual ~BoxStorage(); + + /** + * Storage methods, which are used by CloudManager to save + * storage in configuration file. + */ + + /** + * Save storage data using ConfMan. + * @param keyPrefix all saved keys must start with this prefix. + * @note every Storage must write keyPrefix + "type" key + * with common value (e.g. "Dropbox"). + */ + virtual void saveConfig(Common::String keyPrefix); + + /** + * Return unique storage name. + * @returns some unique storage name (for example, "Dropbox (user@example.com)") + */ + virtual Common::String name() const; + + /** Public Cloud API comes down there. */ + + virtual Networking::Request *listDirectoryById(Common::String id, ListDirectoryCallback callback, Networking::ErrorCallback errorCallback); + virtual Networking::Request *createDirectoryWithParentId(Common::String parentId, Common::String name, BoolCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns UploadStatus struct with info about uploaded file. */ + virtual Networking::Request *upload(Common::String remotePath, Common::String localPath, UploadCallback callback, Networking::ErrorCallback errorCallback); + virtual Networking::Request *upload(Common::String path, Common::SeekableReadStream *contents, UploadCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns whether Storage supports upload(ReadStream). */ + virtual bool uploadStreamSupported(); + + /** Returns pointer to Networking::NetworkReadStream. */ + virtual Networking::Request *streamFileById(Common::String path, Networking::NetworkReadStreamCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns the StorageInfo struct. */ + virtual Networking::Request *info(StorageInfoCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns storage's saves directory path with the trailing slash. */ + virtual Common::String savesDirectoryPath(); + + /** + * Load token and user id from configs and return BoxStorage for those. + * @return pointer to the newly created BoxStorage or 0 if some problem occured. + */ + static BoxStorage *loadFromConfig(Common::String keyPrefix); + + virtual Common::String getRootDirectoryId(); + + /** + * Gets new access_token. If <code> passed is "", refresh_token is used. + * Use "" in order to refresh token and pass a callback, so you could + * continue your work when new token is available. + */ + void getAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback = nullptr, Common::String code = ""); + + Common::String accessToken() const { return _token; } +}; + +} // End of namespace Box +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/box/boxtokenrefresher.cpp b/backends/cloud/box/boxtokenrefresher.cpp new file mode 100644 index 0000000000..ca05eef838 --- /dev/null +++ b/backends/cloud/box/boxtokenrefresher.cpp @@ -0,0 +1,137 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ +#define FORBIDDEN_SYMBOL_ALLOW_ALL + +#include "backends/cloud/box/boxtokenrefresher.h" +#include "backends/cloud/box/boxstorage.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/debug.h" +#include "common/json.h" +#include <curl/curl.h> + +namespace Cloud { +namespace Box { + +BoxTokenRefresher::BoxTokenRefresher(BoxStorage *parent, Networking::JsonCallback callback, Networking::ErrorCallback ecb, const char *url): + CurlJsonRequest(callback, ecb, url), _parentStorage(parent) {} + +BoxTokenRefresher::~BoxTokenRefresher() {} + +void BoxTokenRefresher::tokenRefreshed(Storage::BoolResponse response) { + if (!response.value) { + //failed to refresh token, notify user with NULL in original callback + warning("BoxTokenRefresher: failed to refresh token"); + finishError(Networking::ErrorResponse(this, false, true, "", -1)); + return; + } + + //update headers: first change header with token, then pass those to request + for (uint32 i = 0; i < _headers.size(); ++i) { + if (_headers[i].contains("Authorization")) { + _headers[i] = "Authorization: Bearer " + _parentStorage->accessToken(); + } + } + setHeaders(_headers); + + //successfully received refreshed token, can restart the original request now + retry(0); +} + +void BoxTokenRefresher::finishJson(Common::JSONValue *json) { + if (!json) { + //that's probably not an error (200 OK) + CurlJsonRequest::finishJson(nullptr); + return; + } + + if (jsonIsObject(json, "BoxTokenRefresher")) { + Common::JSONObject result = json->asObject(); + if (result.contains("type") && result.getVal("type")->isString() && result.getVal("type")->asString() == "error") { + //new token needed => request token & then retry original request + long httpCode = -1; + if (_stream) { + httpCode = _stream->httpResponseCode(); + debug(9, "BoxTokenRefresher: code %ld", httpCode); + } + + bool irrecoverable = true; + + Common::String code, message; + if (jsonContainsString(result, "code", "BoxTokenRefresher")) { + code = result.getVal("code")->asString(); + debug(9, "BoxTokenRefresher: code = %s", code.c_str()); + } + + if (jsonContainsString(result, "message", "BoxTokenRefresher")) { + message = result.getVal("message")->asString(); + debug(9, "BoxTokenRefresher: message = %s", message.c_str()); + } + + //TODO: decide when token refreshment will help + //for now refreshment is used only when HTTP 401 is passed in finishError() + //if (code == "unauthenticated") irrecoverable = false; + + if (irrecoverable) { + finishError(Networking::ErrorResponse(this, false, true, json->stringify(true), httpCode)); + delete json; + return; + } + + pause(); + delete json; + _parentStorage->getAccessToken(new Common::Callback<BoxTokenRefresher, Storage::BoolResponse>(this, &BoxTokenRefresher::tokenRefreshed)); + return; + } + } + + //notify user of success + CurlJsonRequest::finishJson(json); +} + +void BoxTokenRefresher::finishError(Networking::ErrorResponse error) { + if (error.httpResponseCode == 401) { // invalid_token + pause(); + _parentStorage->getAccessToken(new Common::Callback<BoxTokenRefresher, Storage::BoolResponse>(this, &BoxTokenRefresher::tokenRefreshed)); + return; + } + + // there are also 400 == invalid_request and 403 == insufficient_scope + // but TokenRefresher is there to refresh token when it's invalid only + + Request::finishError(error); +} + +void BoxTokenRefresher::setHeaders(Common::Array<Common::String> &headers) { + _headers = headers; + curl_slist_free_all(_headersList); + _headersList = 0; + for (uint32 i = 0; i < headers.size(); ++i) + CurlJsonRequest::addHeader(headers[i]); +} + +void BoxTokenRefresher::addHeader(Common::String header) { + _headers.push_back(header); + CurlJsonRequest::addHeader(header); +} + +} // End of namespace Box +} // End of namespace Cloud diff --git a/backends/cloud/box/boxtokenrefresher.h b/backends/cloud/box/boxtokenrefresher.h new file mode 100644 index 0000000000..c08e8468c3 --- /dev/null +++ b/backends/cloud/box/boxtokenrefresher.h @@ -0,0 +1,53 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_BOX_BOXTOKENREFRESHER_H +#define BACKENDS_CLOUD_BOX_BOXTOKENREFRESHER_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/curljsonrequest.h" + +namespace Cloud { +namespace Box { + +class BoxStorage; + +class BoxTokenRefresher: public Networking::CurlJsonRequest { + BoxStorage *_parentStorage; + Common::Array<Common::String> _headers; + + void tokenRefreshed(Storage::BoolResponse response); + + virtual void finishJson(Common::JSONValue *json); + virtual void finishError(Networking::ErrorResponse error); +public: + BoxTokenRefresher(BoxStorage *parent, Networking::JsonCallback callback, Networking::ErrorCallback ecb, const char *url); + virtual ~BoxTokenRefresher(); + + virtual void setHeaders(Common::Array<Common::String> &headers); + virtual void addHeader(Common::String header); +}; + +} // End of namespace Box +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/box/boxuploadrequest.cpp b/backends/cloud/box/boxuploadrequest.cpp new file mode 100644 index 0000000000..5084aa5652 --- /dev/null +++ b/backends/cloud/box/boxuploadrequest.cpp @@ -0,0 +1,232 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/cloud/box/boxuploadrequest.h" +#include "backends/cloud/box/boxstorage.h" +#include "backends/cloud/box/boxtokenrefresher.h" +#include "backends/cloud/iso8601.h" +#include "backends/cloud/storage.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/json.h" + +namespace Cloud { +namespace Box { + +#define BOX_API_FILES "https://upload.box.com/api/2.0/files" + +BoxUploadRequest::BoxUploadRequest(BoxStorage *storage, Common::String path, Common::String localPath, Storage::UploadCallback callback, Networking::ErrorCallback ecb): + Networking::Request(nullptr, ecb), _storage(storage), _savePath(path), _localPath(localPath), _uploadCallback(callback), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +BoxUploadRequest::~BoxUploadRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _uploadCallback; +} + +void BoxUploadRequest::start() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _resolvedId = ""; //used to update file contents + _parentId = ""; //used to create file within parent directory + _ignoreCallback = false; + + resolveId(); +} + +void BoxUploadRequest::resolveId() { + //check whether such file already exists + Storage::UploadCallback innerCallback = new Common::Callback<BoxUploadRequest, Storage::UploadResponse>(this, &BoxUploadRequest::idResolvedCallback); + Networking::ErrorCallback innerErrorCallback = new Common::Callback<BoxUploadRequest, Networking::ErrorResponse>(this, &BoxUploadRequest::idResolveFailedCallback); + _workingRequest = _storage->resolveFileId(_savePath, innerCallback, innerErrorCallback); +} + +void BoxUploadRequest::idResolvedCallback(Storage::UploadResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) return; + _resolvedId = response.value.id(); + upload(); +} + +void BoxUploadRequest::idResolveFailedCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) return; + + //not resolved => error or no such file + if (error.response.contains("no such file found in its parent directory")) { + //parent's id after the '\n' + Common::String parentId = error.response; + for (uint32 i = 0; i < parentId.size(); ++i) + if (parentId[i] == '\n') { + parentId.erase(0, i + 1); + break; + } + + _parentId = parentId; + upload(); + return; + } + + finishError(error); +} + +void BoxUploadRequest::upload() { + Common::String name = _savePath; + for (uint32 i = name.size(); i > 0; --i) { + if (name[i - 1] == '/' || name[i - 1] == '\\') { + name.erase(0, i); + break; + } + } + + Common::String url = BOX_API_FILES; + if (_resolvedId != "") + url += "/" + _resolvedId; + url += "/content"; + Networking::JsonCallback callback = new Common::Callback<BoxUploadRequest, Networking::JsonResponse>(this, &BoxUploadRequest::uploadedCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<BoxUploadRequest, Networking::ErrorResponse>(this, &BoxUploadRequest::notUploadedCallback); + Networking::CurlJsonRequest *request = new BoxTokenRefresher(_storage, callback, failureCallback, url.c_str()); + request->addHeader("Authorization: Bearer " + _storage->accessToken()); + + Common::JSONObject jsonRequestParameters; + if (_resolvedId == "") { + Common::JSONObject parentObject; + parentObject.setVal("id", new Common::JSONValue(_parentId)); + jsonRequestParameters.setVal("parent", new Common::JSONValue(parentObject)); + jsonRequestParameters.setVal("name", new Common::JSONValue(name)); + } + + Common::JSONValue value(jsonRequestParameters); + request->addFormField("attributes", Common::JSON::stringify(&value)); + request->addFormFile("file", _localPath); + + _workingRequest = ConnMan.addRequest(request); +} + +void BoxUploadRequest::uploadedCallback(Networking::JsonResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) return; + + Networking::ErrorResponse error(this, false, true, "", -1); + Networking::CurlJsonRequest *rq = (Networking::CurlJsonRequest *)response.request; + if (rq) { + const Networking::NetworkReadStream *stream = rq->getNetworkReadStream(); + if (stream) { + long code = stream->httpResponseCode(); + error.httpResponseCode = code; + } + } + + if (error.httpResponseCode != 200 && error.httpResponseCode != 201) + warning("BoxUploadRequest: looks like an error (bad HTTP code)"); + + //check JSON and show warnings if it's malformed + Common::JSONValue *json = response.value; + if (json == nullptr) { + error.response = "Failed to parse JSON, null passed!"; + finishError(error); + return; + } + + if (!json->isObject()) { + error.response = "Passed JSON is not an object!"; + finishError(error); + delete json; + return; + } + + Common::JSONObject object = json->asObject(); + if (Networking::CurlJsonRequest::jsonContainsArray(object, "entries", "BoxUploadRequest")) { + Common::JSONArray entries = object.getVal("entries")->asArray(); + if (entries.size() == 0) { + warning("BoxUploadRequest: 'entries' found, but it's empty"); + } else if (!Networking::CurlJsonRequest::jsonIsObject(entries[0], "BoxUploadRequest")) { + warning("BoxUploadRequest: 'entries' first item is not an object"); + } else { + Common::JSONObject item = entries[0]->asObject(); + + if (Networking::CurlJsonRequest::jsonContainsString(item, "id", "BoxUploadRequest") && + Networking::CurlJsonRequest::jsonContainsString(item, "name", "BoxUploadRequest") && + Networking::CurlJsonRequest::jsonContainsString(item, "type", "BoxUploadRequest") && + Networking::CurlJsonRequest::jsonContainsString(item, "modified_at", "BoxUploadRequest") && + Networking::CurlJsonRequest::jsonContainsStringOrIntegerNumber(item, "size", "BoxUploadRequest")) { + + //finished + Common::String id = item.getVal("id")->asString(); + Common::String name = item.getVal("name")->asString(); + bool isDirectory = (item.getVal("type")->asString() == "folder"); + uint32 size; + if (item.getVal("size")->isString()) { + size = item.getVal("size")->asString().asUint64(); + } else { + size = item.getVal("size")->asIntegerNumber(); + } + uint32 timestamp = ISO8601::convertToTimestamp(item.getVal("modified_at")->asString()); + + finishUpload(StorageFile(id, _savePath, name, size, timestamp, isDirectory)); + delete json; + return; + } + } + } + + //TODO: check errors + /* + if (object.contains("error")) { + warning("Box returned error: %s", json->stringify(true).c_str()); + delete json; + error.response = json->stringify(true); + finishError(error); + return; + } + */ + + warning("BoxUploadRequest: no file info to return"); + finishUpload(StorageFile(_savePath, 0, 0, false)); + + delete json; +} + +void BoxUploadRequest::notUploadedCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) return; + finishError(error); +} + +void BoxUploadRequest::handle() {} + +void BoxUploadRequest::restart() { start(); } + +void BoxUploadRequest::finishUpload(StorageFile file) { + Request::finishSuccess(); + if (_uploadCallback) + (*_uploadCallback)(Storage::UploadResponse(this, file)); +} + +} // End of namespace Box +} // End of namespace Cloud diff --git a/backends/cloud/box/boxuploadrequest.h b/backends/cloud/box/boxuploadrequest.h new file mode 100644 index 0000000000..1bc8690210 --- /dev/null +++ b/backends/cloud/box/boxuploadrequest.h @@ -0,0 +1,63 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_BOX_BOXUPLOADREQUEST_H +#define BACKENDS_CLOUD_BOX_BOXUPLOADREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/request.h" +#include "common/callback.h" + +namespace Cloud { +namespace Box { +class BoxStorage; + +class BoxUploadRequest: public Networking::Request { + BoxStorage *_storage; + Common::String _savePath, _localPath; + Storage::UploadCallback _uploadCallback; + Request *_workingRequest; + bool _ignoreCallback; + Common::String _resolvedId, _parentId; + + void start(); + void resolveId(); + void idResolvedCallback(Storage::UploadResponse response); + void idResolveFailedCallback(Networking::ErrorResponse error); + void upload(); + void uploadedCallback(Networking::JsonResponse response); + void notUploadedCallback(Networking::ErrorResponse error); + void finishUpload(StorageFile status); + +public: + BoxUploadRequest(BoxStorage *storage, Common::String path, Common::String localPath, Storage::UploadCallback callback, Networking::ErrorCallback ecb); + virtual ~BoxUploadRequest(); + + virtual void handle(); + virtual void restart(); +}; + +} // End of namespace Box +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/cloudmanager.cpp b/backends/cloud/cloudmanager.cpp new file mode 100644 index 0000000000..826fc6103d --- /dev/null +++ b/backends/cloud/cloudmanager.cpp @@ -0,0 +1,456 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/cloud/cloudmanager.h" +#include "backends/cloud/box/boxstorage.h" +#include "backends/cloud/dropbox/dropboxstorage.h" +#include "backends/cloud/onedrive/onedrivestorage.h" +#include "backends/cloud/googledrive/googledrivestorage.h" +#include "common/translation.h" +#include "common/config-manager.h" +#include "common/str.h" +#ifdef USE_SDL_NET +#include "backends/networking/sdl_net/localwebserver.h" +#endif + +namespace Common { + +DECLARE_SINGLETON(Cloud::CloudManager); + +} + +namespace Cloud { + +const char *const CloudManager::kStoragePrefix = "storage_"; + +CloudManager::CloudManager() : _currentStorageIndex(0), _activeStorage(nullptr) {} + +CloudManager::~CloudManager() { + delete _activeStorage; + freeStorages(); +} + +Common::String CloudManager::getStorageConfigName(uint32 index) const { + switch (index) { + case kStorageNoneId: return "<none>"; + case kStorageDropboxId: return "Dropbox"; + case kStorageOneDriveId: return "OneDrive"; + case kStorageGoogleDriveId: return "GoogleDrive"; + case kStorageBoxId: return "Box"; + } + assert(false); // Unhandled StorageID value + return ""; +} + +void CloudManager::loadStorage() { + switch (_currentStorageIndex) { + case kStorageDropboxId: + _activeStorage = Dropbox::DropboxStorage::loadFromConfig(kStoragePrefix + getStorageConfigName(_currentStorageIndex) + "_"); + break; + case kStorageOneDriveId: + _activeStorage = OneDrive::OneDriveStorage::loadFromConfig(kStoragePrefix + getStorageConfigName(_currentStorageIndex) + "_"); + break; + case kStorageGoogleDriveId: + _activeStorage = GoogleDrive::GoogleDriveStorage::loadFromConfig(kStoragePrefix + getStorageConfigName(_currentStorageIndex) + "_"); + break; + case kStorageBoxId: + _activeStorage = Box::BoxStorage::loadFromConfig(kStoragePrefix + getStorageConfigName(_currentStorageIndex) + "_"); + break; + default: + _activeStorage = nullptr; + } + + if (!_activeStorage) { + _currentStorageIndex = kStorageNoneId; + } +} + +void CloudManager::init() { + //init configs structs + for (uint32 i = 0; i < kStorageTotal; ++i) { + Common::String name = getStorageConfigName(i); + StorageConfig config; + config.name = _(name); + config.username = ""; + config.lastSyncDate = ""; + config.usedBytes = 0; + if (ConfMan.hasKey(kStoragePrefix + name + "_username", ConfMan.kCloudDomain)) + config.username = ConfMan.get(kStoragePrefix + name + "_username", ConfMan.kCloudDomain); + if (ConfMan.hasKey(kStoragePrefix + name + "_lastSync", ConfMan.kCloudDomain)) + config.lastSyncDate = ConfMan.get(kStoragePrefix + name + "_lastSync", ConfMan.kCloudDomain); + if (ConfMan.hasKey(kStoragePrefix + name + "_usedBytes", ConfMan.kCloudDomain)) + config.usedBytes = ConfMan.get(kStoragePrefix + name + "_usedBytes", ConfMan.kCloudDomain).asUint64(); + _storages.push_back(config); + } + + //load an active storage if there is any + _currentStorageIndex = kStorageNoneId; + if (ConfMan.hasKey("current_storage", ConfMan.kCloudDomain)) + _currentStorageIndex = ConfMan.getInt("current_storage", ConfMan.kCloudDomain); + + loadStorage(); +} + +void CloudManager::save() { + for (uint32 i = 0; i < _storages.size(); ++i) { + if (i == kStorageNoneId) + continue; + Common::String name = getStorageConfigName(i); + ConfMan.set(kStoragePrefix + name + "_username", _storages[i].username, ConfMan.kCloudDomain); + ConfMan.set(kStoragePrefix + name + "_lastSync", _storages[i].lastSyncDate, ConfMan.kCloudDomain); + ConfMan.set(kStoragePrefix + name + "_usedBytes", Common::String::format("%lu", _storages[i].usedBytes), ConfMan.kCloudDomain); + } + + ConfMan.set("current_storage", Common::String::format("%u", _currentStorageIndex), ConfMan.kCloudDomain); + if (_activeStorage) + _activeStorage->saveConfig(kStoragePrefix + getStorageConfigName(_currentStorageIndex) + "_"); + ConfMan.flushToDisk(); +} + +void CloudManager::replaceStorage(Storage *storage, uint32 index) { + freeStorages(); + if (!storage) + error("CloudManager::replaceStorage: NULL storage passed"); + if (index >= kStorageTotal) + error("CloudManager::replaceStorage: invalid index passed"); + if (_activeStorage != nullptr && _activeStorage->isWorking()) { + warning("CloudManager::replaceStorage: replacing Storage while the other is working"); + if (_activeStorage->isDownloading()) + _activeStorage->cancelDownload(); + if (_activeStorage->isSyncing()) + _activeStorage->cancelSync(); + removeStorage(_activeStorage); + } else { + delete _activeStorage; + } + _activeStorage = storage; + _currentStorageIndex = index; + save(); + + //do what should be done on first Storage connect + if (_activeStorage) { + _activeStorage->info(nullptr, nullptr); //automatically calls setStorageUsername() + _activeStorage->syncSaves(nullptr, nullptr); + } +} + +void CloudManager::removeStorage(Storage *storage) { + // can't just delete it as it's mostly likely the one who calls the method + // it would be freed on freeStorages() call (on next Storage connect or replace) + _storagesToRemove.push_back(storage); +} + +void CloudManager::freeStorages() { + for (uint32 i = 0; i < _storagesToRemove.size(); ++i) + delete _storagesToRemove[i]; + _storagesToRemove.clear(); +} + +void CloudManager::passNoStorageConnected(Networking::ErrorCallback errorCallback) const { + if (errorCallback == nullptr) + return; + (*errorCallback)(Networking::ErrorResponse(nullptr, false, true, "No Storage connected!", -1)); +} + +Storage *CloudManager::getCurrentStorage() const { + return _activeStorage; +} + +uint32 CloudManager::getStorageIndex() const { + return _currentStorageIndex; +} + +Common::StringArray CloudManager::listStorages() const { + Common::StringArray result; + for (uint32 i = 0; i < _storages.size(); ++i) { + result.push_back(_storages[i].name); + } + return result; +} + +bool CloudManager::switchStorage(uint32 index) { + if (index >= _storages.size()) { + warning("CloudManager::switchStorage: invalid index passed"); + return false; + } + + Storage *storage = getCurrentStorage(); + if (storage && storage->isWorking()) { + warning("CloudManager::switchStorage: another storage is working now"); + return false; + } + + _currentStorageIndex = index; + loadStorage(); + save(); + return true; +} + +Common::String CloudManager::getStorageUsername(uint32 index) { + if (index >= _storages.size()) + return ""; + return _storages[index].username; +} + +uint64 CloudManager::getStorageUsedSpace(uint32 index) { + if (index >= _storages.size()) + return 0; + return _storages[index].usedBytes; +} + +Common::String CloudManager::getStorageLastSync(uint32 index) { + if (index >= _storages.size()) + return ""; + if (index == _currentStorageIndex && isSyncing()) + return ""; + return _storages[index].lastSyncDate; +} + +void CloudManager::setStorageUsername(uint32 index, Common::String name) { + if (index >= _storages.size()) + return; + _storages[index].username = name; + save(); +} + +void CloudManager::setStorageUsedSpace(uint32 index, uint64 used) { + if (index >= _storages.size()) + return; + _storages[index].usedBytes = used; + save(); +} + +void CloudManager::setStorageLastSync(uint32 index, Common::String date) { + if (index >= _storages.size()) + return; + _storages[index].lastSyncDate = date; + save(); +} + +void CloudManager::connectStorage(uint32 index, Common::String code) { + freeStorages(); + + Storage *storage = nullptr; + switch (index) { + case kStorageDropboxId: + storage = new Dropbox::DropboxStorage(code); + break; + case kStorageOneDriveId: + storage = new OneDrive::OneDriveStorage(code); + break; + case kStorageGoogleDriveId: + storage = new GoogleDrive::GoogleDriveStorage(code); + break; + case kStorageBoxId: + storage = new Box::BoxStorage(code); + break; + } + // in these constructors Storages request token using the passed code + // when the token is received, they call replaceStorage() + // or removeStorage(), if some error occurred + // thus, no memory leak happens +} + +Networking::Request *CloudManager::listDirectory(Common::String path, Storage::ListDirectoryCallback callback, Networking::ErrorCallback errorCallback, bool recursive) { + Storage *storage = getCurrentStorage(); + if (storage) { + return storage->listDirectory(path, callback, errorCallback, recursive); + } else { + passNoStorageConnected(errorCallback); + delete callback; + delete errorCallback; + } + return nullptr; +} + +Networking::Request *CloudManager::downloadFolder(Common::String remotePath, Common::String localPath, Storage::FileArrayCallback callback, Networking::ErrorCallback errorCallback, bool recursive) { + Storage *storage = getCurrentStorage(); + if (storage) { + return storage->downloadFolder(remotePath, localPath, callback, errorCallback, recursive); + } else { + passNoStorageConnected(errorCallback); + delete callback; + delete errorCallback; + } + return nullptr; +} + +Networking::Request *CloudManager::info(Storage::StorageInfoCallback callback, Networking::ErrorCallback errorCallback) { + Storage *storage = getCurrentStorage(); + if (storage) { + return storage->info(callback, errorCallback); + } else { + passNoStorageConnected(errorCallback); + delete callback; + delete errorCallback; + } + return nullptr; +} + +Common::String CloudManager::savesDirectoryPath() { + Storage *storage = getCurrentStorage(); + if (storage) + return storage->savesDirectoryPath(); + return ""; +} + +SavesSyncRequest *CloudManager::syncSaves(Storage::BoolCallback callback, Networking::ErrorCallback errorCallback) { + Storage *storage = getCurrentStorage(); + if (storage) { + setStorageLastSync(_currentStorageIndex, "???"); //TODO get the date + return storage->syncSaves(callback, errorCallback); + } else { + passNoStorageConnected(errorCallback); + delete callback; + delete errorCallback; + } + return nullptr; +} + +bool CloudManager::isWorking() const { + Storage *storage = getCurrentStorage(); + if (storage) + return storage->isWorking(); + return false; +} + +bool CloudManager::couldUseLocalServer() { +#ifdef USE_SDL_NET + return Networking::LocalWebserver::getPort() == Networking::LocalWebserver::DEFAULT_SERVER_PORT; +#else + return false; +#endif +} + +///// SavesSyncRequest-related ///// + +bool CloudManager::isSyncing() const { + Storage *storage = getCurrentStorage(); + if (storage) + return storage->isSyncing(); + return false; +} + +double CloudManager::getSyncDownloadingProgress() const { + Storage *storage = getCurrentStorage(); + if (storage) + return storage->getSyncDownloadingProgress(); + return 1; +} + +double CloudManager::getSyncProgress() const { + Storage *storage = getCurrentStorage(); + if (storage) + return storage->getSyncProgress(); + return 1; +} + +Common::Array<Common::String> CloudManager::getSyncingFiles() const { + Storage *storage = getCurrentStorage(); + if (storage) + return storage->getSyncingFiles(); + return Common::Array<Common::String>(); +} + +void CloudManager::cancelSync() const { + Storage *storage = getCurrentStorage(); + if (storage) + storage->cancelSync(); +} + +void CloudManager::setSyncTarget(GUI::CommandReceiver *target) const { + Storage *storage = getCurrentStorage(); + if (storage) + storage->setSyncTarget(target); +} + +///// DownloadFolderRequest-related ///// + +bool CloudManager::startDownload(Common::String remotePath, Common::String localPath) const { + Storage *storage = getCurrentStorage(); + if (storage) + return storage->startDownload(remotePath, localPath); + return false; +} + +void CloudManager::cancelDownload() const { + Storage *storage = getCurrentStorage(); + if (storage) + storage->cancelDownload(); +} + +void CloudManager::setDownloadTarget(GUI::CommandReceiver *target) const { + Storage *storage = getCurrentStorage(); + if (storage) + storage->setDownloadTarget(target); +} + +bool CloudManager::isDownloading() const { + Storage *storage = getCurrentStorage(); + if (storage) + return storage->isDownloading(); + return false; +} + +double CloudManager::getDownloadingProgress() const { + Storage *storage = getCurrentStorage(); + if (storage) + return storage->getDownloadingProgress(); + return 1; +} + +uint64 CloudManager::getDownloadBytesNumber() const { + Storage *storage = getCurrentStorage(); + if (storage) + return storage->getDownloadBytesNumber(); + return 0; +} + +uint64 CloudManager::getDownloadTotalBytesNumber() const { + Storage *storage = getCurrentStorage(); + if (storage) + return storage->getDownloadTotalBytesNumber(); + return 0; +} + +uint64 CloudManager::getDownloadSpeed() const { + Storage *storage = getCurrentStorage(); + if (storage) + return storage->getDownloadSpeed(); + return 0; +} + +Common::String CloudManager::getDownloadRemoteDirectory() const { + Storage *storage = getCurrentStorage(); + if (storage) + return storage->getDownloadRemoteDirectory(); + return ""; +} + +Common::String CloudManager::getDownloadLocalDirectory() const { + Storage *storage = getCurrentStorage(); + if (storage) + return storage->getDownloadLocalDirectory(); + return ""; +} + +} // End of namespace Cloud diff --git a/backends/cloud/cloudmanager.h b/backends/cloud/cloudmanager.h new file mode 100644 index 0000000000..c504ff39cb --- /dev/null +++ b/backends/cloud/cloudmanager.h @@ -0,0 +1,274 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef CLOUD_CLOUDMANAGER_H +#define CLOUD_CLOUDMANAGER_H + +#include "backends/cloud/storage.h" +#include "common/array.h" +#include "common/singleton.h" +#include "common/str-array.h" + +namespace GUI { + +class CommandReceiver; + +} + +namespace Cloud { + +// The actual indexes in CloudManager's array +enum StorageID { + kStorageNoneId = 0, + kStorageDropboxId = 1, + kStorageOneDriveId = 2, + kStorageGoogleDriveId = 3, + kStorageBoxId = 4, + + kStorageTotal +}; + +class CloudManager : public Common::Singleton<CloudManager> { + static const char *const kStoragePrefix; + + struct StorageConfig { + Common::String name, username; + uint64 usedBytes; + Common::String lastSyncDate; + }; + + Common::Array<StorageConfig> _storages; + uint _currentStorageIndex; + Storage *_activeStorage; + Common::Array<Storage *> _storagesToRemove; + + void loadStorage(); + + Common::String getStorageConfigName(uint32 index) const; + + /** Frees memory used by storages which failed to connect. */ + void freeStorages(); + + /** Calls the error callback with a special "no storage connected" message. */ + void passNoStorageConnected(Networking::ErrorCallback errorCallback) const; + +public: + CloudManager(); + virtual ~CloudManager(); + + /** + * Loads all information from configs and creates current Storage instance. + * + * @note It's called once on startup in scummvm_main(). + */ + void init(); + + /** + * Saves all information into configuration file. + */ + void save(); + + /** + * Replace active Storage. + * @note this method automatically saves the changes with ConfMan. + * + * @param storage Cloud::Storage to replace active storage with. + * @param index one of Cloud::StorageID enum values to indicate what storage type is replaced. + */ + void replaceStorage(Storage *storage, uint32 index); + + /** Adds storage in the list of storages to remove later. */ + void removeStorage(Storage *storage); + + /** + * Returns active Storage, which could be used to interact + * with cloud storage. + * + * @return active Cloud::Storage or null, if there is no active Storage. + */ + Cloud::Storage *getCurrentStorage() const; + + /** + * Return active Storage's index. + * + * @return active Storage's index. + */ + uint32 getStorageIndex() const; + + /** + * Return Storages names as list. + * + * @return a list of Storages names. + */ + Common::StringArray listStorages() const; + + /** + * Changes the storage to the one with given index. + * + * @param new Storage's index. + */ + bool switchStorage(uint32 index); + + /** + * Return username used by Storage. + * + * @param Storage's index. + * @returns username or "" if index is invalid (no such Storage). + */ + Common::String getStorageUsername(uint32 index); + + /** + * Return space used by Storage. + * + * @param Storage's index. + * @returns used space in bytes or 0 if index is invalid (no such Storage). + */ + uint64 getStorageUsedSpace(uint32 index); + + /** + * Return Storage's last sync date. + * + * @param Storage's index. + * @returns last sync date or "" if index is invalid (no such Storage). + It also returns "" if there never was any sync + or if storage is syncing right now. + */ + Common::String getStorageLastSync(uint32 index); + + /** + * Set Storage's username. + * Automatically saves changes to the config. + * + * @param index Storage's index. + * @param name username to set + */ + void setStorageUsername(uint32 index, Common::String name); + + /** + * Set Storage's used space field. + * Automatically saves changes to the config. + * + * @param index Storage's index. + * @param used value to set + */ + void setStorageUsedSpace(uint32 index, uint64 used); + + /** + * Set Storage's last sync date. + * Automatically saves changes to the config. + * + * @param index Storage's index. + * @param date date to set + */ + void setStorageLastSync(uint32 index, Common::String date); + + /** + * Replace Storage which has given index with a + * storage created with given code. + * + * @param index Storage's index + * @param code OAuth2 code received from user + */ + void connectStorage(uint32 index, Common::String code); + + /** Returns ListDirectoryResponse with list of files. */ + Networking::Request *listDirectory(Common::String path, Storage::ListDirectoryCallback callback, Networking::ErrorCallback errorCallback, bool recursive = false); + + /** Returns Common::Array<StorageFile> with list of files, which were not downloaded. */ + Networking::Request *downloadFolder(Common::String remotePath, Common::String localPath, Storage::FileArrayCallback callback, Networking::ErrorCallback errorCallback, bool recursive = false); + + /** Return the StorageInfo struct. */ + Networking::Request *info(Storage::StorageInfoCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns storage's saves directory path with the trailing slash. */ + Common::String savesDirectoryPath(); + + /** + * Starts saves syncing process in currently active storage if there is any. + */ + SavesSyncRequest *syncSaves(Cloud::Storage::BoolCallback callback = nullptr, Networking::ErrorCallback errorCallback = nullptr); + + /** Returns whether there are any requests running. */ + bool isWorking() const; + + /** Returns whether LocalWebserver is available to use for auth. */ + static bool couldUseLocalServer(); + + ///// SavesSyncRequest-related ///// + + /** Returns whether there is a SavesSyncRequest running. */ + bool isSyncing() const; + + /** Returns a number in [0, 1] range which represents current sync downloading progress (1 = complete). */ + double getSyncDownloadingProgress() const; + + /** Returns a number in [0, 1] range which represents current sync progress (1 = complete). */ + double getSyncProgress() const; + + /** Returns an array of saves names which are not yet synced (thus cannot be used). */ + Common::Array<Common::String> getSyncingFiles() const; + + /** Cancels running sync. */ + void cancelSync() const; + + /** Sets SavesSyncRequest's target to given CommandReceiver. */ + void setSyncTarget(GUI::CommandReceiver *target) const; + + ///// DownloadFolderRequest-related ///// + + /** Starts a folder download. */ + bool startDownload(Common::String remotePath, Common::String localPath) const; + + /** Cancels running download. */ + void cancelDownload() const; + + /** Sets FolderDownloadRequest's target to given CommandReceiver. */ + void setDownloadTarget(GUI::CommandReceiver *target) const; + + /** Returns whether there is a FolderDownloadRequest running. */ + bool isDownloading() const; + + /** Returns a number in [0, 1] range which represents current download progress (1 = complete). */ + double getDownloadingProgress() const; + + /** Returns a number of bytes that is downloaded in current download progress. */ + uint64 getDownloadBytesNumber() const; + + /** Returns a total number of bytes to be downloaded in current download progress. */ + uint64 getDownloadTotalBytesNumber() const; + + /** Returns download speed of current download progress. */ + uint64 getDownloadSpeed() const; + + /** Returns remote directory path. */ + Common::String getDownloadRemoteDirectory() const; + + /** Returns local directory path. */ + Common::String getDownloadLocalDirectory() const; +}; + +/** Shortcut for accessing the connection manager. */ +#define CloudMan Cloud::CloudManager::instance() + +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/downloadrequest.cpp b/backends/cloud/downloadrequest.cpp new file mode 100644 index 0000000000..e28670fc7b --- /dev/null +++ b/backends/cloud/downloadrequest.cpp @@ -0,0 +1,138 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/cloud/downloadrequest.h" +#include "backends/networking/curl/connectionmanager.h" +#include "common/textconsole.h" + +namespace Cloud { + +DownloadRequest::DownloadRequest(Storage *storage, Storage::BoolCallback callback, Networking::ErrorCallback ecb, Common::String remoteFileId, Common::DumpFile *dumpFile): + Request(nullptr, ecb), _boolCallback(callback), _localFile(dumpFile), _remoteFileId(remoteFileId), _storage(storage), + _remoteFileStream(nullptr), _workingRequest(nullptr), _ignoreCallback(false), _buffer(new byte[DOWNLOAD_REQUEST_BUFFER_SIZE]) { + start(); +} + +DownloadRequest::~DownloadRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _boolCallback; + delete _localFile; + delete[] _buffer; +} + +void DownloadRequest::start() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _remoteFileStream = nullptr; + //TODO: add some way to reopen DumpFile, so DownloadRequest could be restarted + _ignoreCallback = false; + + _workingRequest = _storage->streamFileById( + _remoteFileId, + new Common::Callback<DownloadRequest, Networking::NetworkReadStreamResponse>(this, &DownloadRequest::streamCallback), + new Common::Callback<DownloadRequest, Networking::ErrorResponse>(this, &DownloadRequest::streamErrorCallback) + ); +} + +void DownloadRequest::streamCallback(Networking::NetworkReadStreamResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + _remoteFileStream = (Networking::NetworkReadStream *)response.value; +} + +void DownloadRequest::streamErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + finishError(error); +} + +void DownloadRequest::handle() { + if (!_localFile) { + warning("DownloadRequest: no file to write"); + finishError(Networking::ErrorResponse(this, false, true, "", -1)); + return; + } + + if (!_localFile->isOpen()) { + warning("DownloadRequest: failed to open file to write"); + finishError(Networking::ErrorResponse(this, false, true, "", -1)); + return; + } + + if (!_remoteFileStream) { + //waiting for callback + return; + } + + uint32 readBytes = _remoteFileStream->read(_buffer, DOWNLOAD_REQUEST_BUFFER_SIZE); + + if (readBytes != 0) + if (_localFile->write(_buffer, readBytes) != readBytes) { + warning("DownloadRequest: unable to write all received bytes into output file"); + finishError(Networking::ErrorResponse(this, false, true, "", -1)); + return; + } + + if (_remoteFileStream->eos()) { + if (_remoteFileStream->httpResponseCode() != 200) { + warning("DownloadRequest: HTTP response code is not 200 OK (it's %ld)", _remoteFileStream->httpResponseCode()); + //TODO: do something about it actually + // the problem is file's already downloaded, stream is over + // so we can't return error message anymore + } + + finishDownload(_remoteFileStream->httpResponseCode() == 200); + + _localFile->close(); //yes, I know it's closed automatically in ~DumpFile() + } +} + +void DownloadRequest::restart() { + warning("DownloadRequest: can't restart as there are no means to reopen DumpFile"); + finishError(Networking::ErrorResponse(this, false, true, "", -1)); + //start(); +} + +void DownloadRequest::finishDownload(bool success) { + Request::finishSuccess(); + if (_boolCallback) + (*_boolCallback)(Storage::BoolResponse(this, success)); +} + +void DownloadRequest::finishError(Networking::ErrorResponse error) { + if (_localFile) + _localFile->close(); + Request::finishError(error); +} + +double DownloadRequest::getProgress() const { + if (_remoteFileStream) + return _remoteFileStream->getProgress(); + return 0; +} + +} // End of namespace Cloud diff --git a/backends/cloud/downloadrequest.h b/backends/cloud/downloadrequest.h new file mode 100644 index 0000000000..7e58098849 --- /dev/null +++ b/backends/cloud/downloadrequest.h @@ -0,0 +1,64 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_DOWNLOADREQUEST_H +#define BACKENDS_CLOUD_DOWNLOADREQUEST_H + +#include "backends/networking/curl/request.h" +#include "backends/networking/curl/networkreadstream.h" +#include "backends/cloud/storage.h" +#include "common/file.h" + +namespace Cloud { + +#define DOWNLOAD_REQUEST_BUFFER_SIZE 1 * 1024 * 1024 + +class DownloadRequest: public Networking::Request { + Storage::BoolCallback _boolCallback; + Common::DumpFile *_localFile; + Common::String _remoteFileId; + Storage *_storage; + Networking::NetworkReadStream *_remoteFileStream; + Request *_workingRequest; + bool _ignoreCallback; + byte *_buffer; + + void start(); + void streamCallback(Networking::NetworkReadStreamResponse response); + void streamErrorCallback(Networking::ErrorResponse error); + void finishDownload(bool success); + virtual void finishError(Networking::ErrorResponse error); + +public: + DownloadRequest(Storage *storage, Storage::BoolCallback callback, Networking::ErrorCallback ecb, Common::String remoteFileId, Common::DumpFile *dumpFile); + virtual ~DownloadRequest(); + + virtual void handle(); + virtual void restart(); + + /** Returns a number in range [0, 1], where 1 is "complete". */ + double getProgress() const; +}; + +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/dropbox/dropboxcreatedirectoryrequest.cpp b/backends/cloud/dropbox/dropboxcreatedirectoryrequest.cpp new file mode 100644 index 0000000000..97090b44f8 --- /dev/null +++ b/backends/cloud/dropbox/dropboxcreatedirectoryrequest.cpp @@ -0,0 +1,137 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/cloud/dropbox/dropboxcreatedirectoryrequest.h" +#include "backends/cloud/storage.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/json.h" + +namespace Cloud { +namespace Dropbox { + +#define DROPBOX_API_CREATE_FOLDER "https://api.dropboxapi.com/2/files/create_folder" + +DropboxCreateDirectoryRequest::DropboxCreateDirectoryRequest(Common::String token, Common::String path, Storage::BoolCallback cb, Networking::ErrorCallback ecb): + Networking::Request(nullptr, ecb), _token(token), _path(path), _boolCallback(cb), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +DropboxCreateDirectoryRequest::~DropboxCreateDirectoryRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _boolCallback; +} + +void DropboxCreateDirectoryRequest::start() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _ignoreCallback = false; + + Networking::JsonCallback innerCallback = new Common::Callback<DropboxCreateDirectoryRequest, Networking::JsonResponse>(this, &DropboxCreateDirectoryRequest::responseCallback); + Networking::ErrorCallback errorCallback = new Common::Callback<DropboxCreateDirectoryRequest, Networking::ErrorResponse>(this, &DropboxCreateDirectoryRequest::errorCallback); + Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(innerCallback, errorCallback, DROPBOX_API_CREATE_FOLDER); + request->addHeader("Authorization: Bearer " + _token); + request->addHeader("Content-Type: application/json"); + + Common::JSONObject jsonRequestParameters; + jsonRequestParameters.setVal("path", new Common::JSONValue(_path)); + Common::JSONValue value(jsonRequestParameters); + request->addPostField(Common::JSON::stringify(&value)); + + _workingRequest = ConnMan.addRequest(request); +} + +void DropboxCreateDirectoryRequest::responseCallback(Networking::JsonResponse response) { + Common::JSONValue *json = response.value; + _workingRequest = nullptr; + if (_ignoreCallback) { + delete json; + return; + } + if (response.request) _date = response.request->date(); + + Networking::ErrorResponse error(this); + Networking::CurlJsonRequest *rq = (Networking::CurlJsonRequest *)response.request; + if (rq && rq->getNetworkReadStream()) + error.httpResponseCode = rq->getNetworkReadStream()->httpResponseCode(); + + if (json == nullptr) { + error.response = "Failed to parse JSON, null passed!"; + finishError(error); + return; + } + + if (!json->isObject()) { + error.response = "Passed JSON is not an object!"; + finishError(error); + delete json; + return; + } + + Common::JSONObject info = json->asObject(); + if (info.contains("id")) { + finishCreation(true); + } else { + if (Networking::CurlJsonRequest::jsonContainsString(info, "error_summary", "DropboxCreateDirectoryRequest")) { + Common::String summary = info.getVal("error_summary")->asString(); + if (summary.contains("path") && summary.contains("conflict") && summary.contains("folder")) { + // existing directory - not an error for CreateDirectoryRequest + finishCreation(false); + delete json; + return; + } + } + error.response = json->stringify(true); + finishError(error); + } + + delete json; +} + +void DropboxCreateDirectoryRequest::errorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (error.request) + _date = error.request->date(); + finishError(error); +} + +void DropboxCreateDirectoryRequest::handle() {} + +void DropboxCreateDirectoryRequest::restart() { start(); } + +Common::String DropboxCreateDirectoryRequest::date() const { return _date; } + +void DropboxCreateDirectoryRequest::finishCreation(bool success) { + Request::finishSuccess(); + if (_boolCallback) + (*_boolCallback)(Storage::BoolResponse(this, success)); +} + +} // End of namespace Dropbox +} // End of namespace Cloud diff --git a/backends/cloud/dropbox/dropboxcreatedirectoryrequest.h b/backends/cloud/dropbox/dropboxcreatedirectoryrequest.h new file mode 100644 index 0000000000..0ef6a22a04 --- /dev/null +++ b/backends/cloud/dropbox/dropboxcreatedirectoryrequest.h @@ -0,0 +1,57 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_DROPBOX_DROPBOXCREATEDIRECTORYREQUEST_H +#define BACKENDS_CLOUD_DROPBOX_DROPBOXCREATEDIRECTORYREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/request.h" +#include "backends/networking/curl/curljsonrequest.h" + +namespace Cloud { +namespace Dropbox { + +class DropboxCreateDirectoryRequest: public Networking::Request { + Common::String _token; + Common::String _path; + Storage::BoolCallback _boolCallback; + Request *_workingRequest; + bool _ignoreCallback; + Common::String _date; + + void start(); + void responseCallback(Networking::JsonResponse response); + void errorCallback(Networking::ErrorResponse error); + void finishCreation(bool success); +public: + DropboxCreateDirectoryRequest(Common::String token, Common::String path, Storage::BoolCallback cb, Networking::ErrorCallback ecb); + virtual ~DropboxCreateDirectoryRequest(); + + virtual void handle(); + virtual void restart(); + virtual Common::String date() const; +}; + +} // End of namespace Dropbox +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/dropbox/dropboxinforequest.cpp b/backends/cloud/dropbox/dropboxinforequest.cpp new file mode 100644 index 0000000000..6cdbe3321b --- /dev/null +++ b/backends/cloud/dropbox/dropboxinforequest.cpp @@ -0,0 +1,192 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/cloud/dropbox/dropboxinforequest.h" +#include "backends/cloud/cloudmanager.h" +#include "backends/cloud/storage.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/json.h" + +namespace Cloud { +namespace Dropbox { + +#define DROPBOX_API_GET_CURRENT_ACCOUNT "https://api.dropboxapi.com/2/users/get_current_account" +#define DROPBOX_API_GET_SPACE_USAGE "https://api.dropboxapi.com/2/users/get_space_usage" + +DropboxInfoRequest::DropboxInfoRequest(Common::String token, Storage::StorageInfoCallback cb, Networking::ErrorCallback ecb): + Networking::Request(nullptr, ecb), _token(token), _infoCallback(cb), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +DropboxInfoRequest::~DropboxInfoRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _infoCallback; +} + +void DropboxInfoRequest::start() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _ignoreCallback = false; + + Networking::JsonCallback innerCallback = new Common::Callback<DropboxInfoRequest, Networking::JsonResponse>(this, &DropboxInfoRequest::userResponseCallback); + Networking::ErrorCallback errorCallback = new Common::Callback<DropboxInfoRequest, Networking::ErrorResponse>(this, &DropboxInfoRequest::errorCallback); + Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(innerCallback, errorCallback, DROPBOX_API_GET_CURRENT_ACCOUNT); + request->addHeader("Authorization: Bearer " + _token); + request->addHeader("Content-Type: application/json"); + request->addPostField("null"); //use POST + + _workingRequest = ConnMan.addRequest(request); +} + +void DropboxInfoRequest::userResponseCallback(Networking::JsonResponse response) { + Common::JSONValue *json = response.value; + _workingRequest = nullptr; + if (_ignoreCallback) { + delete json; + return; + } + + Networking::ErrorResponse error(this); + Networking::CurlJsonRequest *rq = (Networking::CurlJsonRequest *)response.request; + if (rq && rq->getNetworkReadStream()) + error.httpResponseCode = rq->getNetworkReadStream()->httpResponseCode(); + + if (json == nullptr) { + error.response = "Failed to parse JSON, null passed!"; + finishError(error); + return; + } + + if (!json->isObject()) { + error.response = "Passed JSON is not an object!"; + finishError(error); + delete json; + return; + } + + //Dropbox documentation states there are no errors for this API method + Common::JSONObject info = json->asObject(); + if (Networking::CurlJsonRequest::jsonContainsAttribute(info, "name", "DropboxInfoRequest") && + Networking::CurlJsonRequest::jsonIsObject(info.getVal("name"), "DropboxInfoRequest")) { + Common::JSONObject nameInfo = info.getVal("name")->asObject(); + if (Networking::CurlJsonRequest::jsonContainsString(nameInfo, "display_name", "DropboxInfoRequest")) { + _name = nameInfo.getVal("display_name")->asString(); + } + } + if (Networking::CurlJsonRequest::jsonContainsString(info, "account_id", "DropboxInfoRequest")) { + _uid = info.getVal("account_id")->asString(); + } + if (Networking::CurlJsonRequest::jsonContainsString(info, "email", "DropboxInfoRequest")) { + _email = info.getVal("email")->asString(); + } + CloudMan.setStorageUsername(kStorageDropboxId, _email); + delete json; + + Networking::JsonCallback innerCallback = new Common::Callback<DropboxInfoRequest, Networking::JsonResponse>(this, &DropboxInfoRequest::quotaResponseCallback); + Networking::ErrorCallback errorCallback = new Common::Callback<DropboxInfoRequest, Networking::ErrorResponse>(this, &DropboxInfoRequest::errorCallback); + Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(innerCallback, errorCallback, DROPBOX_API_GET_SPACE_USAGE); + request->addHeader("Authorization: Bearer " + _token); + request->addHeader("Content-Type: application/json"); + request->addPostField("null"); //use POST + + _workingRequest = ConnMan.addRequest(request); +} + +void DropboxInfoRequest::quotaResponseCallback(Networking::JsonResponse response) { + Common::JSONValue *json = response.value; + _workingRequest = nullptr; + if (_ignoreCallback) { + delete json; + return; + } + + Networking::ErrorResponse error(this); + Networking::CurlJsonRequest *rq = (Networking::CurlJsonRequest *)response.request; + if (rq && rq->getNetworkReadStream()) + error.httpResponseCode = rq->getNetworkReadStream()->httpResponseCode(); + + if (json == nullptr) { + error.response = "Failed to parse JSON, null passed!"; + finishError(error); + return; + } + + if (!json->isObject()) { + error.response = "Passed JSON is not an object!"; + finishError(error); + delete json; + return; + } + + //Dropbox documentation states there are no errors for this API method + Common::JSONObject info = json->asObject(); + + if (!Networking::CurlJsonRequest::jsonContainsIntegerNumber(info, "used", "DropboxInfoRequest")) { + error.response = "Passed JSON misses 'used' attribute!"; + finishError(error); + delete json; + return; + } + + uint64 used = info.getVal("used")->asIntegerNumber(), allocated = 0; + + if (Networking::CurlJsonRequest::jsonContainsAttribute(info, "allocation", "DropboxInfoRequest") && + Networking::CurlJsonRequest::jsonIsObject(info.getVal("allocation"), "DropboxInfoRequest")) { + Common::JSONObject allocation = info.getVal("allocation")->asObject(); + if (!Networking::CurlJsonRequest::jsonContainsIntegerNumber(allocation, "allocated", "DropboxInfoRequest")) { + error.response = "Passed JSON misses 'allocation/allocated' attribute!"; + finishError(error); + delete json; + return; + } + + allocated = allocation.getVal("allocated")->asIntegerNumber(); + } + + finishInfo(StorageInfo(_uid, _name, _email, used, allocated)); + delete json; +} + +void DropboxInfoRequest::errorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) return; + finishError(error); +} + +void DropboxInfoRequest::handle() {} + +void DropboxInfoRequest::restart() { start(); } + +void DropboxInfoRequest::finishInfo(StorageInfo info) { + Request::finishSuccess(); + if (_infoCallback) + (*_infoCallback)(Storage::StorageInfoResponse(this, info)); +} + +} // End of namespace Dropbox +} // End of namespace Cloud diff --git a/backends/cloud/dropbox/dropboxinforequest.h b/backends/cloud/dropbox/dropboxinforequest.h new file mode 100644 index 0000000000..68a3e135de --- /dev/null +++ b/backends/cloud/dropbox/dropboxinforequest.h @@ -0,0 +1,56 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_DROPBOX_DROPBOXINFOREQUEST_H +#define BACKENDS_CLOUD_DROPBOX_DROPBOXINFOREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/request.h" +#include "backends/networking/curl/curljsonrequest.h" + +namespace Cloud { +namespace Dropbox { + +class DropboxInfoRequest: public Networking::Request { + Common::String _token; + Common::String _uid, _name, _email; + Storage::StorageInfoCallback _infoCallback; + Request *_workingRequest; + bool _ignoreCallback; + + void start(); + void userResponseCallback(Networking::JsonResponse response); + void quotaResponseCallback(Networking::JsonResponse response); + void errorCallback(Networking::ErrorResponse error); + void finishInfo(StorageInfo info); +public: + DropboxInfoRequest(Common::String token, Storage::StorageInfoCallback cb, Networking::ErrorCallback ecb); + virtual ~DropboxInfoRequest(); + + virtual void handle(); + virtual void restart(); +}; + +} // End of namespace Dropbox +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/dropbox/dropboxlistdirectoryrequest.cpp b/backends/cloud/dropbox/dropboxlistdirectoryrequest.cpp new file mode 100644 index 0000000000..8b00f7c2bf --- /dev/null +++ b/backends/cloud/dropbox/dropboxlistdirectoryrequest.cpp @@ -0,0 +1,222 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/cloud/dropbox/dropboxlistdirectoryrequest.h" +#include "backends/cloud/iso8601.h" +#include "backends/cloud/storage.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/json.h" + +namespace Cloud { +namespace Dropbox { + +#define DROPBOX_API_LIST_FOLDER "https://api.dropboxapi.com/2/files/list_folder" +#define DROPBOX_API_LIST_FOLDER_CONTINUE "https://api.dropboxapi.com/2/files/list_folder/continue" + +DropboxListDirectoryRequest::DropboxListDirectoryRequest(Common::String token, Common::String path, Storage::ListDirectoryCallback cb, Networking::ErrorCallback ecb, bool recursive): + Networking::Request(nullptr, ecb), _requestedPath(path), _requestedRecursive(recursive), _listDirectoryCallback(cb), + _token(token), _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +DropboxListDirectoryRequest::~DropboxListDirectoryRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _listDirectoryCallback; +} + +void DropboxListDirectoryRequest::start() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _files.clear(); + _ignoreCallback = false; + + Networking::JsonCallback callback = new Common::Callback<DropboxListDirectoryRequest, Networking::JsonResponse>(this, &DropboxListDirectoryRequest::responseCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<DropboxListDirectoryRequest, Networking::ErrorResponse>(this, &DropboxListDirectoryRequest::errorCallback); + Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(callback, failureCallback, DROPBOX_API_LIST_FOLDER); + request->addHeader("Authorization: Bearer " + _token); + request->addHeader("Content-Type: application/json"); + + Common::JSONObject jsonRequestParameters; + jsonRequestParameters.setVal("path", new Common::JSONValue(_requestedPath)); + jsonRequestParameters.setVal("recursive", new Common::JSONValue(_requestedRecursive)); + jsonRequestParameters.setVal("include_media_info", new Common::JSONValue(false)); + jsonRequestParameters.setVal("include_deleted", new Common::JSONValue(false)); + + Common::JSONValue value(jsonRequestParameters); + request->addPostField(Common::JSON::stringify(&value)); + + _workingRequest = ConnMan.addRequest(request); +} + +void DropboxListDirectoryRequest::responseCallback(Networking::JsonResponse response) { + _workingRequest = nullptr; + + if (_ignoreCallback) { + delete response.value; + return; + } + + if (response.request) + _date = response.request->date(); + + Networking::ErrorResponse error(this); + Networking::CurlJsonRequest *rq = (Networking::CurlJsonRequest *)response.request; + if (rq && rq->getNetworkReadStream()) + error.httpResponseCode = rq->getNetworkReadStream()->httpResponseCode(); + + Common::JSONValue *json = response.value; + if (json == nullptr) { + error.response = "Failed to parse JSON, null passed!"; + finishError(error); + return; + } + + if (!json->isObject()) { + error.response = "Passed JSON is not an object!"; + finishError(error); + delete json; + return; + } + + Common::JSONObject responseObject = json->asObject(); + + if (responseObject.contains("error") || responseObject.contains("error_summary")) { + if (responseObject.contains("error_summary") && responseObject.getVal("error_summary")->isString()) { + warning("Dropbox returned error: %s", responseObject.getVal("error_summary")->asString().c_str()); + } + error.failed = true; + error.response = json->stringify(); + finishError(error); + delete json; + return; + } + + //check that ALL keys exist AND HAVE RIGHT TYPE to avoid segfaults + if (responseObject.contains("entries")) { + if (!responseObject.getVal("entries")->isArray()) { + error.response = Common::String::format( + "\"entries\" found, but that's not an array!\n%s", + responseObject.getVal("entries")->stringify(true).c_str() + ); + finishError(error); + delete json; + return; + } + + Common::JSONArray items = responseObject.getVal("entries")->asArray(); + for (uint32 i = 0; i < items.size(); ++i) { + if (!Networking::CurlJsonRequest::jsonIsObject(items[i], "DropboxListDirectoryRequest")) + continue; + + Common::JSONObject item = items[i]->asObject(); + + if (!Networking::CurlJsonRequest::jsonContainsString(item, "path_lower", "DropboxListDirectoryRequest")) + continue; + if (!Networking::CurlJsonRequest::jsonContainsString(item, ".tag", "DropboxListDirectoryRequest")) + continue; + + Common::String path = item.getVal("path_lower")->asString(); + bool isDirectory = (item.getVal(".tag")->asString() == "folder"); + uint32 size = 0, timestamp = 0; + if (!isDirectory) { + if (!Networking::CurlJsonRequest::jsonContainsString(item, "server_modified", "DropboxListDirectoryRequest")) + continue; + if (!Networking::CurlJsonRequest::jsonContainsIntegerNumber(item, "size", "DropboxListDirectoryRequest")) + continue; + + size = item.getVal("size")->asIntegerNumber(); + timestamp = ISO8601::convertToTimestamp(item.getVal("server_modified")->asString()); + } + _files.push_back(StorageFile(path, size, timestamp, isDirectory)); + } + } + + bool hasMore = false; + if (responseObject.contains("has_more")) { + if (!responseObject.getVal("has_more")->isBool()) { + warning("DropboxListDirectoryRequest: \"has_more\" is not a boolean"); + debug(9, "%s", responseObject.getVal("has_more")->stringify(true).c_str()); + error.response = "\"has_more\" is not a boolean!"; + finishError(error); + delete json; + return; + } + + hasMore = responseObject.getVal("has_more")->asBool(); + } + + if (hasMore) { + if (!Networking::CurlJsonRequest::jsonContainsString(responseObject, "cursor", "DropboxListDirectoryRequest")) { + error.response = "\"has_more\" found, but \"cursor\" is not (or it's not a string)!"; + finishError(error); + delete json; + return; + } + + Networking::JsonCallback callback = new Common::Callback<DropboxListDirectoryRequest, Networking::JsonResponse>(this, &DropboxListDirectoryRequest::responseCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<DropboxListDirectoryRequest, Networking::ErrorResponse>(this, &DropboxListDirectoryRequest::errorCallback); + Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(callback, failureCallback, DROPBOX_API_LIST_FOLDER_CONTINUE); + request->addHeader("Authorization: Bearer " + _token); + request->addHeader("Content-Type: application/json"); + + Common::JSONObject jsonRequestParameters; + jsonRequestParameters.setVal("cursor", new Common::JSONValue(responseObject.getVal("cursor")->asString())); + + Common::JSONValue value(jsonRequestParameters); + request->addPostField(Common::JSON::stringify(&value)); + + _workingRequest = ConnMan.addRequest(request); + } else { + finishListing(_files); + } + + delete json; +} + +void DropboxListDirectoryRequest::errorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (error.request) + _date = error.request->date(); + finishError(error); +} + +void DropboxListDirectoryRequest::handle() {} + +void DropboxListDirectoryRequest::restart() { start(); } + +Common::String DropboxListDirectoryRequest::date() const { return _date; } + +void DropboxListDirectoryRequest::finishListing(Common::Array<StorageFile> &files) { + Request::finishSuccess(); + if (_listDirectoryCallback) + (*_listDirectoryCallback)(Storage::ListDirectoryResponse(this, files)); +} + +} // End of namespace Dropbox +} // End of namespace Cloud diff --git a/backends/cloud/dropbox/dropboxlistdirectoryrequest.h b/backends/cloud/dropbox/dropboxlistdirectoryrequest.h new file mode 100644 index 0000000000..5c0d8dfa21 --- /dev/null +++ b/backends/cloud/dropbox/dropboxlistdirectoryrequest.h @@ -0,0 +1,61 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_DROPBOX_DROPBOXLISTDIRECTORYREQUEST_H +#define BACKENDS_CLOUD_DROPBOX_DROPBOXLISTDIRECTORYREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/request.h" +#include "common/callback.h" +#include "backends/networking/curl/curljsonrequest.h" + +namespace Cloud { +namespace Dropbox { + +class DropboxListDirectoryRequest: public Networking::Request { + Common::String _requestedPath; + bool _requestedRecursive; + + Storage::ListDirectoryCallback _listDirectoryCallback; + Common::String _token; + Common::Array<StorageFile> _files; + Request *_workingRequest; + bool _ignoreCallback; + Common::String _date; + + void start(); + void responseCallback(Networking::JsonResponse response); + void errorCallback(Networking::ErrorResponse error); + void finishListing(Common::Array<StorageFile> &files); +public: + DropboxListDirectoryRequest(Common::String token, Common::String path, Storage::ListDirectoryCallback cb, Networking::ErrorCallback ecb, bool recursive = false); + virtual ~DropboxListDirectoryRequest(); + + virtual void handle(); + virtual void restart(); + virtual Common::String date() const; +}; + +} // End of namespace Dropbox +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/dropbox/dropboxstorage.cpp b/backends/cloud/dropbox/dropboxstorage.cpp new file mode 100644 index 0000000000..d12070316b --- /dev/null +++ b/backends/cloud/dropbox/dropboxstorage.cpp @@ -0,0 +1,198 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ +#define FORBIDDEN_SYMBOL_ALLOW_ALL + +#include "backends/cloud/dropbox/dropboxstorage.h" +#include "backends/cloud/dropbox/dropboxcreatedirectoryrequest.h" +#include "backends/cloud/dropbox/dropboxinforequest.h" +#include "backends/cloud/dropbox/dropboxlistdirectoryrequest.h" +#include "backends/cloud/dropbox/dropboxuploadrequest.h" +#include "backends/cloud/cloudmanager.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "common/config-manager.h" +#include "common/debug.h" +#include "common/json.h" +#include <curl/curl.h> + +#ifdef ENABLE_RELEASE +#include "dists/clouds/cloud_keys.h" +#endif + +namespace Cloud { +namespace Dropbox { + +#define DROPBOX_OAUTH2_TOKEN "https://api.dropboxapi.com/oauth2/token" +#define DROPBOX_API_FILES_DOWNLOAD "https://content.dropboxapi.com/2/files/download" + +char *DropboxStorage::KEY = nullptr; //can't use CloudConfig there yet, loading it on instance creation/auth +char *DropboxStorage::SECRET = nullptr; + +void DropboxStorage::loadKeyAndSecret() { +#ifdef ENABLE_RELEASE + KEY = RELEASE_DROPBOX_KEY; + SECRET = RELEASE_DROPBOX_SECRET; +#else + Common::String k = ConfMan.get("DROPBOX_KEY", ConfMan.kCloudDomain); + KEY = new char[k.size() + 1]; + memcpy(KEY, k.c_str(), k.size()); + KEY[k.size()] = 0; + + k = ConfMan.get("DROPBOX_SECRET", ConfMan.kCloudDomain); + SECRET = new char[k.size() + 1]; + memcpy(SECRET, k.c_str(), k.size()); + SECRET[k.size()] = 0; +#endif +} + +DropboxStorage::DropboxStorage(Common::String accessToken, Common::String userId): _token(accessToken), _uid(userId) {} + +DropboxStorage::DropboxStorage(Common::String code) { + getAccessToken(code); +} + +DropboxStorage::~DropboxStorage() {} + +void DropboxStorage::getAccessToken(Common::String code) { + if (!KEY || !SECRET) + loadKeyAndSecret(); + Networking::JsonCallback callback = new Common::Callback<DropboxStorage, Networking::JsonResponse>(this, &DropboxStorage::codeFlowComplete); + Networking::ErrorCallback errorCallback = new Common::Callback<DropboxStorage, Networking::ErrorResponse>(this, &DropboxStorage::codeFlowFailed); + Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(callback, errorCallback, DROPBOX_OAUTH2_TOKEN); + request->addPostField("code=" + code); + request->addPostField("grant_type=authorization_code"); + request->addPostField("client_id=" + Common::String(KEY)); + request->addPostField("client_secret=" + Common::String(SECRET)); + if (Cloud::CloudManager::couldUseLocalServer()) { + request->addPostField("&redirect_uri=http%3A%2F%2Flocalhost%3A12345%2F"); + } else { + request->addPostField("&redirect_uri=https%3A%2F%2Fwww.scummvm.org/c/code"); + } + addRequest(request); +} + +void DropboxStorage::codeFlowComplete(Networking::JsonResponse response) { + Common::JSONValue *json = (Common::JSONValue *)response.value; + if (json == nullptr) { + debug(9, "DropboxStorage::codeFlowComplete: got NULL instead of JSON!"); + CloudMan.removeStorage(this); + return; + } + + if (!json->isObject()) { + debug(9, "DropboxStorage::codeFlowComplete: Passed JSON is not an object!"); + CloudMan.removeStorage(this); + delete json; + return; + } + + Common::JSONObject result = json->asObject(); + if (!Networking::CurlJsonRequest::jsonContainsString(result, "access_token", "DropboxStorage::codeFlowComplete") || + !Networking::CurlJsonRequest::jsonContainsString(result, "uid", "DropboxStorage::codeFlowComplete")) { + warning("DropboxStorage: bad response, no token/uid passed"); + debug(9, "%s", json->stringify(true).c_str()); + CloudMan.removeStorage(this); + } else { + _token = result.getVal("access_token")->asString(); + _uid = result.getVal("uid")->asString(); + ConfMan.removeKey("dropbox_code", ConfMan.kCloudDomain); + CloudMan.replaceStorage(this, kStorageDropboxId); + ConfMan.flushToDisk(); + } + + delete json; +} + +void DropboxStorage::codeFlowFailed(Networking::ErrorResponse error) { + debug(9, "DropboxStorage: code flow failed (%s, %ld):", (error.failed ? "failed" : "interrupted"), error.httpResponseCode); + debug(9, "%s", error.response.c_str()); + CloudMan.removeStorage(this); +} + +void DropboxStorage::saveConfig(Common::String keyPrefix) { + ConfMan.set(keyPrefix + "access_token", _token, ConfMan.kCloudDomain); + ConfMan.set(keyPrefix + "user_id", _uid, ConfMan.kCloudDomain); +} + +Common::String DropboxStorage::name() const { + return "Dropbox"; +} + +Networking::Request *DropboxStorage::listDirectory(Common::String path, ListDirectoryCallback outerCallback, Networking::ErrorCallback errorCallback, bool recursive) { + return addRequest(new DropboxListDirectoryRequest(_token, path, outerCallback, errorCallback, recursive)); +} + +Networking::Request *DropboxStorage::upload(Common::String path, Common::SeekableReadStream *contents, UploadCallback callback, Networking::ErrorCallback errorCallback) { + return addRequest(new DropboxUploadRequest(_token, path, contents, callback, errorCallback)); +} + +Networking::Request *DropboxStorage::streamFileById(Common::String path, Networking::NetworkReadStreamCallback callback, Networking::ErrorCallback errorCallback) { + Common::JSONObject jsonRequestParameters; + jsonRequestParameters.setVal("path", new Common::JSONValue(path)); + Common::JSONValue value(jsonRequestParameters); + + Networking::CurlRequest *request = new Networking::CurlRequest(nullptr, nullptr, DROPBOX_API_FILES_DOWNLOAD); //TODO: is it OK to pass no callbacks? + request->addHeader("Authorization: Bearer " + _token); + request->addHeader("Dropbox-API-Arg: " + Common::JSON::stringify(&value)); + request->addHeader("Content-Type: "); //required to be empty (as we do POST, it's usually app/form-url-encoded) + + Networking::NetworkReadStreamResponse response = request->execute(); + if (callback) + (*callback)(response); + return response.request; // no leak here, response.request == request +} + +Networking::Request *DropboxStorage::createDirectory(Common::String path, BoolCallback callback, Networking::ErrorCallback errorCallback) { + if (!errorCallback) + errorCallback = getErrorPrintingCallback(); + return addRequest(new DropboxCreateDirectoryRequest(_token, path, callback, errorCallback)); +} + +Networking::Request *DropboxStorage::info(StorageInfoCallback callback, Networking::ErrorCallback errorCallback) { + if (!errorCallback) + errorCallback = getErrorPrintingCallback(); + return addRequest(new DropboxInfoRequest(_token, callback, errorCallback)); +} + +Common::String DropboxStorage::savesDirectoryPath() { return "/saves/"; } + +DropboxStorage *DropboxStorage::loadFromConfig(Common::String keyPrefix) { + loadKeyAndSecret(); + + if (!ConfMan.hasKey(keyPrefix + "access_token", ConfMan.kCloudDomain)) { + warning("DropboxStorage: no access_token found"); + return nullptr; + } + + if (!ConfMan.hasKey(keyPrefix + "user_id", ConfMan.kCloudDomain)) { + warning("DropboxStorage: no user_id found"); + return nullptr; + } + + Common::String accessToken = ConfMan.get(keyPrefix + "access_token", ConfMan.kCloudDomain); + Common::String userId = ConfMan.get(keyPrefix + "user_id", ConfMan.kCloudDomain); + + return new DropboxStorage(accessToken, userId); +} + +} // End of namespace Dropbox +} // End of namespace Cloud diff --git a/backends/cloud/dropbox/dropboxstorage.h b/backends/cloud/dropbox/dropboxstorage.h new file mode 100644 index 0000000000..0a0043abee --- /dev/null +++ b/backends/cloud/dropbox/dropboxstorage.h @@ -0,0 +1,101 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_DROPBOX_STORAGE_H +#define BACKENDS_CLOUD_DROPBOX_STORAGE_H + +#include "backends/cloud/storage.h" +#include "common/callback.h" +#include "backends/networking/curl/curljsonrequest.h" + +namespace Cloud { +namespace Dropbox { + +class DropboxStorage: public Cloud::Storage { + static char *KEY, *SECRET; + + static void loadKeyAndSecret(); + + Common::String _token, _uid; + + /** This private constructor is called from loadFromConfig(). */ + DropboxStorage(Common::String token, Common::String uid); + + void getAccessToken(Common::String code); + void codeFlowComplete(Networking::JsonResponse response); + void codeFlowFailed(Networking::ErrorResponse error); + +public: + /** This constructor uses OAuth code flow to get tokens. */ + DropboxStorage(Common::String code); + virtual ~DropboxStorage(); + + /** + * Storage methods, which are used by CloudManager to save + * storage in configuration file. + */ + + /** + * Save storage data using ConfMan. + * @param keyPrefix all saved keys must start with this prefix. + * @note every Storage must write keyPrefix + "type" key + * with common value (e.g. "Dropbox"). + */ + virtual void saveConfig(Common::String keyPrefix); + + /** + * Return unique storage name. + * @returns some unique storage name (for example, "Dropbox (user@example.com)") + */ + virtual Common::String name() const; + + /** Public Cloud API comes down there. */ + + /** Returns ListDirectoryStatus struct with list of files. */ + virtual Networking::Request *listDirectory(Common::String path, ListDirectoryCallback callback, Networking::ErrorCallback errorCallback, bool recursive = false); + + /** Returns UploadStatus struct with info about uploaded file. */ + virtual Networking::Request *upload(Common::String path, Common::SeekableReadStream *contents, UploadCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns pointer to Networking::NetworkReadStream. */ + virtual Networking::Request *streamFileById(Common::String path, Networking::NetworkReadStreamCallback callback, Networking::ErrorCallback errorCallback); + + /** Calls the callback when finished. */ + virtual Networking::Request *createDirectory(Common::String path, BoolCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns the StorageInfo struct. */ + virtual Networking::Request *info(StorageInfoCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns storage's saves directory path with the trailing slash. */ + virtual Common::String savesDirectoryPath(); + + /** + * Load token and user id from configs and return DropboxStorage for those. + * @return pointer to the newly created DropboxStorage or 0 if some problem occured. + */ + static DropboxStorage *loadFromConfig(Common::String keyPrefix); +}; + +} // End of namespace Dropbox +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/dropbox/dropboxuploadrequest.cpp b/backends/cloud/dropbox/dropboxuploadrequest.cpp new file mode 100644 index 0000000000..2c9dcc4109 --- /dev/null +++ b/backends/cloud/dropbox/dropboxuploadrequest.cpp @@ -0,0 +1,204 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/cloud/dropbox/dropboxuploadrequest.h" +#include "backends/cloud/iso8601.h" +#include "backends/cloud/storage.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/json.h" + +namespace Cloud { +namespace Dropbox { + +#define DROPBOX_API_FILES_UPLOAD "https://content.dropboxapi.com/2/files/upload" +#define DROPBOX_API_FILES_UPLOAD_SESSION "https://content.dropboxapi.com/2/files/upload_session/" + +DropboxUploadRequest::DropboxUploadRequest(Common::String token, Common::String path, Common::SeekableReadStream *contents, Storage::UploadCallback callback, Networking::ErrorCallback ecb): + Networking::Request(nullptr, ecb), _token(token), _savePath(path), _contentsStream(contents), _uploadCallback(callback), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +DropboxUploadRequest::~DropboxUploadRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _contentsStream; + delete _uploadCallback; +} + +void DropboxUploadRequest::start() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + if (!_contentsStream) { + warning("DropboxUploadRequest: cannot start because stream is invalid"); + finishError(Networking::ErrorResponse(this, false, true, "", -1)); + return; + } + if (!_contentsStream->seek(0)) { + warning("DropboxUploadRequest: cannot restart because stream couldn't seek(0)"); + finishError(Networking::ErrorResponse(this, false, true, "", -1)); + return; + } + _ignoreCallback = false; + + uploadNextPart(); +} + +void DropboxUploadRequest::uploadNextPart() { + const uint32 UPLOAD_PER_ONE_REQUEST = 10 * 1024 * 1024; + + Common::String url = DROPBOX_API_FILES_UPLOAD_SESSION; + Common::JSONObject jsonRequestParameters; + + if (_contentsStream->pos() == 0 || _sessionId == "") { + if ((uint32)_contentsStream->size() <= UPLOAD_PER_ONE_REQUEST) { + url = DROPBOX_API_FILES_UPLOAD; + jsonRequestParameters.setVal("path", new Common::JSONValue(_savePath)); + jsonRequestParameters.setVal("mode", new Common::JSONValue("overwrite")); + jsonRequestParameters.setVal("autorename", new Common::JSONValue(false)); + jsonRequestParameters.setVal("mute", new Common::JSONValue(false)); + } else { + url += "start"; + jsonRequestParameters.setVal("close", new Common::JSONValue(false)); + } + } else { + if ((uint32)(_contentsStream->size() - _contentsStream->pos()) <= UPLOAD_PER_ONE_REQUEST) { + url += "finish"; + Common::JSONObject jsonCursor, jsonCommit; + jsonCursor.setVal("session_id", new Common::JSONValue(_sessionId)); + jsonCursor.setVal("offset", new Common::JSONValue((long long int)_contentsStream->pos())); + jsonCommit.setVal("path", new Common::JSONValue(_savePath)); + jsonCommit.setVal("mode", new Common::JSONValue("overwrite")); + jsonCommit.setVal("autorename", new Common::JSONValue(false)); + jsonCommit.setVal("mute", new Common::JSONValue(false)); + jsonRequestParameters.setVal("cursor", new Common::JSONValue(jsonCursor)); + jsonRequestParameters.setVal("commit", new Common::JSONValue(jsonCommit)); + } else { + url += "append_v2"; + Common::JSONObject jsonCursor; + jsonCursor.setVal("session_id", new Common::JSONValue(_sessionId)); + jsonCursor.setVal("offset", new Common::JSONValue((long long int)_contentsStream->pos())); + jsonRequestParameters.setVal("cursor", new Common::JSONValue(jsonCursor)); + jsonRequestParameters.setVal("close", new Common::JSONValue(false)); + } + } + + Common::JSONValue value(jsonRequestParameters); + Networking::JsonCallback callback = new Common::Callback<DropboxUploadRequest, Networking::JsonResponse>(this, &DropboxUploadRequest::partUploadedCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<DropboxUploadRequest, Networking::ErrorResponse>(this, &DropboxUploadRequest::partUploadedErrorCallback); + Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(callback, failureCallback, url); + request->addHeader("Authorization: Bearer " + _token); + request->addHeader("Content-Type: application/octet-stream"); + request->addHeader("Dropbox-API-Arg: " + Common::JSON::stringify(&value)); + + byte *buffer = new byte[UPLOAD_PER_ONE_REQUEST]; + uint32 size = _contentsStream->read(buffer, UPLOAD_PER_ONE_REQUEST); + request->setBuffer(buffer, size); + + _workingRequest = ConnMan.addRequest(request); +} + +void DropboxUploadRequest::partUploadedCallback(Networking::JsonResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + Networking::ErrorResponse error(this, false, true, "", -1); + Networking::CurlJsonRequest *rq = (Networking::CurlJsonRequest *)response.request; + if (rq && rq->getNetworkReadStream()) + error.httpResponseCode = rq->getNetworkReadStream()->httpResponseCode(); + + Common::JSONValue *json = response.value; + if (json == nullptr) { + error.response = "Failed to parse JSON, null passed!"; + finishError(error); + return; + } + + bool needsFinishRequest = false; + + if (json->isObject()) { + Common::JSONObject object = json->asObject(); + + //debug(9, "%s", json->stringify(true).c_str()); + + if (object.contains("error") || object.contains("error_summary")) { + if (Networking::CurlJsonRequest::jsonContainsString(object, "error_summary", "DropboxUploadRequest")) { + warning("Dropbox returned error: %s", object.getVal("error_summary")->asString().c_str()); + } + error.response = json->stringify(true); + finishError(error); + delete json; + return; + } + + if (Networking::CurlJsonRequest::jsonContainsString(object, "path_lower", "DropboxUploadRequest") && + Networking::CurlJsonRequest::jsonContainsString(object, "server_modified", "DropboxUploadRequest") && + Networking::CurlJsonRequest::jsonContainsIntegerNumber(object, "size", "DropboxUploadRequest")) { + //finished + Common::String path = object.getVal("path_lower")->asString(); + uint32 size = object.getVal("size")->asIntegerNumber(); + uint32 timestamp = ISO8601::convertToTimestamp(object.getVal("server_modified")->asString()); + finishUpload(StorageFile(path, size, timestamp, false)); + return; + } + + if (_sessionId == "") { + if (Networking::CurlJsonRequest::jsonContainsString(object, "session_id", "DropboxUploadRequest")) + _sessionId = object.getVal("session_id")->asString(); + needsFinishRequest = true; + } + } + + if (!needsFinishRequest && (_contentsStream->eos() || _contentsStream->pos() >= _contentsStream->size() - 1)) { + warning("DropboxUploadRequest: no file info to return"); + finishUpload(StorageFile(_savePath, 0, 0, false)); + } else { + uploadNextPart(); + } + + delete json; +} + +void DropboxUploadRequest::partUploadedErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + finishError(error); +} + +void DropboxUploadRequest::handle() {} + +void DropboxUploadRequest::restart() { start(); } + +void DropboxUploadRequest::finishUpload(StorageFile file) { + Request::finishSuccess(); + if (_uploadCallback) + (*_uploadCallback)(Storage::UploadResponse(this, file)); +} + +} // End of namespace Dropbox +} // End of namespace Cloud diff --git a/backends/cloud/dropbox/dropboxuploadrequest.h b/backends/cloud/dropbox/dropboxuploadrequest.h new file mode 100644 index 0000000000..5adf5a6df2 --- /dev/null +++ b/backends/cloud/dropbox/dropboxuploadrequest.h @@ -0,0 +1,60 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_DROPBOX_DROPBOXUPLOADREQUEST_H +#define BACKENDS_CLOUD_DROPBOX_DROPBOXUPLOADREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/request.h" +#include "common/callback.h" + +namespace Cloud { +namespace Dropbox { + +class DropboxUploadRequest: public Networking::Request { + Common::String _token; + Common::String _savePath; + Common::SeekableReadStream *_contentsStream; + Storage::UploadCallback _uploadCallback; + Request *_workingRequest; + bool _ignoreCallback; + Common::String _sessionId; + + void start(); + void uploadNextPart(); + void partUploadedCallback(Networking::JsonResponse response); + void partUploadedErrorCallback(Networking::ErrorResponse error); + void finishUpload(StorageFile status); + +public: + DropboxUploadRequest(Common::String token, Common::String path, Common::SeekableReadStream *contents, Storage::UploadCallback callback, Networking::ErrorCallback ecb); + virtual ~DropboxUploadRequest(); + + virtual void handle(); + virtual void restart(); +}; + +} // End of namespace Dropbox +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/folderdownloadrequest.cpp b/backends/cloud/folderdownloadrequest.cpp new file mode 100644 index 0000000000..7eeee0c6d6 --- /dev/null +++ b/backends/cloud/folderdownloadrequest.cpp @@ -0,0 +1,197 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/cloud/folderdownloadrequest.h" +#include "backends/cloud/downloadrequest.h" +#include "backends/cloud/id/iddownloadrequest.h" +#include "common/debug.h" +#include "gui/downloaddialog.h" +#include <backends/networking/curl/connectionmanager.h> + +namespace Cloud { + +FolderDownloadRequest::FolderDownloadRequest(Storage *storage, Storage::FileArrayCallback callback, Networking::ErrorCallback ecb, Common::String remoteDirectoryPath, Common::String localDirectoryPath, bool recursive): + Request(nullptr, ecb), CommandSender(nullptr), _storage(storage), _fileArrayCallback(callback), + _remoteDirectoryPath(remoteDirectoryPath), _localDirectoryPath(localDirectoryPath), _recursive(recursive), + _workingRequest(nullptr), _ignoreCallback(false), _totalFiles(0) { + start(); +} + +FolderDownloadRequest::~FolderDownloadRequest() { + sendCommand(GUI::kDownloadEndedCmd, 0); + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _fileArrayCallback; +} + +void FolderDownloadRequest::start() { + //cleanup + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _currentFile = StorageFile(); + _pendingFiles.clear(); + _failedFiles.clear(); + _ignoreCallback = false; + _totalFiles = 0; + _downloadedBytes = _totalBytes = _wasDownloadedBytes = _currentDownloadSpeed = 0; + + //list directory first + _workingRequest = _storage->listDirectory( + _remoteDirectoryPath, + new Common::Callback<FolderDownloadRequest, Storage::ListDirectoryResponse>(this, &FolderDownloadRequest::directoryListedCallback), + new Common::Callback<FolderDownloadRequest, Networking::ErrorResponse>(this, &FolderDownloadRequest::directoryListedErrorCallback), + _recursive + ); +} + +void FolderDownloadRequest::directoryListedCallback(Storage::ListDirectoryResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + _pendingFiles = response.value; + + // remove all directories + // non-empty directories would be created by DumpFile, and empty ones are just ignored + for (Common::Array<StorageFile>::iterator i = _pendingFiles.begin(); i != _pendingFiles.end();) + if (i->isDirectory()) + _pendingFiles.erase(i); + else { + _totalBytes += i->size(); + ++i; + } + + _totalFiles = _pendingFiles.size(); + downloadNextFile(); +} + +void FolderDownloadRequest::directoryListedErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + finishError(error); +} + +void FolderDownloadRequest::fileDownloadedCallback(Storage::BoolResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (!response.value) _failedFiles.push_back(_currentFile); + _downloadedBytes += _currentFile.size(); + downloadNextFile(); +} + +void FolderDownloadRequest::fileDownloadedErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + fileDownloadedCallback(Storage::BoolResponse(error.request, false)); +} + +void FolderDownloadRequest::downloadNextFile() { + do { + if (_pendingFiles.empty()) { + sendCommand(GUI::kDownloadEndedCmd, 0); + finishDownload(_failedFiles); + return; + } + + _currentFile = _pendingFiles.back(); + _pendingFiles.pop_back(); + } while (_currentFile.isDirectory()); // directories are actually removed earlier, in the directoryListedCallback() + + sendCommand(GUI::kDownloadProgressCmd, (int)(getProgress() * 100)); + + Common::String remotePath = _currentFile.path(); + Common::String localPath = remotePath; + if (_remoteDirectoryPath == "" || remotePath.hasPrefix(_remoteDirectoryPath)) { + localPath.erase(0, _remoteDirectoryPath.size()); + if (_remoteDirectoryPath != "" && (_remoteDirectoryPath.lastChar() != '/' && _remoteDirectoryPath.lastChar() != '\\')) + localPath.erase(0, 1); + } else { + warning("FolderDownloadRequest: Can't process the following paths:"); + warning("remote directory: %s", _remoteDirectoryPath.c_str()); + warning("remote file under that directory: %s", remotePath.c_str()); + } + if (_localDirectoryPath != "") { + if (_localDirectoryPath.lastChar() == '/' || _localDirectoryPath.lastChar() == '\\') + localPath = _localDirectoryPath + localPath; + else + localPath = _localDirectoryPath + "/" + localPath; + } + debug(9, "FolderDownloadRequest: %s -> %s", remotePath.c_str(), localPath.c_str()); + _workingRequest = _storage->downloadById( + _currentFile.id(), localPath, + new Common::Callback<FolderDownloadRequest, Storage::BoolResponse>(this, &FolderDownloadRequest::fileDownloadedCallback), + new Common::Callback<FolderDownloadRequest, Networking::ErrorResponse>(this, &FolderDownloadRequest::fileDownloadedErrorCallback) + ); +} + +void FolderDownloadRequest::handle() { + uint32 microsecondsPassed = Networking::ConnectionManager::getCloudRequestsPeriodInMicroseconds(); + uint64 currentDownloadedBytes = getDownloadedBytes(); + uint64 downloadedThisPeriod = currentDownloadedBytes - _wasDownloadedBytes; + _currentDownloadSpeed = downloadedThisPeriod * (1000000L / microsecondsPassed); + _wasDownloadedBytes = currentDownloadedBytes; +} + +void FolderDownloadRequest::restart() { start(); } + +void FolderDownloadRequest::finishDownload(Common::Array<StorageFile> &files) { + Request::finishSuccess(); + if (_fileArrayCallback) + (*_fileArrayCallback)(Storage::FileArrayResponse(this, files)); +} + +double FolderDownloadRequest::getProgress() const { + if (_totalFiles == 0 || _totalBytes == 0) + return 0; + return (double)getDownloadedBytes() / (double)getTotalBytesToDownload(); +} + +uint64 FolderDownloadRequest::getDownloadedBytes() const { + if (_totalFiles == 0) + return 0; + + double currentFileProgress = 0; + DownloadRequest *downloadRequest = dynamic_cast<DownloadRequest *>(_workingRequest); + if (downloadRequest != nullptr) { + currentFileProgress = downloadRequest->getProgress(); + } else { + Id::IdDownloadRequest *idDownloadRequest = dynamic_cast<Id::IdDownloadRequest *>(_workingRequest); + if (idDownloadRequest != nullptr) + currentFileProgress = idDownloadRequest->getProgress(); + } + + return _downloadedBytes + (uint64)(currentFileProgress * _currentFile.size()); +} + +uint64 FolderDownloadRequest::getTotalBytesToDownload() const { + return _totalBytes; +} + +uint64 FolderDownloadRequest::getDownloadSpeed() const { + return _currentDownloadSpeed; +} + +} // End of namespace Cloud diff --git a/backends/cloud/folderdownloadrequest.h b/backends/cloud/folderdownloadrequest.h new file mode 100644 index 0000000000..08fa193e07 --- /dev/null +++ b/backends/cloud/folderdownloadrequest.h @@ -0,0 +1,79 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_FOLDERDOWNLOADREQUEST_H +#define BACKENDS_CLOUD_FOLDERDOWNLOADREQUEST_H + +#include "backends/networking/curl/request.h" +#include "backends/cloud/storage.h" +#include "gui/object.h" + +namespace Cloud { + +class FolderDownloadRequest: public Networking::Request, public GUI::CommandSender { + Storage *_storage; + Storage::FileArrayCallback _fileArrayCallback; + Common::String _remoteDirectoryPath, _localDirectoryPath; + bool _recursive; + Common::Array<StorageFile> _pendingFiles, _failedFiles; + StorageFile _currentFile; + Request *_workingRequest; + bool _ignoreCallback; + uint32 _totalFiles; + uint64 _downloadedBytes, _totalBytes, _wasDownloadedBytes, _currentDownloadSpeed; + + void start(); + void directoryListedCallback(Storage::ListDirectoryResponse response); + void directoryListedErrorCallback(Networking::ErrorResponse error); + void fileDownloadedCallback(Storage::BoolResponse response); + void fileDownloadedErrorCallback(Networking::ErrorResponse error); + void downloadNextFile(); + void finishDownload(Common::Array<StorageFile> &files); +public: + FolderDownloadRequest(Storage *storage, Storage::FileArrayCallback callback, Networking::ErrorCallback ecb, Common::String remoteDirectoryPath, Common::String localDirectoryPath, bool recursive); + virtual ~FolderDownloadRequest(); + + virtual void handle(); + virtual void restart(); + + /** Returns a number in range [0, 1], where 1 is "complete". */ + double getProgress() const; + + /** Returns a number of downloaded bytes. */ + uint64 getDownloadedBytes() const; + + /** Returns a total number of bytes to download. */ + uint64 getTotalBytesToDownload() const; + + /** Returns average download speed for the last second. */ + uint64 getDownloadSpeed() const; + + /** Returns remote directory path. */ + Common::String getRemotePath() const { return _remoteDirectoryPath; } + + /** Returns local directory path. */ + Common::String getLocalPath() const { return _localDirectoryPath; } +}; + +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/googledrive/googledrivelistdirectorybyidrequest.cpp b/backends/cloud/googledrive/googledrivelistdirectorybyidrequest.cpp new file mode 100644 index 0000000000..52611126a0 --- /dev/null +++ b/backends/cloud/googledrive/googledrivelistdirectorybyidrequest.cpp @@ -0,0 +1,163 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/cloud/googledrive/googledrivelistdirectorybyidrequest.h" +#include "backends/cloud/googledrive/googledrivestorage.h" +#include "backends/cloud/iso8601.h" +#include "backends/cloud/storage.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/json.h" +#include "googledrivetokenrefresher.h" + +namespace Cloud { +namespace GoogleDrive { + +#define GOOGLEDRIVE_API_FILES "https://www.googleapis.com/drive/v3/files?spaces=drive&fields=files%28id,mimeType,modifiedTime,name,size%29,nextPageToken&orderBy=folder,name" +//files(id,mimeType,modifiedTime,name,size),nextPageToken + +GoogleDriveListDirectoryByIdRequest::GoogleDriveListDirectoryByIdRequest(GoogleDriveStorage *storage, Common::String id, Storage::ListDirectoryCallback cb, Networking::ErrorCallback ecb): + Networking::Request(nullptr, ecb), _requestedId(id), _storage(storage), _listDirectoryCallback(cb), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +GoogleDriveListDirectoryByIdRequest::~GoogleDriveListDirectoryByIdRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _listDirectoryCallback; +} + +void GoogleDriveListDirectoryByIdRequest::start() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _files.clear(); + _ignoreCallback = false; + + makeRequest(""); +} + +void GoogleDriveListDirectoryByIdRequest::makeRequest(Common::String pageToken) { + Common::String url = GOOGLEDRIVE_API_FILES; + if (pageToken != "") + url += "&pageToken=" + pageToken; + url += "&q=%27" + _requestedId + "%27+in+parents"; + + Networking::JsonCallback callback = new Common::Callback<GoogleDriveListDirectoryByIdRequest, Networking::JsonResponse>(this, &GoogleDriveListDirectoryByIdRequest::responseCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<GoogleDriveListDirectoryByIdRequest, Networking::ErrorResponse>(this, &GoogleDriveListDirectoryByIdRequest::errorCallback); + Networking::CurlJsonRequest *request = new GoogleDriveTokenRefresher(_storage, callback, failureCallback, url.c_str()); + request->addHeader("Authorization: Bearer " + _storage->accessToken()); + _workingRequest = ConnMan.addRequest(request); +} + +void GoogleDriveListDirectoryByIdRequest::responseCallback(Networking::JsonResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) { + delete response.value; + return; + } + if (response.request) + _date = response.request->date(); + + Networking::ErrorResponse error(this); + Networking::CurlJsonRequest *rq = (Networking::CurlJsonRequest *)response.request; + if (rq && rq->getNetworkReadStream()) + error.httpResponseCode = rq->getNetworkReadStream()->httpResponseCode(); + + Common::JSONValue *json = response.value; + if (json) { + Common::JSONObject responseObject = json->asObject(); + + ///debug("%s", json->stringify(true).c_str()); + + if (responseObject.contains("error") || responseObject.contains("error_summary")) { + warning("GoogleDrive returned error: %s", responseObject.getVal("error_summary")->asString().c_str()); + error.failed = true; + error.response = json->stringify(); + finishError(error); + delete json; + return; + } + + //TODO: check that ALL keys exist AND HAVE RIGHT TYPE to avoid segfaults + + if (responseObject.contains("files") && responseObject.getVal("files")->isArray()) { + Common::JSONArray items = responseObject.getVal("files")->asArray(); + for (uint32 i = 0; i < items.size(); ++i) { + Common::JSONObject item = items[i]->asObject(); + Common::String id = item.getVal("id")->asString(); + Common::String name = item.getVal("name")->asString(); + bool isDirectory = (item.getVal("mimeType")->asString() == "application/vnd.google-apps.folder"); + uint32 size = 0, timestamp = 0; + if (item.contains("size") && item.getVal("size")->isString()) + size = item.getVal("size")->asString().asUint64(); + if (item.contains("modifiedTime") && item.getVal("modifiedTime")->isString()) + timestamp = ISO8601::convertToTimestamp(item.getVal("modifiedTime")->asString()); + + //as we list directory by id, we can't determine full path for the file, so we leave it empty + _files.push_back(StorageFile(id, "", name, size, timestamp, isDirectory)); + } + } + + bool hasMore = (responseObject.contains("nextPageToken")); + + if (hasMore) { + Common::String token = responseObject.getVal("nextPageToken")->asString(); + makeRequest(token); + } else { + finishListing(_files); + } + } else { + warning("null, not json"); + error.failed = true; + finishError(error); + } + + delete json; +} + +void GoogleDriveListDirectoryByIdRequest::errorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (error.request) + _date = error.request->date(); + finishError(error); +} + +void GoogleDriveListDirectoryByIdRequest::handle() {} + +void GoogleDriveListDirectoryByIdRequest::restart() { start(); } + +Common::String GoogleDriveListDirectoryByIdRequest::date() const { return _date; } + +void GoogleDriveListDirectoryByIdRequest::finishListing(Common::Array<StorageFile> &files) { + Request::finishSuccess(); + if (_listDirectoryCallback) + (*_listDirectoryCallback)(Storage::ListDirectoryResponse(this, files)); +} + +} // End of namespace GoogleDrive +} // End of namespace Cloud diff --git a/backends/cloud/googledrive/googledrivelistdirectorybyidrequest.h b/backends/cloud/googledrive/googledrivelistdirectorybyidrequest.h new file mode 100644 index 0000000000..e94c6b1f4e --- /dev/null +++ b/backends/cloud/googledrive/googledrivelistdirectorybyidrequest.h @@ -0,0 +1,63 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_GOOGLEDRIVE_GOOGLEDRIVELISTDIRECTORYBYIDREQUEST_H +#define BACKENDS_CLOUD_GOOGLEDRIVE_GOOGLEDRIVELISTDIRECTORYBYIDREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/request.h" +#include "common/callback.h" +#include "backends/networking/curl/curljsonrequest.h" + +namespace Cloud { +namespace GoogleDrive { + +class GoogleDriveStorage; + +class GoogleDriveListDirectoryByIdRequest: public Networking::Request { + Common::String _requestedId; + GoogleDriveStorage *_storage; + + Storage::ListDirectoryCallback _listDirectoryCallback; + Common::Array<StorageFile> _files; + Request *_workingRequest; + bool _ignoreCallback; + Common::String _date; + + void start(); + void makeRequest(Common::String pageToken); + void responseCallback(Networking::JsonResponse response); + void errorCallback(Networking::ErrorResponse error); + void finishListing(Common::Array<StorageFile> &files); +public: + GoogleDriveListDirectoryByIdRequest(GoogleDriveStorage *storage, Common::String id, Storage::ListDirectoryCallback cb, Networking::ErrorCallback ecb); + virtual ~GoogleDriveListDirectoryByIdRequest(); + + virtual void handle(); + virtual void restart(); + virtual Common::String date() const; +}; + +} // End of namespace GoogleDrive +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/googledrive/googledrivestorage.cpp b/backends/cloud/googledrive/googledrivestorage.cpp new file mode 100644 index 0000000000..1b4b8baf56 --- /dev/null +++ b/backends/cloud/googledrive/googledrivestorage.cpp @@ -0,0 +1,348 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ +#define FORBIDDEN_SYMBOL_ALLOW_ALL + +#include "backends/cloud/googledrive/googledrivestorage.h" +#include "backends/cloud/cloudmanager.h" +#include "backends/cloud/googledrive/googledrivetokenrefresher.h" +#include "backends/cloud/googledrive/googledrivelistdirectorybyidrequest.h" +#include "backends/cloud/googledrive/googledriveuploadrequest.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/config-manager.h" +#include "common/debug.h" +#include "common/json.h" +#include <curl/curl.h> + +#ifdef ENABLE_RELEASE +#include "dists/clouds/cloud_keys.h" +#endif + +namespace Cloud { +namespace GoogleDrive { + +#define GOOGLEDRIVE_OAUTH2_TOKEN "https://accounts.google.com/o/oauth2/token" +#define GOOGLEDRIVE_API_FILES_ALT_MEDIA "https://www.googleapis.com/drive/v3/files/%s?alt=media" +#define GOOGLEDRIVE_API_FILES "https://www.googleapis.com/drive/v3/files" +#define GOOGLEDRIVE_API_ABOUT "https://www.googleapis.com/drive/v3/about?fields=storageQuota,user" + +char *GoogleDriveStorage::KEY = nullptr; //can't use CloudConfig there yet, loading it on instance creation/auth +char *GoogleDriveStorage::SECRET = nullptr; + +void GoogleDriveStorage::loadKeyAndSecret() { +#ifdef ENABLE_RELEASE + KEY = RELEASE_GOOGLE_DRIVE_KEY; + SECRET = RELEASE_GOOGLE_DRIVE_SECRET; +#else + Common::String k = ConfMan.get("GOOGLE_DRIVE_KEY", ConfMan.kCloudDomain); + KEY = new char[k.size() + 1]; + memcpy(KEY, k.c_str(), k.size()); + KEY[k.size()] = 0; + + k = ConfMan.get("GOOGLE_DRIVE_SECRET", ConfMan.kCloudDomain); + SECRET = new char[k.size() + 1]; + memcpy(SECRET, k.c_str(), k.size()); + SECRET[k.size()] = 0; +#endif +} + +GoogleDriveStorage::GoogleDriveStorage(Common::String accessToken, Common::String refreshToken): + _token(accessToken), _refreshToken(refreshToken) {} + +GoogleDriveStorage::GoogleDriveStorage(Common::String code) { + getAccessToken( + new Common::Callback<GoogleDriveStorage, BoolResponse>(this, &GoogleDriveStorage::codeFlowComplete), + new Common::Callback<GoogleDriveStorage, Networking::ErrorResponse>(this, &GoogleDriveStorage::codeFlowFailed), + code + ); +} + +GoogleDriveStorage::~GoogleDriveStorage() {} + +void GoogleDriveStorage::getAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback, Common::String code) { + if (!KEY || !SECRET) loadKeyAndSecret(); + bool codeFlow = (code != ""); + + if (!codeFlow && _refreshToken == "") { + warning("GoogleDriveStorage: no refresh token available to get new access token."); + if (callback) + (*callback)(BoolResponse(nullptr, false)); + return; + } + + Networking::JsonCallback innerCallback = new Common::CallbackBridge<GoogleDriveStorage, BoolResponse, Networking::JsonResponse>(this, &GoogleDriveStorage::tokenRefreshed, callback); + if (errorCallback == nullptr) + errorCallback = getErrorPrintingCallback(); + Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(innerCallback, errorCallback, GOOGLEDRIVE_OAUTH2_TOKEN); + if (codeFlow) { + request->addPostField("code=" + code); + request->addPostField("grant_type=authorization_code"); + } else { + request->addPostField("refresh_token=" + _refreshToken); + request->addPostField("grant_type=refresh_token"); + } + request->addPostField("client_id=" + Common::String(KEY)); + request->addPostField("client_secret=" + Common::String(SECRET)); + if (Cloud::CloudManager::couldUseLocalServer()) { + request->addPostField("&redirect_uri=http%3A%2F%2Flocalhost%3A12345"); + } else { + request->addPostField("&redirect_uri=https%3A%2F%2Fwww.scummvm.org/c/code"); + } + addRequest(request); +} + +void GoogleDriveStorage::tokenRefreshed(BoolCallback callback, Networking::JsonResponse response) { + Common::JSONValue *json = response.value; + if (!json) { + warning("GoogleDriveStorage: got NULL instead of JSON"); + if (callback) + (*callback)(BoolResponse(nullptr, false)); + delete callback; + return; + } + + if (!Networking::CurlJsonRequest::jsonIsObject(json, "GoogleDriveStorage")) { + if (callback) + (*callback)(BoolResponse(nullptr, false)); + delete json; + delete callback; + return; + } + + Common::JSONObject result = json->asObject(); + if (!Networking::CurlJsonRequest::jsonContainsString(result, "access_token", "GoogleDriveStorage")) { + warning("GoogleDriveStorage: bad response, no token passed"); + debug(9, "%s", json->stringify().c_str()); + if (callback) + (*callback)(BoolResponse(nullptr, false)); + } else { + _token = result.getVal("access_token")->asString(); + if (!Networking::CurlJsonRequest::jsonContainsString(result, "refresh_token", "GoogleDriveStorage")) + warning("GoogleDriveStorage: no refresh_token passed"); + else + _refreshToken = result.getVal("refresh_token")->asString(); + CloudMan.save(); //ask CloudManager to save our new refreshToken + if (callback) + (*callback)(BoolResponse(nullptr, true)); + } + delete json; + delete callback; +} + +void GoogleDriveStorage::codeFlowComplete(BoolResponse response) { + if (!response.value) { + warning("GoogleDriveStorage: failed to get access token through code flow"); + CloudMan.removeStorage(this); + return; + } + + ConfMan.removeKey("googledrive_code", ConfMan.kCloudDomain); + CloudMan.replaceStorage(this, kStorageGoogleDriveId); + ConfMan.flushToDisk(); +} + +void GoogleDriveStorage::codeFlowFailed(Networking::ErrorResponse error) { + debug(9, "GoogleDriveStorage: code flow failed (%s, %ld):", (error.failed ? "failed" : "interrupted"), error.httpResponseCode); + debug(9, "%s", error.response.c_str()); + CloudMan.removeStorage(this); +} + +void GoogleDriveStorage::saveConfig(Common::String keyPrefix) { + ConfMan.set(keyPrefix + "access_token", _token, ConfMan.kCloudDomain); + ConfMan.set(keyPrefix + "refresh_token", _refreshToken, ConfMan.kCloudDomain); +} + +Common::String GoogleDriveStorage::name() const { + return "Google Drive"; +} + +void GoogleDriveStorage::infoInnerCallback(StorageInfoCallback outerCallback, Networking::JsonResponse response) { + Common::JSONValue *json = response.value; + if (!json) { + warning("GoogleDriveStorage::infoInnerCallback: NULL passed instead of JSON"); + delete outerCallback; + return; + } + + if (!Networking::CurlJsonRequest::jsonIsObject(json, "GoogleDriveStorage::infoInnerCallback")) { + delete json; + delete outerCallback; + return; + } + + Common::JSONObject info = json->asObject(); + + Common::String uid, name, email; + uint64 quotaUsed = 0, quotaAllocated = 0; + + if (Networking::CurlJsonRequest::jsonContainsAttribute(info, "user", "GoogleDriveStorage::infoInnerCallback") && + Networking::CurlJsonRequest::jsonIsObject(info.getVal("user"), "GoogleDriveStorage::infoInnerCallback")) { + //"me":true, "kind":"drive#user","photoLink": "", + //"displayName":"Alexander Tkachev","emailAddress":"alexander@tkachov.ru","permissionId":"" + Common::JSONObject user = info.getVal("user")->asObject(); + if (Networking::CurlJsonRequest::jsonContainsString(user, "permissionId", "GoogleDriveStorage::infoInnerCallback")) + uid = user.getVal("permissionId")->asString(); //not sure it's user's id, but who cares anyway? + if (Networking::CurlJsonRequest::jsonContainsString(user, "displayName", "GoogleDriveStorage::infoInnerCallback")) + name = user.getVal("displayName")->asString(); + if (Networking::CurlJsonRequest::jsonContainsString(user, "emailAddress", "GoogleDriveStorage::infoInnerCallback")) + email = user.getVal("emailAddress")->asString(); + } + + if (Networking::CurlJsonRequest::jsonContainsAttribute(info, "storageQuota", "GoogleDriveStorage::infoInnerCallback") && + Networking::CurlJsonRequest::jsonIsObject(info.getVal("storageQuota"), "GoogleDriveStorage::infoInnerCallback")) { + //"usageInDrive":"6332462","limit":"18253611008","usage":"6332462","usageInDriveTrash":"0" + Common::JSONObject storageQuota = info.getVal("storageQuota")->asObject(); + + if (Networking::CurlJsonRequest::jsonContainsString(storageQuota, "usage", "GoogleDriveStorage::infoInnerCallback")) { + Common::String usage = storageQuota.getVal("usage")->asString(); + quotaUsed = usage.asUint64(); + } + + if (Networking::CurlJsonRequest::jsonContainsString(storageQuota, "limit", "GoogleDriveStorage::infoInnerCallback")) { + Common::String limit = storageQuota.getVal("limit")->asString(); + quotaAllocated = limit.asUint64(); + } + } + + CloudMan.setStorageUsername(kStorageGoogleDriveId, email); + + if (outerCallback) { + (*outerCallback)(StorageInfoResponse(nullptr, StorageInfo(uid, name, email, quotaUsed, quotaAllocated))); + delete outerCallback; + } + + delete json; +} + +void GoogleDriveStorage::createDirectoryInnerCallback(BoolCallback outerCallback, Networking::JsonResponse response) { + Common::JSONValue *json = response.value; + if (!json) { + warning("GoogleDriveStorage::createDirectoryInnerCallback: NULL passed instead of JSON"); + delete outerCallback; + return; + } + + if (outerCallback) { + if (Networking::CurlJsonRequest::jsonIsObject(json, "GoogleDriveStorage::createDirectoryInnerCallback")) { + Common::JSONObject info = json->asObject(); + (*outerCallback)(BoolResponse(nullptr, info.contains("id"))); + } else { + (*outerCallback)(BoolResponse(nullptr, false)); + } + delete outerCallback; + } + + delete json; +} + +Networking::Request *GoogleDriveStorage::listDirectoryById(Common::String id, ListDirectoryCallback callback, Networking::ErrorCallback errorCallback) { + if (!errorCallback) + errorCallback = getErrorPrintingCallback(); + if (!callback) + callback = new Common::Callback<GoogleDriveStorage, FileArrayResponse>(this, &GoogleDriveStorage::printFiles); + return addRequest(new GoogleDriveListDirectoryByIdRequest(this, id, callback, errorCallback)); +} + +Networking::Request *GoogleDriveStorage::upload(Common::String path, Common::SeekableReadStream *contents, UploadCallback callback, Networking::ErrorCallback errorCallback) { + return addRequest(new GoogleDriveUploadRequest(this, path, contents, callback, errorCallback)); +} + +Networking::Request *GoogleDriveStorage::streamFileById(Common::String id, Networking::NetworkReadStreamCallback callback, Networking::ErrorCallback errorCallback) { + if (callback) { + Common::String url = Common::String::format(GOOGLEDRIVE_API_FILES_ALT_MEDIA, ConnMan.urlEncode(id).c_str()); + Common::String header = "Authorization: Bearer " + _token; + curl_slist *headersList = curl_slist_append(nullptr, header.c_str()); + Networking::NetworkReadStream *stream = new Networking::NetworkReadStream(url.c_str(), headersList, ""); + (*callback)(Networking::NetworkReadStreamResponse(nullptr, stream)); + } + delete callback; + delete errorCallback; + return nullptr; +} + +void GoogleDriveStorage::printInfo(StorageInfoResponse response) { + debug(9, "\nGoogleDriveStorage: user info:"); + debug(9, "\tname: %s", response.value.name().c_str()); + debug(9, "\temail: %s", response.value.email().c_str()); + debug(9, "\tdisk usage: %lu/%lu", response.value.used(), response.value.available()); +} + +Networking::Request *GoogleDriveStorage::createDirectoryWithParentId(Common::String parentId, Common::String name, BoolCallback callback, Networking::ErrorCallback errorCallback) { + if (!errorCallback) + errorCallback = getErrorPrintingCallback(); + + Common::String url = GOOGLEDRIVE_API_FILES; + Networking::JsonCallback innerCallback = new Common::CallbackBridge<GoogleDriveStorage, BoolResponse, Networking::JsonResponse>(this, &GoogleDriveStorage::createDirectoryInnerCallback, callback); + Networking::CurlJsonRequest *request = new GoogleDriveTokenRefresher(this, innerCallback, errorCallback, url.c_str()); + request->addHeader("Authorization: Bearer " + accessToken()); + request->addHeader("Content-Type: application/json"); + + Common::JSONArray parentsArray; + parentsArray.push_back(new Common::JSONValue(parentId)); + + Common::JSONObject jsonRequestParameters; + jsonRequestParameters.setVal("mimeType", new Common::JSONValue("application/vnd.google-apps.folder")); + jsonRequestParameters.setVal("name", new Common::JSONValue(name)); + jsonRequestParameters.setVal("parents", new Common::JSONValue(parentsArray)); + + Common::JSONValue value(jsonRequestParameters); + request->addPostField(Common::JSON::stringify(&value)); + + return addRequest(request); +} + +Networking::Request *GoogleDriveStorage::info(StorageInfoCallback callback, Networking::ErrorCallback errorCallback) { + if (!callback) + callback = new Common::Callback<GoogleDriveStorage, StorageInfoResponse>(this, &GoogleDriveStorage::printInfo); + Networking::JsonCallback innerCallback = new Common::CallbackBridge<GoogleDriveStorage, StorageInfoResponse, Networking::JsonResponse>(this, &GoogleDriveStorage::infoInnerCallback, callback); + Networking::CurlJsonRequest *request = new GoogleDriveTokenRefresher(this, innerCallback, errorCallback, GOOGLEDRIVE_API_ABOUT); + request->addHeader("Authorization: Bearer " + _token); + return addRequest(request); +} + +Common::String GoogleDriveStorage::savesDirectoryPath() { return "scummvm/saves/"; } + +GoogleDriveStorage *GoogleDriveStorage::loadFromConfig(Common::String keyPrefix) { + loadKeyAndSecret(); + + if (!ConfMan.hasKey(keyPrefix + "access_token", ConfMan.kCloudDomain)) { + warning("GoogleDriveStorage: no access_token found"); + return nullptr; + } + + if (!ConfMan.hasKey(keyPrefix + "refresh_token", ConfMan.kCloudDomain)) { + warning("GoogleDriveStorage: no refresh_token found"); + return nullptr; + } + + Common::String accessToken = ConfMan.get(keyPrefix + "access_token", ConfMan.kCloudDomain); + Common::String refreshToken = ConfMan.get(keyPrefix + "refresh_token", ConfMan.kCloudDomain); + return new GoogleDriveStorage(accessToken, refreshToken); +} + +Common::String GoogleDriveStorage::getRootDirectoryId() { + return "root"; +} + +} // End of namespace GoogleDrive +} // End of namespace Cloud diff --git a/backends/cloud/googledrive/googledrivestorage.h b/backends/cloud/googledrive/googledrivestorage.h new file mode 100644 index 0000000000..6a834c44ca --- /dev/null +++ b/backends/cloud/googledrive/googledrivestorage.h @@ -0,0 +1,118 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_GOOGLEDRIVE_GOOGLEDRIVESTORAGE_H +#define BACKENDS_CLOUD_GOOGLEDRIVE_GOOGLEDRIVESTORAGE_H + +#include "backends/cloud/id/idstorage.h" +#include "backends/networking/curl/curljsonrequest.h" + +namespace Cloud { +namespace GoogleDrive { + +class GoogleDriveStorage: public Id::IdStorage { + static char *KEY, *SECRET; + + static void loadKeyAndSecret(); + + Common::String _token, _refreshToken; + + /** This private constructor is called from loadFromConfig(). */ + GoogleDriveStorage(Common::String token, Common::String refreshToken); + + void tokenRefreshed(BoolCallback callback, Networking::JsonResponse response); + void codeFlowComplete(BoolResponse response); + void codeFlowFailed(Networking::ErrorResponse error); + + /** Constructs StorageInfo based on JSON response from cloud. */ + void infoInnerCallback(StorageInfoCallback outerCallback, Networking::JsonResponse json); + + /** Returns bool based on JSON response from cloud. */ + void createDirectoryInnerCallback(BoolCallback outerCallback, Networking::JsonResponse json); + + void printInfo(StorageInfoResponse response); +public: + /** This constructor uses OAuth code flow to get tokens. */ + GoogleDriveStorage(Common::String code); + virtual ~GoogleDriveStorage(); + + /** + * Storage methods, which are used by CloudManager to save + * storage in configuration file. + */ + + /** + * Save storage data using ConfMan. + * @param keyPrefix all saved keys must start with this prefix. + * @note every Storage must write keyPrefix + "type" key + * with common value (e.g. "Dropbox"). + */ + virtual void saveConfig(Common::String keyPrefix); + + /** + * Return unique storage name. + * @returns some unique storage name (for example, "Dropbox (user@example.com)") + */ + virtual Common::String name() const; + + /** Public Cloud API comes down there. */ + + /** Returns Array<StorageFile> - the list of files. */ + virtual Networking::Request *listDirectoryById(Common::String id, ListDirectoryCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns UploadStatus struct with info about uploaded file. */ + virtual Networking::Request *upload(Common::String path, Common::SeekableReadStream *contents, UploadCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns pointer to Networking::NetworkReadStream. */ + virtual Networking::Request *streamFileById(Common::String id, Networking::NetworkReadStreamCallback callback, Networking::ErrorCallback errorCallback); + + /** Calls the callback when finished. */ + virtual Networking::Request *createDirectoryWithParentId(Common::String parentId, Common::String name, BoolCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns the StorageInfo struct. */ + virtual Networking::Request *info(StorageInfoCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns storage's saves directory path with the trailing slash. */ + virtual Common::String savesDirectoryPath(); + + /** + * Load token and user id from configs and return GoogleDriveStorage for those. + * @return pointer to the newly created GoogleDriveStorage or 0 if some problem occured. + */ + static GoogleDriveStorage *loadFromConfig(Common::String keyPrefix); + + virtual Common::String getRootDirectoryId(); + + /** + * Gets new access_token. If <code> passed is "", refresh_token is used. + * Use "" in order to refresh token and pass a callback, so you could + * continue your work when new token is available. + */ + void getAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback = nullptr, Common::String code = ""); + + Common::String accessToken() const { return _token; } +}; + +} // End of namespace GoogleDrive +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/googledrive/googledrivetokenrefresher.cpp b/backends/cloud/googledrive/googledrivetokenrefresher.cpp new file mode 100644 index 0000000000..8cc492d6b4 --- /dev/null +++ b/backends/cloud/googledrive/googledrivetokenrefresher.cpp @@ -0,0 +1,125 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ +#define FORBIDDEN_SYMBOL_ALLOW_ALL + +#include "backends/cloud/googledrive/googledrivetokenrefresher.h" +#include "backends/cloud/googledrive/googledrivestorage.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/debug.h" +#include "common/json.h" +#include <curl/curl.h> + +namespace Cloud { +namespace GoogleDrive { + +GoogleDriveTokenRefresher::GoogleDriveTokenRefresher(GoogleDriveStorage *parent, Networking::JsonCallback callback, Networking::ErrorCallback ecb, const char *url): + CurlJsonRequest(callback, ecb, url), _parentStorage(parent) {} + +GoogleDriveTokenRefresher::~GoogleDriveTokenRefresher() {} + +void GoogleDriveTokenRefresher::tokenRefreshed(Storage::BoolResponse response) { + if (!response.value) { + //failed to refresh token, notify user with NULL in original callback + warning("GoogleDriveTokenRefresher: failed to refresh token"); + finishError(Networking::ErrorResponse(this, false, true, "", -1)); + return; + } + + //update headers: first change header with token, then pass those to request + for (uint32 i = 0; i < _headers.size(); ++i) { + if (_headers[i].contains("Authorization")) { + _headers[i] = "Authorization: Bearer " + _parentStorage->accessToken(); + } + } + setHeaders(_headers); + + //successfully received refreshed token, can restart the original request now + retry(0); +} + +void GoogleDriveTokenRefresher::finishJson(Common::JSONValue *json) { + if (!json) { + //that's probably not an error (200 OK) + CurlJsonRequest::finishJson(nullptr); + return; + } + + if (jsonIsObject(json, "GoogleDriveTokenRefresher")) { + Common::JSONObject result = json->asObject(); + long httpResponseCode = -1; + if (result.contains("error") && jsonIsObject(result.getVal("error"), "GoogleDriveTokenRefresher")) { + //new token needed => request token & then retry original request + if (_stream) { + httpResponseCode = _stream->httpResponseCode(); + debug(9, "GoogleDriveTokenRefresher: code = %ld", httpResponseCode); + } + + Common::JSONObject error = result.getVal("error")->asObject(); + bool irrecoverable = true; + + uint32 code = -1; + Common::String message; + if (jsonContainsIntegerNumber(error, "code", "GoogleDriveTokenRefresher")) { + code = error.getVal("code")->asIntegerNumber(); + debug(9, "GoogleDriveTokenRefresher: code = %u", code); + } + + if (jsonContainsString(error, "message", "GoogleDriveTokenRefresher")) { + message = error.getVal("message")->asString(); + debug(9, "GoogleDriveTokenRefresher: message = %s", message.c_str()); + } + + if (code == 401 || message == "Invalid Credentials") + irrecoverable = false; + + if (irrecoverable) { + finishError(Networking::ErrorResponse(this, false, true, json->stringify(true), httpResponseCode)); + delete json; + return; + } + + pause(); + delete json; + _parentStorage->getAccessToken(new Common::Callback<GoogleDriveTokenRefresher, Storage::BoolResponse>(this, &GoogleDriveTokenRefresher::tokenRefreshed)); + return; + } + } + + //notify user of success + CurlJsonRequest::finishJson(json); +} + +void GoogleDriveTokenRefresher::setHeaders(Common::Array<Common::String> &headers) { + _headers = headers; + curl_slist_free_all(_headersList); + _headersList = 0; + for (uint32 i = 0; i < headers.size(); ++i) + CurlJsonRequest::addHeader(headers[i]); +} + +void GoogleDriveTokenRefresher::addHeader(Common::String header) { + _headers.push_back(header); + CurlJsonRequest::addHeader(header); +} + +} // End of namespace GoogleDrive +} // End of namespace Cloud diff --git a/backends/cloud/googledrive/googledrivetokenrefresher.h b/backends/cloud/googledrive/googledrivetokenrefresher.h new file mode 100644 index 0000000000..6cb3e41849 --- /dev/null +++ b/backends/cloud/googledrive/googledrivetokenrefresher.h @@ -0,0 +1,52 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_GOOGLEDRIVE_GOOGLEDRIVETOKENREFRESHER_H +#define BACKENDS_CLOUD_GOOGLEDRIVE_GOOGLEDRIVETOKENREFRESHER_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/curljsonrequest.h" + +namespace Cloud { +namespace GoogleDrive { + +class GoogleDriveStorage; + +class GoogleDriveTokenRefresher: public Networking::CurlJsonRequest { + GoogleDriveStorage *_parentStorage; + Common::Array<Common::String> _headers; + + void tokenRefreshed(Storage::BoolResponse response); + + virtual void finishJson(Common::JSONValue *json); +public: + GoogleDriveTokenRefresher(GoogleDriveStorage *parent, Networking::JsonCallback callback, Networking::ErrorCallback ecb, const char *url); + virtual ~GoogleDriveTokenRefresher(); + + virtual void setHeaders(Common::Array<Common::String> &headers); + virtual void addHeader(Common::String header); +}; + +} // End of namespace GoogleDrive +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/googledrive/googledriveuploadrequest.cpp b/backends/cloud/googledrive/googledriveuploadrequest.cpp new file mode 100644 index 0000000000..5f61dcd2a8 --- /dev/null +++ b/backends/cloud/googledrive/googledriveuploadrequest.cpp @@ -0,0 +1,353 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/cloud/googledrive/googledriveuploadrequest.h" +#include "backends/cloud/googledrive/googledrivestorage.h" +#include "backends/cloud/iso8601.h" +#include "backends/cloud/storage.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/json.h" +#include "googledrivetokenrefresher.h" + +namespace Cloud { +namespace GoogleDrive { + +#define GOOGLEDRIVE_API_FILES "https://www.googleapis.com/upload/drive/v3/files" + +GoogleDriveUploadRequest::GoogleDriveUploadRequest(GoogleDriveStorage *storage, Common::String path, Common::SeekableReadStream *contents, Storage::UploadCallback callback, Networking::ErrorCallback ecb): + Networking::Request(nullptr, ecb), _storage(storage), _savePath(path), _contentsStream(contents), _uploadCallback(callback), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +GoogleDriveUploadRequest::~GoogleDriveUploadRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _contentsStream; + delete _uploadCallback; +} + +void GoogleDriveUploadRequest::start() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + if (_contentsStream == nullptr || !_contentsStream->seek(0)) { + warning("GoogleDriveUploadRequest: cannot restart because stream couldn't seek(0)"); + finishError(Networking::ErrorResponse(this, false, true, "", -1)); + return; + } + _resolvedId = ""; //used to update file contents + _parentId = ""; //used to create file within parent directory + _serverReceivedBytes = 0; + _ignoreCallback = false; + + resolveId(); +} + +void GoogleDriveUploadRequest::resolveId() { + //check whether such file already exists + Storage::UploadCallback innerCallback = new Common::Callback<GoogleDriveUploadRequest, Storage::UploadResponse>(this, &GoogleDriveUploadRequest::idResolvedCallback); + Networking::ErrorCallback innerErrorCallback = new Common::Callback<GoogleDriveUploadRequest, Networking::ErrorResponse>(this, &GoogleDriveUploadRequest::idResolveFailedCallback); + _workingRequest = _storage->resolveFileId(_savePath, innerCallback, innerErrorCallback); +} + +void GoogleDriveUploadRequest::idResolvedCallback(Storage::UploadResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + _resolvedId = response.value.id(); + startUpload(); +} + +void GoogleDriveUploadRequest::idResolveFailedCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + //not resolved => error or no such file + if (error.response.contains("no such file found in its parent directory")) { + //parent's id after the '\n' + Common::String parentId = error.response; + for (uint32 i = 0; i < parentId.size(); ++i) + if (parentId[i] == '\n') { + parentId.erase(0, i + 1); + break; + } + + _parentId = parentId; + startUpload(); + return; + } + + finishError(error); +} + +void GoogleDriveUploadRequest::startUpload() { + Common::String name = _savePath; + for (uint32 i = name.size(); i > 0; --i) { + if (name[i - 1] == '/' || name[i - 1] == '\\') { + name.erase(0, i); + break; + } + } + + Common::String url = GOOGLEDRIVE_API_FILES; + if (_resolvedId != "") + url += "/" + ConnMan.urlEncode(_resolvedId); + url += "?uploadType=resumable&fields=id,mimeType,modifiedTime,name,size"; + Networking::JsonCallback callback = new Common::Callback<GoogleDriveUploadRequest, Networking::JsonResponse>(this, &GoogleDriveUploadRequest::startUploadCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<GoogleDriveUploadRequest, Networking::ErrorResponse>(this, &GoogleDriveUploadRequest::startUploadErrorCallback); + Networking::CurlJsonRequest *request = new GoogleDriveTokenRefresher(_storage, callback, failureCallback, url.c_str()); + request->addHeader("Authorization: Bearer " + _storage->accessToken()); + request->addHeader("Content-Type: application/json"); + if (_resolvedId != "") + request->usePatch(); + + Common::JSONObject jsonRequestParameters; + if (_resolvedId != "") { + jsonRequestParameters.setVal("id", new Common::JSONValue(_resolvedId)); + } else { + Common::JSONArray parentsArray; + parentsArray.push_back(new Common::JSONValue(_parentId)); + jsonRequestParameters.setVal("parents", new Common::JSONValue(parentsArray)); + } + jsonRequestParameters.setVal("name", new Common::JSONValue(name)); + + Common::JSONValue value(jsonRequestParameters); + request->addPostField(Common::JSON::stringify(&value)); + + _workingRequest = ConnMan.addRequest(request); +} + +void GoogleDriveUploadRequest::startUploadCallback(Networking::JsonResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + Networking::ErrorResponse error(this, false, true, "", -1); + Networking::CurlJsonRequest *rq = (Networking::CurlJsonRequest *)response.request; + if (rq) { + const Networking::NetworkReadStream *stream = rq->getNetworkReadStream(); + if (stream) { + long code = stream->httpResponseCode(); + Common::String headers = stream->responseHeaders(); + if (code == 200) { + const char *cstr = headers.c_str(); + const char *position = strstr(cstr, "Location: "); + + if (position) { + Common::String result = ""; + char c; + for (const char *i = position + 10; c = *i, c != 0; ++i) { + if (c == '\n' || c == '\r') + break; + result += c; + } + _uploadUrl = result; + uploadNextPart(); + return; + } + } + + error.httpResponseCode = code; + } + } + + Common::JSONValue *json = response.value; + delete json; + + finishError(error); +} + +void GoogleDriveUploadRequest::startUploadErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + finishError(error); +} + +void GoogleDriveUploadRequest::uploadNextPart() { + const uint32 UPLOAD_PER_ONE_REQUEST = 10 * 1024 * 1024; + Common::String url = _uploadUrl; + + Networking::JsonCallback callback = new Common::Callback<GoogleDriveUploadRequest, Networking::JsonResponse>(this, &GoogleDriveUploadRequest::partUploadedCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<GoogleDriveUploadRequest, Networking::ErrorResponse>(this, &GoogleDriveUploadRequest::partUploadedErrorCallback); + Networking::CurlJsonRequest *request = new GoogleDriveTokenRefresher(_storage, callback, failureCallback, url.c_str()); + request->addHeader("Authorization: Bearer " + _storage->accessToken()); + request->usePut(); + + uint32 oldPos = _contentsStream->pos(); + if (oldPos != _serverReceivedBytes) { + if (!_contentsStream->seek(_serverReceivedBytes)) { + warning("GoogleDriveUploadRequest: cannot upload because stream couldn't seek(%lu)", _serverReceivedBytes); + finishError(Networking::ErrorResponse(this, false, true, "", -1)); + return; + } + oldPos = _serverReceivedBytes; + } + + byte *buffer = new byte[UPLOAD_PER_ONE_REQUEST]; + uint32 size = _contentsStream->read(buffer, UPLOAD_PER_ONE_REQUEST); + if (size != 0) + request->setBuffer(buffer, size); + + if (_uploadUrl != "") { + if (_contentsStream->pos() == 0) + request->addHeader(Common::String::format("Content-Length: 0")); + else + request->addHeader(Common::String::format("Content-Range: bytes %u-%u/%u", oldPos, _contentsStream->pos() - 1, _contentsStream->size())); + } + + _workingRequest = ConnMan.addRequest(request); +} + +bool GoogleDriveUploadRequest::handleHttp308(const Networking::NetworkReadStream *stream) { + //308 Resume Incomplete, with Range: X-Y header + if (!stream) + return false; + if (stream->httpResponseCode() != 308) + return false; //seriously + + Common::String headers = stream->responseHeaders(); + const char *cstr = headers.c_str(); + for (int rangeTry = 0; rangeTry < 2; ++rangeTry) { + const char *needle = (rangeTry == 0 ? "Range: 0-" : "Range: bytes=0-"); + uint32 needleLength = (rangeTry == 0 ? 9 : 15); + + const char *position = strstr(cstr, needle); //if it lost the first part, I refuse to talk with it + + if (position) { + Common::String result = ""; + char c; + for (const char *i = position + needleLength; c = *i, c != 0; ++i) { + if (c == '\n' || c == '\r') + break; + result += c; + } + _serverReceivedBytes = result.asUint64() + 1; + uploadNextPart(); + return true; + } + } + + return false; +} + +void GoogleDriveUploadRequest::partUploadedCallback(Networking::JsonResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + Networking::ErrorResponse error(this, false, true, "", -1); + Networking::CurlJsonRequest *rq = (Networking::CurlJsonRequest *)response.request; + if (rq) { + const Networking::NetworkReadStream *stream = rq->getNetworkReadStream(); + if (stream) { + long code = stream->httpResponseCode(); + error.httpResponseCode = code; + if (code == 308 && handleHttp308(stream)) { + delete (Common::JSONValue *)response.value; + return; + } + } + } + + Common::JSONValue *json = response.value; + if (json == nullptr) { + error.response = "Failed to parse JSON, null passed!"; + finishError(error); + return; + } + + if (json->isObject()) { + Common::JSONObject object = json->asObject(); + + if (object.contains("error")) { + warning("GoogleDrive returned error: %s", json->stringify(true).c_str()); + error.response = json->stringify(true); + finishError(error); + delete json; + return; + } + + if (Networking::CurlJsonRequest::jsonContainsString(object, "id", "GoogleDriveUploadRequest") && + Networking::CurlJsonRequest::jsonContainsString(object, "name", "GoogleDriveUploadRequest") && + Networking::CurlJsonRequest::jsonContainsString(object, "mimeType", "GoogleDriveUploadRequest")) { + //finished + Common::String id = object.getVal("id")->asString(); + Common::String name = object.getVal("name")->asString(); + bool isDirectory = (object.getVal("mimeType")->asString() == "application/vnd.google-apps.folder"); + uint32 size = 0, timestamp = 0; + if (Networking::CurlJsonRequest::jsonContainsString(object, "size", "GoogleDriveUploadRequest", true)) + size = object.getVal("size")->asString().asUint64(); + if (Networking::CurlJsonRequest::jsonContainsString(object, "modifiedTime", "GoogleDriveUploadRequest", true)) + timestamp = ISO8601::convertToTimestamp(object.getVal("modifiedTime")->asString()); + + finishUpload(StorageFile(id, _savePath, name, size, timestamp, isDirectory)); + return; + } + } + + if (_contentsStream->eos() || _contentsStream->pos() >= _contentsStream->size() - 1) { + warning("GoogleDriveUploadRequest: no file info to return"); + finishUpload(StorageFile(_savePath, 0, 0, false)); + } else { + uploadNextPart(); + } + + delete json; +} + +void GoogleDriveUploadRequest::partUploadedErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + Networking::CurlJsonRequest *rq = (Networking::CurlJsonRequest *)error.request; + if (rq) { + const Networking::NetworkReadStream *stream = rq->getNetworkReadStream(); + if (stream) { + long code = stream->httpResponseCode(); + if (code == 308 && handleHttp308(stream)) { + return; + } + } + } + + finishError(error); +} + +void GoogleDriveUploadRequest::handle() {} + +void GoogleDriveUploadRequest::restart() { start(); } + +void GoogleDriveUploadRequest::finishUpload(StorageFile file) { + Request::finishSuccess(); + if (_uploadCallback) + (*_uploadCallback)(Storage::UploadResponse(this, file)); +} + +} // End of namespace GoogleDrive +} // End of namespace Cloud diff --git a/backends/cloud/googledrive/googledriveuploadrequest.h b/backends/cloud/googledrive/googledriveuploadrequest.h new file mode 100644 index 0000000000..73acab5bbd --- /dev/null +++ b/backends/cloud/googledrive/googledriveuploadrequest.h @@ -0,0 +1,70 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_GOOGLEDRIVE_GOOGLEDRIVEUPLOADREQUEST_H +#define BACKENDS_CLOUD_GOOGLEDRIVE_GOOGLEDRIVEUPLOADREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/request.h" +#include "common/callback.h" + +namespace Cloud { +namespace GoogleDrive { +class GoogleDriveStorage; + +class GoogleDriveUploadRequest: public Networking::Request { + GoogleDriveStorage *_storage; + Common::String _savePath; + Common::SeekableReadStream *_contentsStream; + Storage::UploadCallback _uploadCallback; + Request *_workingRequest; + bool _ignoreCallback; + Common::String _resolvedId, _parentId; + Common::String _uploadUrl; + uint64 _serverReceivedBytes; + + void start(); + void resolveId(); + void idResolvedCallback(Storage::UploadResponse response); + void idResolveFailedCallback(Networking::ErrorResponse error); + void startUpload(); + void startUploadCallback(Networking::JsonResponse response); + void startUploadErrorCallback(Networking::ErrorResponse error); + void uploadNextPart(); + void partUploadedCallback(Networking::JsonResponse response); + void partUploadedErrorCallback(Networking::ErrorResponse error); + bool handleHttp308(const Networking::NetworkReadStream *stream); + void finishUpload(StorageFile status); + +public: + GoogleDriveUploadRequest(GoogleDriveStorage *storage, Common::String path, Common::SeekableReadStream *contents, Storage::UploadCallback callback, Networking::ErrorCallback ecb); + virtual ~GoogleDriveUploadRequest(); + + virtual void handle(); + virtual void restart(); +}; + +} // End of namespace GoogleDrive +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/id/idcreatedirectoryrequest.cpp b/backends/cloud/id/idcreatedirectoryrequest.cpp new file mode 100644 index 0000000000..37f417f806 --- /dev/null +++ b/backends/cloud/id/idcreatedirectoryrequest.cpp @@ -0,0 +1,163 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/cloud/id/idcreatedirectoryrequest.h" +#include "backends/cloud/id/idstorage.h" +#include "common/debug.h" + +namespace Cloud { +namespace Id { + +IdCreateDirectoryRequest::IdCreateDirectoryRequest(IdStorage *storage, Common::String parentPath, Common::String directoryName, Storage::BoolCallback cb, Networking::ErrorCallback ecb): + Networking::Request(nullptr, ecb), + _requestedParentPath(parentPath), _requestedDirectoryName(directoryName), _storage(storage), _boolCallback(cb), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +IdCreateDirectoryRequest::~IdCreateDirectoryRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _boolCallback; +} + +void IdCreateDirectoryRequest::start() { + //cleanup + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _workingRequest = nullptr; + _ignoreCallback = false; + + //the only exception when we create parent folder - is when it's ScummVM/ base folder + Common::String prefix = _requestedParentPath; + if (prefix.size() > 7) + prefix.erase(7); + if (prefix.equalsIgnoreCase("ScummVM")) { + Storage::BoolCallback callback = new Common::Callback<IdCreateDirectoryRequest, Storage::BoolResponse>(this, &IdCreateDirectoryRequest::createdBaseDirectoryCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<IdCreateDirectoryRequest, Networking::ErrorResponse>(this, &IdCreateDirectoryRequest::createdBaseDirectoryErrorCallback); + _workingRequest = _storage->createDirectory("ScummVM", callback, failureCallback); + return; + } + + resolveId(); +} + +void IdCreateDirectoryRequest::createdBaseDirectoryCallback(Storage::BoolResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (response.request) + _date = response.request->date(); + resolveId(); +} + +void IdCreateDirectoryRequest::createdBaseDirectoryErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (error.request) + _date = error.request->date(); + finishError(error); +} + +void IdCreateDirectoryRequest::resolveId() { + //check whether such folder already exists + Storage::UploadCallback innerCallback = new Common::Callback<IdCreateDirectoryRequest, Storage::UploadResponse>(this, &IdCreateDirectoryRequest::idResolvedCallback); + Networking::ErrorCallback innerErrorCallback = new Common::Callback<IdCreateDirectoryRequest, Networking::ErrorResponse>(this, &IdCreateDirectoryRequest::idResolveFailedCallback); + Common::String path = _requestedParentPath; + if (_requestedParentPath != "") + path += "/"; + path += _requestedDirectoryName; + _workingRequest = _storage->resolveFileId(path, innerCallback, innerErrorCallback); +} + +void IdCreateDirectoryRequest::idResolvedCallback(Storage::UploadResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (response.request) + _date = response.request->date(); + + //resolved => folder already exists + finishCreation(false); +} + +void IdCreateDirectoryRequest::idResolveFailedCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (error.request) + _date = error.request->date(); + + //not resolved => folder not exists + if (error.response.contains("no such file found in its parent directory")) { + //parent's id after the '\n' + Common::String parentId = error.response; + for (uint32 i = 0; i < parentId.size(); ++i) + if (parentId[i] == '\n') { + parentId.erase(0, i + 1); + break; + } + + Storage::BoolCallback callback = new Common::Callback<IdCreateDirectoryRequest, Storage::BoolResponse>(this, &IdCreateDirectoryRequest::createdDirectoryCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<IdCreateDirectoryRequest, Networking::ErrorResponse>(this, &IdCreateDirectoryRequest::createdDirectoryErrorCallback); + _workingRequest = _storage->createDirectoryWithParentId(parentId, _requestedDirectoryName, callback, failureCallback); + return; + } + + finishError(error); +} + +void IdCreateDirectoryRequest::createdDirectoryCallback(Storage::BoolResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (response.request) + _date = response.request->date(); + finishCreation(response.value); +} + +void IdCreateDirectoryRequest::createdDirectoryErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (error.request) + _date = error.request->date(); + finishError(error); +} + +void IdCreateDirectoryRequest::handle() {} + +void IdCreateDirectoryRequest::restart() { start(); } + +Common::String IdCreateDirectoryRequest::date() const { return _date; } + +void IdCreateDirectoryRequest::finishCreation(bool success) { + Request::finishSuccess(); + if (_boolCallback) + (*_boolCallback)(Storage::BoolResponse(this, success)); +} + +} // End of namespace Id +} // End of namespace Cloud diff --git a/backends/cloud/id/idcreatedirectoryrequest.h b/backends/cloud/id/idcreatedirectoryrequest.h new file mode 100644 index 0000000000..241bcd30be --- /dev/null +++ b/backends/cloud/id/idcreatedirectoryrequest.h @@ -0,0 +1,65 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_ID_IDCREATEDIRECTORYREQUEST_H +#define BACKENDS_CLOUD_ID_IDCREATEDIRECTORYREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/request.h" +#include "common/callback.h" + +namespace Cloud { +namespace Id { + +class IdStorage; + +class IdCreateDirectoryRequest: public Networking::Request { + Common::String _requestedParentPath; + Common::String _requestedDirectoryName; + IdStorage *_storage; + Storage::BoolCallback _boolCallback; + Request *_workingRequest; + bool _ignoreCallback; + Common::String _date; + + void start(); + void createdBaseDirectoryCallback(Storage::BoolResponse response); + void createdBaseDirectoryErrorCallback(Networking::ErrorResponse error); + void resolveId(); + void idResolvedCallback(Storage::UploadResponse response); + void idResolveFailedCallback(Networking::ErrorResponse error); + void createdDirectoryCallback(Storage::BoolResponse response); + void createdDirectoryErrorCallback(Networking::ErrorResponse error); + void finishCreation(bool success); +public: + IdCreateDirectoryRequest(IdStorage *storage, Common::String parentPath, Common::String directoryName, Storage::BoolCallback cb, Networking::ErrorCallback ecb); + virtual ~IdCreateDirectoryRequest(); + + virtual void handle(); + virtual void restart(); + virtual Common::String date() const; +}; + +} // End of namespace Id +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/id/iddownloadrequest.cpp b/backends/cloud/id/iddownloadrequest.cpp new file mode 100644 index 0000000000..2532d611b8 --- /dev/null +++ b/backends/cloud/id/iddownloadrequest.cpp @@ -0,0 +1,108 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/cloud/id/iddownloadrequest.h" +#include "backends/cloud/id/idstorage.h" +#include "backends/cloud/downloadrequest.h" + +namespace Cloud { +namespace Id { + +IdDownloadRequest::IdDownloadRequest(IdStorage *storage, Common::String remotePath, Common::String localPath, Storage::BoolCallback cb, Networking::ErrorCallback ecb): + Networking::Request(nullptr, ecb), _requestedFile(remotePath), _requestedLocalFile(localPath), _storage(storage), _boolCallback(cb), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +IdDownloadRequest::~IdDownloadRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _boolCallback; +} + +void IdDownloadRequest::start() { + //cleanup + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _workingRequest = nullptr; + _ignoreCallback = false; + + //find file's id + Storage::UploadCallback innerCallback = new Common::Callback<IdDownloadRequest, Storage::UploadResponse>(this, &IdDownloadRequest::idResolvedCallback); + Networking::ErrorCallback innerErrorCallback = new Common::Callback<IdDownloadRequest, Networking::ErrorResponse>(this, &IdDownloadRequest::idResolveFailedCallback); + _workingRequest = _storage->resolveFileId(_requestedFile, innerCallback, innerErrorCallback); +} + +void IdDownloadRequest::idResolvedCallback(Storage::UploadResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + Storage::BoolCallback innerCallback = new Common::Callback<IdDownloadRequest, Storage::BoolResponse>(this, &IdDownloadRequest::downloadCallback); + Networking::ErrorCallback innerErrorCallback = new Common::Callback<IdDownloadRequest, Networking::ErrorResponse>(this, &IdDownloadRequest::downloadErrorCallback); + _workingRequest = _storage->downloadById(response.value.id(), _requestedLocalFile, innerCallback, innerErrorCallback); +} + +void IdDownloadRequest::idResolveFailedCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + finishError(error); +} + +void IdDownloadRequest::downloadCallback(Storage::BoolResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + finishDownload(response.value); +} + +void IdDownloadRequest::downloadErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + finishError(error); +} + +void IdDownloadRequest::handle() {} + +void IdDownloadRequest::restart() { start(); } + +void IdDownloadRequest::finishDownload(bool success) { + Request::finishSuccess(); + if (_boolCallback) + (*_boolCallback)(Storage::BoolResponse(this, success)); +} + +double IdDownloadRequest::getProgress() const { + DownloadRequest *downloadRequest = dynamic_cast<DownloadRequest *>(_workingRequest); + if (downloadRequest == nullptr) + return 0; // resolving id still + + // id resolve is 10 % and download is the other 90 % + return 0.1 + 0.9 * downloadRequest->getProgress(); // downloading +} + +} // End of namespace Id +} // End of namespace Cloud diff --git a/backends/cloud/id/iddownloadrequest.h b/backends/cloud/id/iddownloadrequest.h new file mode 100644 index 0000000000..65e05c00b3 --- /dev/null +++ b/backends/cloud/id/iddownloadrequest.h @@ -0,0 +1,62 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_ID_IDDOWNLOADREQUEST_H +#define BACKENDS_CLOUD_ID_IDDOWNLOADREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/request.h" +#include "common/callback.h" + +namespace Cloud { +namespace Id { + +class IdStorage; + +class IdDownloadRequest: public Networking::Request { + Common::String _requestedFile, _requestedLocalFile; + IdStorage *_storage; + Storage::BoolCallback _boolCallback; + Request *_workingRequest; + bool _ignoreCallback; + + void start(); + void idResolvedCallback(Storage::UploadResponse response); + void idResolveFailedCallback(Networking::ErrorResponse error); + void downloadCallback(Storage::BoolResponse response); + void downloadErrorCallback(Networking::ErrorResponse error); + void finishDownload(bool success); +public: + IdDownloadRequest(IdStorage *storage, Common::String remotePath, Common::String localPath, Storage::BoolCallback cb, Networking::ErrorCallback ecb); + virtual ~IdDownloadRequest(); + + virtual void handle(); + virtual void restart(); + + /** Returns a number in range [0, 1], where 1 is "complete". */ + double getProgress() const; +}; + +} // End of namespace Id +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/id/idlistdirectoryrequest.cpp b/backends/cloud/id/idlistdirectoryrequest.cpp new file mode 100644 index 0000000000..4e63709984 --- /dev/null +++ b/backends/cloud/id/idlistdirectoryrequest.cpp @@ -0,0 +1,141 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/cloud/id/idlistdirectoryrequest.h" +#include "backends/cloud/id/idstorage.h" + +namespace Cloud { +namespace Id { + +IdListDirectoryRequest::IdListDirectoryRequest(IdStorage *storage, Common::String path, Storage::ListDirectoryCallback cb, Networking::ErrorCallback ecb, bool recursive): + Networking::Request(nullptr, ecb), + _requestedPath(path), _requestedRecursive(recursive), _storage(storage), _listDirectoryCallback(cb), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +IdListDirectoryRequest::~IdListDirectoryRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _listDirectoryCallback; +} + +void IdListDirectoryRequest::start() { + //cleanup + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _workingRequest = nullptr; + _files.clear(); + _directoriesQueue.clear(); + _currentDirectory = StorageFile(); + _ignoreCallback = false; + + //find out that directory's id + Storage::UploadCallback innerCallback = new Common::Callback<IdListDirectoryRequest, Storage::UploadResponse>(this, &IdListDirectoryRequest::idResolvedCallback); + Networking::ErrorCallback innerErrorCallback = new Common::Callback<IdListDirectoryRequest, Networking::ErrorResponse>(this, &IdListDirectoryRequest::idResolveErrorCallback); + _workingRequest = _storage->resolveFileId(_requestedPath, innerCallback, innerErrorCallback); +} + +void IdListDirectoryRequest::idResolvedCallback(Storage::UploadResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (response.request) + _date = response.request->date(); + + StorageFile directory = response.value; + directory.setPath(_requestedPath); + _directoriesQueue.push_back(directory); + listNextDirectory(); +} + +void IdListDirectoryRequest::idResolveErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (error.request) + _date = error.request->date(); + finishError(error); +} + +void IdListDirectoryRequest::listNextDirectory() { + if (_directoriesQueue.empty()) { + finishListing(_files); + return; + } + + _currentDirectory = _directoriesQueue.back(); + _directoriesQueue.pop_back(); + + Storage::FileArrayCallback callback = new Common::Callback<IdListDirectoryRequest, Storage::FileArrayResponse>(this, &IdListDirectoryRequest::listedDirectoryCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<IdListDirectoryRequest, Networking::ErrorResponse>(this, &IdListDirectoryRequest::listedDirectoryErrorCallback); + _workingRequest = _storage->listDirectoryById(_currentDirectory.id(), callback, failureCallback); +} + +void IdListDirectoryRequest::listedDirectoryCallback(Storage::FileArrayResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (response.request) + _date = response.request->date(); + + for (uint32 i = 0; i < response.value.size(); ++i) { + StorageFile &file = response.value[i]; + Common::String path = _currentDirectory.path(); + if (path.size() && path.lastChar() != '/' && path.lastChar() != '\\') + path += '/'; + path += file.name(); + file.setPath(path); + _files.push_back(file); + if (_requestedRecursive && file.isDirectory()) { + _directoriesQueue.push_back(file); + } + } + + listNextDirectory(); +} + +void IdListDirectoryRequest::listedDirectoryErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (error.request) + _date = error.request->date(); + finishError(error); +} + +void IdListDirectoryRequest::handle() {} + +void IdListDirectoryRequest::restart() { start(); } + +Common::String IdListDirectoryRequest::date() const { return _date; } + +void IdListDirectoryRequest::finishListing(Common::Array<StorageFile> &files) { + Request::finishSuccess(); + if (_listDirectoryCallback) + (*_listDirectoryCallback)(Storage::ListDirectoryResponse(this, files)); +} + +} // End of namespace Id +} // End of namespace Cloud diff --git a/backends/cloud/id/idlistdirectoryrequest.h b/backends/cloud/id/idlistdirectoryrequest.h new file mode 100644 index 0000000000..58c5d2c864 --- /dev/null +++ b/backends/cloud/id/idlistdirectoryrequest.h @@ -0,0 +1,66 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_ID_IDLISTDIRECTORYREQUEST_H +#define BACKENDS_CLOUD_ID_IDLISTDIRECTORYREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/request.h" +#include "common/callback.h" + +namespace Cloud { +namespace Id { + +class IdStorage; + +class IdListDirectoryRequest: public Networking::Request { + Common::String _requestedPath; + bool _requestedRecursive; + IdStorage *_storage; + Storage::ListDirectoryCallback _listDirectoryCallback; + Common::Array<StorageFile> _files; + Common::Array<StorageFile> _directoriesQueue; + StorageFile _currentDirectory; + Request *_workingRequest; + bool _ignoreCallback; + Common::String _date; + + void start(); + void idResolvedCallback(Storage::UploadResponse response); + void idResolveErrorCallback(Networking::ErrorResponse error); + void listNextDirectory(); + void listedDirectoryCallback(Storage::FileArrayResponse response); + void listedDirectoryErrorCallback(Networking::ErrorResponse error); + void finishListing(Common::Array<StorageFile> &files); +public: + IdListDirectoryRequest(IdStorage *storage, Common::String path, Storage::ListDirectoryCallback cb, Networking::ErrorCallback ecb, bool recursive = false); + virtual ~IdListDirectoryRequest(); + + virtual void handle(); + virtual void restart(); + virtual Common::String date() const; +}; + +} // End of namespace Id +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/id/idresolveidrequest.cpp b/backends/cloud/id/idresolveidrequest.cpp new file mode 100644 index 0000000000..e8589fc204 --- /dev/null +++ b/backends/cloud/id/idresolveidrequest.cpp @@ -0,0 +1,136 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/cloud/id/idresolveidrequest.h" +#include "backends/cloud/id/idstorage.h" + +namespace Cloud { +namespace Id { + +IdResolveIdRequest::IdResolveIdRequest(IdStorage *storage, Common::String path, Storage::UploadCallback cb, Networking::ErrorCallback ecb, bool recursive): + Networking::Request(nullptr, ecb), + _requestedPath(path), _storage(storage), _uploadCallback(cb), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +IdResolveIdRequest::~IdResolveIdRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _uploadCallback; +} + +void IdResolveIdRequest::start() { + //cleanup + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _workingRequest = nullptr; + _currentDirectory = ""; + _currentDirectoryId = _storage->getRootDirectoryId(); + _ignoreCallback = false; + + listNextDirectory(StorageFile(_currentDirectoryId, 0, 0, true)); +} + +void IdResolveIdRequest::listNextDirectory(StorageFile fileToReturn) { + if (_currentDirectory.equalsIgnoreCase(_requestedPath)) { + finishFile(fileToReturn); + return; + } + + Storage::FileArrayCallback callback = new Common::Callback<IdResolveIdRequest, Storage::FileArrayResponse>(this, &IdResolveIdRequest::listedDirectoryCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<IdResolveIdRequest, Networking::ErrorResponse>(this, &IdResolveIdRequest::listedDirectoryErrorCallback); + _workingRequest = _storage->listDirectoryById(_currentDirectoryId, callback, failureCallback); +} + +void IdResolveIdRequest::listedDirectoryCallback(Storage::FileArrayResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + Common::String currentLevelName = _requestedPath; + ///debug(9, "'%s'", currentLevelName.c_str()); + if (_currentDirectory.size()) + currentLevelName.erase(0, _currentDirectory.size()); + if (currentLevelName.size() && (currentLevelName[0] == '/' || currentLevelName[0] == '\\')) + currentLevelName.erase(0, 1); + ///debug(9, "'%s'", currentLevelName.c_str()); + for (uint32 i = 0; i < currentLevelName.size(); ++i) { + if (currentLevelName[i] == '/' || currentLevelName[i] == '\\') { + currentLevelName.erase(i); + ///debug(9, "'%s'", currentLevelName.c_str()); + break; + } + } + + Common::String path = _currentDirectory; + if (path != "") + path += "/"; + path += currentLevelName; + bool lastLevel = (path.equalsIgnoreCase(_requestedPath)); + + ///debug(9, "IdResolveIdRequest: searching for '%s' in '%s'", currentLevelName.c_str(), _currentDirectory.c_str()); + + Common::Array<StorageFile> &files = response.value; + bool found = false; + for (uint32 i = 0; i < files.size(); ++i) { + if ((files[i].isDirectory() || lastLevel) && files[i].name().equalsIgnoreCase(currentLevelName)) { + if (_currentDirectory != "") + _currentDirectory += "/"; + _currentDirectory += files[i].name(); + _currentDirectoryId = files[i].id(); + ///debug(9, "IdResolveIdRequest: found it! new directory and its id: '%s', '%s'", _currentDirectory.c_str(), _currentDirectoryId.c_str()); + listNextDirectory(files[i]); + found = true; + break; + } + } + + if (!found) { + if (lastLevel) + finishError(Networking::ErrorResponse(this, false, true, Common::String("no such file found in its parent directory\n") + _currentDirectoryId, 404)); + else + finishError(Networking::ErrorResponse(this, false, true, "subdirectory not found", 400)); + } +} + +void IdResolveIdRequest::listedDirectoryErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + finishError(error); +} + +void IdResolveIdRequest::handle() {} + +void IdResolveIdRequest::restart() { start(); } + +void IdResolveIdRequest::finishFile(StorageFile file) { + Request::finishSuccess(); + if (_uploadCallback) + (*_uploadCallback)(Storage::UploadResponse(this, file)); +} + +} // End of namespace Id +} // End of namespace Cloud diff --git a/backends/cloud/id/idresolveidrequest.h b/backends/cloud/id/idresolveidrequest.h new file mode 100644 index 0000000000..e735e96385 --- /dev/null +++ b/backends/cloud/id/idresolveidrequest.h @@ -0,0 +1,60 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_ID_IDRESOLVEIDREQUEST_H +#define BACKENDS_CLOUD_ID_IDRESOLVEIDREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/request.h" +#include "common/callback.h" + +namespace Cloud { +namespace Id { + +class IdStorage; + +class IdResolveIdRequest: public Networking::Request { + Common::String _requestedPath; + IdStorage *_storage; + Storage::UploadCallback _uploadCallback; + Common::String _currentDirectory; + Common::String _currentDirectoryId; + Request *_workingRequest; + bool _ignoreCallback; + + void start(); + void listNextDirectory(StorageFile fileToReturn); + void listedDirectoryCallback(Storage::FileArrayResponse response); + void listedDirectoryErrorCallback(Networking::ErrorResponse error); + void finishFile(StorageFile file); +public: + IdResolveIdRequest(IdStorage *storage, Common::String path, Storage::UploadCallback cb, Networking::ErrorCallback ecb, bool recursive = false); //TODO: why upload? + virtual ~IdResolveIdRequest(); + + virtual void handle(); + virtual void restart(); +}; + +} // End of namespace Id +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/id/idstorage.cpp b/backends/cloud/id/idstorage.cpp new file mode 100644 index 0000000000..0ffa8c068c --- /dev/null +++ b/backends/cloud/id/idstorage.cpp @@ -0,0 +1,109 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ +#define FORBIDDEN_SYMBOL_ALLOW_ALL + +#include "backends/cloud/id/idstorage.h" +#include "backends/cloud/id/idcreatedirectoryrequest.h" +#include "backends/cloud/id/iddownloadrequest.h" +#include "backends/cloud/id/idlistdirectoryrequest.h" +#include "backends/cloud/id/idresolveidrequest.h" +#include "backends/cloud/id/idstreamfilerequest.h" +#include "common/debug.h" + +namespace Cloud { +namespace Id { + +IdStorage::~IdStorage() {} + +void IdStorage::printFiles(FileArrayResponse response) { + debug(9, "IdStorage: files:"); + Common::Array<StorageFile> &files = response.value; + for (uint32 i = 0; i < files.size(); ++i) { + debug(9, "\t%s%s", files[i].name().c_str(), files[i].isDirectory() ? " (directory)" : ""); + debug(9, "\t%s", files[i].path().c_str()); + debug(9, "\t%s", files[i].id().c_str()); + debug(9, " "); + } +} + +void IdStorage::printBool(BoolResponse response) { + debug(9, "IdStorage: bool: %s", response.value ? "true" : "false"); +} + +void IdStorage::printFile(UploadResponse response) { + debug(9, "\nIdStorage: uploaded file info:"); + debug(9, "\tid: %s", response.value.path().c_str()); + debug(9, "\tname: %s", response.value.name().c_str()); + debug(9, "\tsize: %u", response.value.size()); + debug(9, "\ttimestamp: %u", response.value.timestamp()); +} + +Storage::ListDirectoryCallback IdStorage::getPrintFilesCallback() { + return new Common::Callback<IdStorage, FileArrayResponse>(this, &IdStorage::printFiles); +} + +Networking::Request *IdStorage::resolveFileId(Common::String path, UploadCallback callback, Networking::ErrorCallback errorCallback) { + if (!errorCallback) + errorCallback = getErrorPrintingCallback(); + if (!callback) + callback = new Common::Callback<IdStorage, UploadResponse>(this, &IdStorage::printFile); + return addRequest(new IdResolveIdRequest(this, path, callback, errorCallback)); +} + +Networking::Request *IdStorage::listDirectory(Common::String path, ListDirectoryCallback callback, Networking::ErrorCallback errorCallback, bool recursive) { + if (!errorCallback) + errorCallback = getErrorPrintingCallback(); + if (!callback) + callback = new Common::Callback<IdStorage, FileArrayResponse>(this, &IdStorage::printFiles); + return addRequest(new IdListDirectoryRequest(this, path, callback, errorCallback, recursive)); +} + +Networking::Request *IdStorage::createDirectory(Common::String path, BoolCallback callback, Networking::ErrorCallback errorCallback) { + if (!errorCallback) + errorCallback = getErrorPrintingCallback(); + if (!callback) + callback = new Common::Callback<IdStorage, BoolResponse>(this, &IdStorage::printBool); + + //find out the parent path and directory name + Common::String parentPath = "", directoryName = path; + for (uint32 i = path.size(); i > 0; --i) { + if (path[i - 1] == '/' || path[i - 1] == '\\') { + parentPath = path; + parentPath.erase(i - 1); + directoryName.erase(0, i); + break; + } + } + + return addRequest(new IdCreateDirectoryRequest(this, parentPath, directoryName, callback, errorCallback)); +} + +Networking::Request *IdStorage::streamFile(Common::String path, Networking::NetworkReadStreamCallback outerCallback, Networking::ErrorCallback errorCallback) { + return addRequest(new IdStreamFileRequest(this, path, outerCallback, errorCallback)); +} + +Networking::Request *IdStorage::download(Common::String remotePath, Common::String localPath, BoolCallback callback, Networking::ErrorCallback errorCallback) { + return addRequest(new IdDownloadRequest(this, remotePath, localPath, callback, errorCallback)); +} + +} // End of namespace Id +} // End of namespace Cloud diff --git a/backends/cloud/id/idstorage.h b/backends/cloud/id/idstorage.h new file mode 100644 index 0000000000..7e64fd4a37 --- /dev/null +++ b/backends/cloud/id/idstorage.h @@ -0,0 +1,83 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_ID_IDSTORAGE_H +#define BACKENDS_CLOUD_ID_IDSTORAGE_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/curljsonrequest.h" + +/* + * Id::IdStorage is a special base class, which is created + * to simplify adding new storages which use ids instead of + * paths in their API. + * + * Some Requests are already implemented, and Storage based + * on IdStorage needs to override/implement a few basic things. + * + * For example, ListDirectoryRequest and ResolveIdRequests are + * based on listDirectoryById() and getRootDirectoryId() methods. + * Implementing these you'll get id resolving and directory + * listing by path. + */ + +namespace Cloud { +namespace Id { + +class IdStorage: public Cloud::Storage { +protected: + void printFiles(FileArrayResponse response); + void printBool(BoolResponse response); + void printFile(UploadResponse response); + + ListDirectoryCallback getPrintFilesCallback(); + +public: + virtual ~IdStorage(); + + /** Public Cloud API comes down there. */ + + /** Returns StorageFile with the resolved file's id. */ + virtual Networking::Request *resolveFileId(Common::String path, UploadCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns ListDirectoryStatus struct with list of files. */ + virtual Networking::Request *listDirectory(Common::String path, ListDirectoryCallback callback, Networking::ErrorCallback errorCallback, bool recursive = false); + virtual Networking::Request *listDirectoryById(Common::String id, ListDirectoryCallback callback, Networking::ErrorCallback errorCallback) = 0; + + /** Calls the callback when finished. */ + virtual Networking::Request *createDirectory(Common::String path, BoolCallback callback, Networking::ErrorCallback errorCallback); + virtual Networking::Request *createDirectoryWithParentId(Common::String parentId, Common::String name, BoolCallback callback, Networking::ErrorCallback errorCallback) = 0; + + /** Returns pointer to Networking::NetworkReadStream. */ + virtual Networking::Request *streamFile(Common::String path, Networking::NetworkReadStreamCallback callback, Networking::ErrorCallback errorCallback); + virtual Networking::Request *streamFileById(Common::String id, Networking::NetworkReadStreamCallback callback, Networking::ErrorCallback errorCallback) = 0; + + /** Calls the callback when finished. */ + virtual Networking::Request *download(Common::String remotePath, Common::String localPath, BoolCallback callback, Networking::ErrorCallback errorCallback); + + virtual Common::String getRootDirectoryId() = 0; +}; + +} // End of namespace Id +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/id/idstreamfilerequest.cpp b/backends/cloud/id/idstreamfilerequest.cpp new file mode 100644 index 0000000000..2e68b15412 --- /dev/null +++ b/backends/cloud/id/idstreamfilerequest.cpp @@ -0,0 +1,98 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/cloud/id/idstreamfilerequest.h" +#include "backends/cloud/id/idstorage.h" + +namespace Cloud { +namespace Id { + +IdStreamFileRequest::IdStreamFileRequest(IdStorage *storage, Common::String path, Networking::NetworkReadStreamCallback cb, Networking::ErrorCallback ecb): + Networking::Request(nullptr, ecb), _requestedFile(path), _storage(storage), _streamCallback(cb), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +IdStreamFileRequest::~IdStreamFileRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _streamCallback; +} + +void IdStreamFileRequest::start() { + //cleanup + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _workingRequest = nullptr; + _ignoreCallback = false; + + //find file's id + Storage::UploadCallback innerCallback = new Common::Callback<IdStreamFileRequest, Storage::UploadResponse>(this, &IdStreamFileRequest::idResolvedCallback); + Networking::ErrorCallback innerErrorCallback = new Common::Callback<IdStreamFileRequest, Networking::ErrorResponse>(this, &IdStreamFileRequest::idResolveFailedCallback); + _workingRequest = _storage->resolveFileId(_requestedFile, innerCallback, innerErrorCallback); +} + +void IdStreamFileRequest::idResolvedCallback(Storage::UploadResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + Networking::NetworkReadStreamCallback innerCallback = new Common::Callback<IdStreamFileRequest, Networking::NetworkReadStreamResponse>(this, &IdStreamFileRequest::streamFileCallback); + Networking::ErrorCallback innerErrorCallback = new Common::Callback<IdStreamFileRequest, Networking::ErrorResponse>(this, &IdStreamFileRequest::streamFileErrorCallback); + _workingRequest = _storage->streamFileById(response.value.id(), innerCallback, innerErrorCallback); +} + +void IdStreamFileRequest::idResolveFailedCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + finishError(error); +} + +void IdStreamFileRequest::streamFileCallback(Networking::NetworkReadStreamResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + finishStream(response.value); +} + +void IdStreamFileRequest::streamFileErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + finishError(error); +} + +void IdStreamFileRequest::handle() {} + +void IdStreamFileRequest::restart() { start(); } + +void IdStreamFileRequest::finishStream(Networking::NetworkReadStream *stream) { + Request::finishSuccess(); + if (_streamCallback) + (*_streamCallback)(Networking::NetworkReadStreamResponse(this, stream)); +} + +} // End of namespace Id +} // End of namespace Cloud diff --git a/backends/cloud/id/idstreamfilerequest.h b/backends/cloud/id/idstreamfilerequest.h new file mode 100644 index 0000000000..20d7c0c25b --- /dev/null +++ b/backends/cloud/id/idstreamfilerequest.h @@ -0,0 +1,59 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_ID_IDSTREAMFILEREQUEST_H +#define BACKENDS_CLOUD_ID_IDSTREAMFILEREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/request.h" +#include "common/callback.h" + +namespace Cloud { +namespace Id { + +class IdStorage; + +class IdStreamFileRequest: public Networking::Request { + Common::String _requestedFile; + IdStorage *_storage; + Networking::NetworkReadStreamCallback _streamCallback; + Request *_workingRequest; + bool _ignoreCallback; + + void start(); + void idResolvedCallback(Storage::UploadResponse response); + void idResolveFailedCallback(Networking::ErrorResponse error); + void streamFileCallback(Networking::NetworkReadStreamResponse response); + void streamFileErrorCallback(Networking::ErrorResponse error); + void finishStream(Networking::NetworkReadStream *stream); +public: + IdStreamFileRequest(IdStorage *storage, Common::String path, Networking::NetworkReadStreamCallback cb, Networking::ErrorCallback ecb); + virtual ~IdStreamFileRequest(); + + virtual void handle(); + virtual void restart(); +}; + +} // End of namespace Id +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/iso8601.cpp b/backends/cloud/iso8601.cpp new file mode 100644 index 0000000000..177ef67f11 --- /dev/null +++ b/backends/cloud/iso8601.cpp @@ -0,0 +1,100 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/cloud/iso8601.h" +#include "common/str.h" + +namespace { + +Common::String getSubstring(const Common::String &s, uint32 beginning, uint32 ending) { + //beginning inclusive, ending exclusive + Common::String result = s; + result.erase(ending); + result.erase(0, beginning); + return result; +} + +int find(const char *cstr, uint32 startPosition, char needle) { + const char *res = strchr(cstr + startPosition, needle); + if (res == nullptr) + return -1; + return res - cstr; +} + +} + +namespace Cloud { +namespace ISO8601 { + +uint32 convertToTimestamp(const Common::String &iso8601Date) { + //2015-05-12T15:50:38Z + const char *cstr = iso8601Date.c_str(); + int firstHyphen = find(cstr, 0, '-'); + int secondHyphen = find(cstr, firstHyphen + 1, '-'); + int tSeparator = find(cstr, secondHyphen + 1, 'T'); + int firstColon = find(cstr, tSeparator + 1, ':'); + int secondColon = find(cstr, firstColon + 1, ':'); + int zSeparator = find(cstr, secondColon + 1, 'Z'); + if (zSeparator == -1) + zSeparator = find(cstr, secondColon + 1, '-'); // Box's RFC 3339 + //now note '+1' which means if there ever was '-1' result of find(), we still did a valid find() from 0th char + + Common::String year = getSubstring(iso8601Date, 0, firstHyphen); + Common::String month = getSubstring(iso8601Date, firstHyphen + 1, secondHyphen); + Common::String day = getSubstring(iso8601Date, secondHyphen + 1, tSeparator); + Common::String hour = getSubstring(iso8601Date, tSeparator + 1, firstColon); + Common::String minute = getSubstring(iso8601Date, firstColon + 1, secondColon); + Common::String second = getSubstring(iso8601Date, secondColon + 1, zSeparator); + //now note only 'ending' argument was not '+1' (which means I could've make that function such that -1 means 'until the end') + + int Y = atoi(year.c_str()); + int M = atoi(month.c_str()); + int D = atoi(day.c_str()); + int h = atoi(hour.c_str()); + int m = atoi(minute.c_str()); + int s = atoi(second.c_str()); + + //ok, now I compose a timestamp based on my basic perception of time/date + //yeah, I know about leap years and leap seconds and all, but still we don't care there + + uint32 days = D - 1; + for (int i = 1970; i < Y; ++i) + if ((i % 4 == 0 && i % 100 != 0) || (i % 400 == 0)) + days += 366; + else + days += 365; + + int mdays[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + for (int i = 1; i < M; ++i) { + days += mdays[i - 1]; + if (i == 2) + if ((Y % 4 == 0 && Y % 100 != 0) || (Y % 400 == 0)) + days += 1; + } + + uint32 hours = days * 24 + h; + uint32 minutes = hours * 60 + m; + return minutes * 60 + s; +} + +} // End of namespace ISO8601 +} // End of namespace Cloud diff --git a/backends/cloud/iso8601.h b/backends/cloud/iso8601.h new file mode 100644 index 0000000000..cdd817bc07 --- /dev/null +++ b/backends/cloud/iso8601.h @@ -0,0 +1,37 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_ISO8601_H +#define BACKENDS_CLOUD_ISO8601_H + +#include "common/str.h" + +namespace Cloud { +namespace ISO8601 { + +/** Returns timestamp corresponding to given ISO 8601 date */ +uint32 convertToTimestamp(const Common::String &iso8601Date); + +} // End of namespace ISO8601 +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/onedrive/onedrivecreatedirectoryrequest.cpp b/backends/cloud/onedrive/onedrivecreatedirectoryrequest.cpp new file mode 100644 index 0000000000..fc7e4f58b0 --- /dev/null +++ b/backends/cloud/onedrive/onedrivecreatedirectoryrequest.cpp @@ -0,0 +1,150 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/cloud/onedrive/onedrivecreatedirectoryrequest.h" +#include "backends/cloud/onedrive/onedrivestorage.h" +#include "backends/cloud/onedrive/onedrivetokenrefresher.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/json.h" + +namespace Cloud { +namespace OneDrive { + +#define ONEDRIVE_API_SPECIAL_APPROOT "https://api.onedrive.com/v1.0/drive/special/approot" + +OneDriveCreateDirectoryRequest::OneDriveCreateDirectoryRequest(OneDriveStorage *storage, Common::String path, Storage::BoolCallback cb, Networking::ErrorCallback ecb): + Networking::Request(nullptr, ecb), _storage(storage), _path(path), _boolCallback(cb), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +OneDriveCreateDirectoryRequest::~OneDriveCreateDirectoryRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _boolCallback; +} + +void OneDriveCreateDirectoryRequest::start() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _ignoreCallback = false; + + Common::String name = _path, parent = _path; + if (name.size() != 0) { + uint32 i = name.size() - 1; + while (true) { + parent.deleteLastChar(); + if (name[i] == '/' || name[i] == '\\') { + name.erase(0, i + 1); + break; + } + if (i == 0) + break; + --i; + } + } + + Common::String url = ONEDRIVE_API_SPECIAL_APPROOT; + if (parent != "") + url += ":/" + ConnMan.urlEncode(parent) + ":"; + url += "/children"; + Networking::JsonCallback innerCallback = new Common::Callback<OneDriveCreateDirectoryRequest, Networking::JsonResponse>(this, &OneDriveCreateDirectoryRequest::responseCallback); + Networking::ErrorCallback errorCallback = new Common::Callback<OneDriveCreateDirectoryRequest, Networking::ErrorResponse>(this, &OneDriveCreateDirectoryRequest::errorCallback); + Networking::CurlJsonRequest *request = new OneDriveTokenRefresher(_storage, innerCallback, errorCallback, url.c_str()); + request->addHeader("Authorization: Bearer " + _storage->accessToken()); + request->addHeader("Content-Type: application/json"); + + Common::JSONObject jsonRequestParameters; + jsonRequestParameters.setVal("name", new Common::JSONValue(name)); + jsonRequestParameters.setVal("folder", new Common::JSONValue(Common::JSONObject())); + Common::JSONValue value(jsonRequestParameters); + request->addPostField(Common::JSON::stringify(&value)); + + _workingRequest = ConnMan.addRequest(request); +} + +void OneDriveCreateDirectoryRequest::responseCallback(Networking::JsonResponse response) { + Common::JSONValue *json = response.value; + _workingRequest = nullptr; + if (_ignoreCallback) { + delete json; + return; + } + if (response.request) + _date = response.request->date(); + + Networking::ErrorResponse error(this); + Networking::CurlJsonRequest *rq = (Networking::CurlJsonRequest *)response.request; + if (rq && rq->getNetworkReadStream()) + error.httpResponseCode = rq->getNetworkReadStream()->httpResponseCode(); + + if (json == nullptr) { + error.response = "Failed to parse JSON, null passed!"; + finishError(error); + return; + } + + if (!json->isObject()) { + error.response = "Passed JSON is not an object!"; + finishError(error); + delete json; + return; + } + + Common::JSONObject info = json->asObject(); + if (info.contains("id")) { + finishCreation(true); + } else { + error.response = json->stringify(true); + finishError(error); + } + + delete json; +} + +void OneDriveCreateDirectoryRequest::errorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (error.request) + _date = error.request->date(); + finishError(error); +} + +void OneDriveCreateDirectoryRequest::handle() {} + +void OneDriveCreateDirectoryRequest::restart() { start(); } + +Common::String OneDriveCreateDirectoryRequest::date() const { return _date; } + +void OneDriveCreateDirectoryRequest::finishCreation(bool success) { + Request::finishSuccess(); + if (_boolCallback) + (*_boolCallback)(Storage::BoolResponse(this, success)); +} + +} // End of namespace OneDrive +} // End of namespace Cloud diff --git a/backends/cloud/onedrive/onedrivecreatedirectoryrequest.h b/backends/cloud/onedrive/onedrivecreatedirectoryrequest.h new file mode 100644 index 0000000000..acaca2bf00 --- /dev/null +++ b/backends/cloud/onedrive/onedrivecreatedirectoryrequest.h @@ -0,0 +1,59 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_ONEDRIVE_ONEDRIVECREATEDIRECTORYREQUEST_H +#define BACKENDS_CLOUD_ONEDRIVE_ONEDRIVECREATEDIRECTORYREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/request.h" +#include "backends/networking/curl/curljsonrequest.h" + +namespace Cloud { +namespace OneDrive { + +class OneDriveStorage; + +class OneDriveCreateDirectoryRequest: public Networking::Request { + OneDriveStorage *_storage; + Common::String _path; + Storage::BoolCallback _boolCallback; + Request *_workingRequest; + bool _ignoreCallback; + Common::String _date; + + void start(); + void responseCallback(Networking::JsonResponse response); + void errorCallback(Networking::ErrorResponse error); + void finishCreation(bool success); +public: + OneDriveCreateDirectoryRequest(OneDriveStorage *storage, Common::String path, Storage::BoolCallback cb, Networking::ErrorCallback ecb); + virtual ~OneDriveCreateDirectoryRequest(); + + virtual void handle(); + virtual void restart(); + virtual Common::String date() const; +}; + +} // End of namespace OneDrive +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/onedrive/onedrivelistdirectoryrequest.cpp b/backends/cloud/onedrive/onedrivelistdirectoryrequest.cpp new file mode 100644 index 0000000000..a247a9f234 --- /dev/null +++ b/backends/cloud/onedrive/onedrivelistdirectoryrequest.cpp @@ -0,0 +1,193 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/cloud/onedrive/onedrivelistdirectoryrequest.h" +#include "backends/cloud/onedrive/onedrivestorage.h" +#include "backends/cloud/onedrive/onedrivetokenrefresher.h" +#include "backends/cloud/iso8601.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/json.h" + +namespace Cloud { +namespace OneDrive { + +#define ONEDRIVE_API_SPECIAL_APPROOT_CHILDREN "https://api.onedrive.com/v1.0/drive/special/approot:/%s:/children" + +OneDriveListDirectoryRequest::OneDriveListDirectoryRequest(OneDriveStorage *storage, Common::String path, Storage::ListDirectoryCallback cb, Networking::ErrorCallback ecb, bool recursive): + Networking::Request(nullptr, ecb), + _requestedPath(path), _requestedRecursive(recursive), _storage(storage), _listDirectoryCallback(cb), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +OneDriveListDirectoryRequest::~OneDriveListDirectoryRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _listDirectoryCallback; +} + +void OneDriveListDirectoryRequest::start() { + //cleanup + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _workingRequest = nullptr; + _files.clear(); + _directoriesQueue.clear(); + _currentDirectory = ""; + _ignoreCallback = false; + + _directoriesQueue.push_back(_requestedPath); + listNextDirectory(); +} + +void OneDriveListDirectoryRequest::listNextDirectory() { + if (_directoriesQueue.empty()) { + finishListing(_files); + return; + } + + _currentDirectory = _directoriesQueue.back(); + _directoriesQueue.pop_back(); + + if (_currentDirectory != "" && _currentDirectory.lastChar() != '/' && _currentDirectory.lastChar() != '\\') + _currentDirectory += '/'; + + Common::String dir = _currentDirectory; + dir.deleteLastChar(); + Common::String url = Common::String::format(ONEDRIVE_API_SPECIAL_APPROOT_CHILDREN, ConnMan.urlEncode(dir).c_str()); + makeRequest(url); +} + +void OneDriveListDirectoryRequest::makeRequest(Common::String url) { + Networking::JsonCallback callback = new Common::Callback<OneDriveListDirectoryRequest, Networking::JsonResponse>(this, &OneDriveListDirectoryRequest::listedDirectoryCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<OneDriveListDirectoryRequest, Networking::ErrorResponse>(this, &OneDriveListDirectoryRequest::listedDirectoryErrorCallback); + Networking::CurlJsonRequest *request = new OneDriveTokenRefresher(_storage, callback, failureCallback, url.c_str()); + request->addHeader("Authorization: Bearer " + _storage->accessToken()); + _workingRequest = ConnMan.addRequest(request); +} + +void OneDriveListDirectoryRequest::listedDirectoryCallback(Networking::JsonResponse response) { + _workingRequest = nullptr; + Common::JSONValue *json = response.value; + + if (_ignoreCallback) { + delete json; + return; + } + + if (response.request) + _date = response.request->date(); + + Networking::ErrorResponse error(this); + Networking::CurlJsonRequest *rq = (Networking::CurlJsonRequest *)response.request; + if (rq && rq->getNetworkReadStream()) + error.httpResponseCode = rq->getNetworkReadStream()->httpResponseCode(); + + if (json == nullptr) { + error.response = "Failed to parse JSON, null passed!"; + finishError(error); + return; + } + + if (!json->isObject()) { + error.response = "Passed JSON is not an object!"; + finishError(error); + delete json; + return; + } + + Common::JSONObject object = json->asObject(); + + //check that ALL keys exist AND HAVE RIGHT TYPE to avoid segfaults + if (!Networking::CurlJsonRequest::jsonContainsArray(object, "value", "OneDriveListDirectoryRequest")) { + error.response = "\"value\" not found or that's not an array!"; + finishError(error); + delete json; + return; + } + + Common::JSONArray items = object.getVal("value")->asArray(); + for (uint32 i = 0; i < items.size(); ++i) { + if (!Networking::CurlJsonRequest::jsonIsObject(items[i], "OneDriveListDirectoryRequest")) continue; + + Common::JSONObject item = items[i]->asObject(); + + if (!Networking::CurlJsonRequest::jsonContainsAttribute(item, "folder", "OneDriveListDirectoryRequest", true)) continue; + if (!Networking::CurlJsonRequest::jsonContainsString(item, "name", "OneDriveListDirectoryRequest")) continue; + if (!Networking::CurlJsonRequest::jsonContainsIntegerNumber(item, "size", "OneDriveListDirectoryRequest")) continue; + if (!Networking::CurlJsonRequest::jsonContainsString(item, "lastModifiedDateTime", "OneDriveListDirectoryRequest")) continue; + + Common::String path = _currentDirectory + item.getVal("name")->asString(); + bool isDirectory = item.contains("folder"); + uint32 size = item.getVal("size")->asIntegerNumber(); + uint32 timestamp = ISO8601::convertToTimestamp(item.getVal("lastModifiedDateTime")->asString()); + + StorageFile file(path, size, timestamp, isDirectory); + _files.push_back(file); + if (_requestedRecursive && file.isDirectory()) { + _directoriesQueue.push_back(file.path()); + } + } + + bool hasMore = object.contains("@odata.nextLink"); + if (hasMore) { + if (!Networking::CurlJsonRequest::jsonContainsString(object, "@odata.nextLink", "OneDriveListDirectoryRequest")) { + error.response = "\"@odata.nextLink\" is not a string!"; + finishError(error); + delete json; + return; + } + + makeRequest(object.getVal("@odata.nextLink")->asString()); + } else { + listNextDirectory(); + } + + delete json; +} + +void OneDriveListDirectoryRequest::listedDirectoryErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (error.request) + _date = error.request->date(); + finishError(error); +} + +void OneDriveListDirectoryRequest::handle() {} + +void OneDriveListDirectoryRequest::restart() { start(); } + +Common::String OneDriveListDirectoryRequest::date() const { return _date; } + +void OneDriveListDirectoryRequest::finishListing(Common::Array<StorageFile> &files) { + Request::finishSuccess(); + if (_listDirectoryCallback) + (*_listDirectoryCallback)(Storage::ListDirectoryResponse(this, files)); +} + +} // End of namespace OneDrive +} // End of namespace Cloud diff --git a/backends/cloud/onedrive/onedrivelistdirectoryrequest.h b/backends/cloud/onedrive/onedrivelistdirectoryrequest.h new file mode 100644 index 0000000000..eb510ab257 --- /dev/null +++ b/backends/cloud/onedrive/onedrivelistdirectoryrequest.h @@ -0,0 +1,66 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_ONEDRIVE_ONEDRIVELISTDIRECTORYREQUEST_H +#define BACKENDS_CLOUD_ONEDRIVE_ONEDRIVELISTDIRECTORYREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/request.h" +#include "common/callback.h" + +namespace Cloud { +namespace OneDrive { + +class OneDriveStorage; + +class OneDriveListDirectoryRequest: public Networking::Request { + Common::String _requestedPath; + bool _requestedRecursive; + OneDriveStorage *_storage; + Storage::ListDirectoryCallback _listDirectoryCallback; + Common::Array<StorageFile> _files; + Common::Array<Common::String> _directoriesQueue; + Common::String _currentDirectory; + Request *_workingRequest; + bool _ignoreCallback; + Common::String _date; + + void start(); + void listNextDirectory(); + void listedDirectoryCallback(Networking::JsonResponse response); + void listedDirectoryErrorCallback(Networking::ErrorResponse error); + void makeRequest(Common::String url); + void finishListing(Common::Array<StorageFile> &files); +public: + OneDriveListDirectoryRequest(OneDriveStorage *storage, Common::String path, Storage::ListDirectoryCallback cb, Networking::ErrorCallback ecb, bool recursive = false); + virtual ~OneDriveListDirectoryRequest(); + + virtual void handle(); + virtual void restart(); + virtual Common::String date() const; +}; + +} // End of namespace OneDrive +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/onedrive/onedrivestorage.cpp b/backends/cloud/onedrive/onedrivestorage.cpp new file mode 100644 index 0000000000..4b70bb73b9 --- /dev/null +++ b/backends/cloud/onedrive/onedrivestorage.cpp @@ -0,0 +1,326 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ +#define FORBIDDEN_SYMBOL_ALLOW_ALL + +#include "backends/cloud/onedrive/onedrivestorage.h" +#include "backends/cloud/cloudmanager.h" +#include "backends/cloud/onedrive/onedrivecreatedirectoryrequest.h" +#include "backends/cloud/onedrive/onedrivetokenrefresher.h" +#include "backends/cloud/onedrive/onedrivelistdirectoryrequest.h" +#include "backends/cloud/onedrive/onedriveuploadrequest.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/config-manager.h" +#include "common/debug.h" +#include "common/json.h" +#include <curl/curl.h> + +#ifdef ENABLE_RELEASE +#include "dists/clouds/cloud_keys.h" +#endif + +namespace Cloud { +namespace OneDrive { + +#define ONEDRIVE_OAUTH2_TOKEN "https://login.live.com/oauth20_token.srf" +#define ONEDRIVE_API_SPECIAL_APPROOT_ID "https://api.onedrive.com/v1.0/drive/special/approot:/" +#define ONEDRIVE_API_SPECIAL_APPROOT "https://api.onedrive.com/v1.0/drive/special/approot" + +char *OneDriveStorage::KEY = nullptr; //can't use CloudConfig there yet, loading it on instance creation/auth +char *OneDriveStorage::SECRET = nullptr; + +void OneDriveStorage::loadKeyAndSecret() { +#ifdef ENABLE_RELEASE + KEY = RELEASE_ONEDRIVE_KEY; + SECRET = RELEASE_ONEDRIVE_SECRET; +#else + Common::String k = ConfMan.get("ONEDRIVE_KEY", ConfMan.kCloudDomain); + KEY = new char[k.size() + 1]; + memcpy(KEY, k.c_str(), k.size()); + KEY[k.size()] = 0; + + k = ConfMan.get("ONEDRIVE_SECRET", ConfMan.kCloudDomain); + SECRET = new char[k.size() + 1]; + memcpy(SECRET, k.c_str(), k.size()); + SECRET[k.size()] = 0; +#endif +} + +OneDriveStorage::OneDriveStorage(Common::String accessToken, Common::String userId, Common::String refreshToken): + _token(accessToken), _uid(userId), _refreshToken(refreshToken) {} + +OneDriveStorage::OneDriveStorage(Common::String code) { + getAccessToken( + new Common::Callback<OneDriveStorage, BoolResponse>(this, &OneDriveStorage::codeFlowComplete), + new Common::Callback<OneDriveStorage, Networking::ErrorResponse>(this, &OneDriveStorage::codeFlowFailed), + code + ); +} + +OneDriveStorage::~OneDriveStorage() {} + +void OneDriveStorage::getAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback, Common::String code) { + if (!KEY || !SECRET) + loadKeyAndSecret(); + bool codeFlow = (code != ""); + + if (!codeFlow && _refreshToken == "") { + warning("OneDriveStorage: no refresh token available to get new access token."); + if (callback) + (*callback)(BoolResponse(nullptr, false)); + return; + } + + Networking::JsonCallback innerCallback = new Common::CallbackBridge<OneDriveStorage, BoolResponse, Networking::JsonResponse>(this, &OneDriveStorage::tokenRefreshed, callback); + if (errorCallback == nullptr) + errorCallback = getErrorPrintingCallback(); + Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(innerCallback, errorCallback, ONEDRIVE_OAUTH2_TOKEN); + if (codeFlow) { + request->addPostField("code=" + code); + request->addPostField("grant_type=authorization_code"); + } else { + request->addPostField("refresh_token=" + _refreshToken); + request->addPostField("grant_type=refresh_token"); + } + request->addPostField("client_id=" + Common::String(KEY)); + request->addPostField("client_secret=" + Common::String(SECRET)); + if (Cloud::CloudManager::couldUseLocalServer()) { + request->addPostField("&redirect_uri=http%3A%2F%2Flocalhost%3A12345%2F"); + } else { + request->addPostField("&redirect_uri=https%3A%2F%2Fwww.scummvm.org/c/code"); + } + addRequest(request); +} + +void OneDriveStorage::tokenRefreshed(BoolCallback callback, Networking::JsonResponse response) { + Common::JSONValue *json = response.value; + if (!json) { + warning("OneDriveStorage: got NULL instead of JSON"); + if (callback) + (*callback)(BoolResponse(nullptr, false)); + delete callback; + return; + } + + if (!Networking::CurlJsonRequest::jsonIsObject(json, "OneDriveStorage")) { + if (callback) + (*callback)(BoolResponse(nullptr, false)); + delete json; + delete callback; + return; + } + + Common::JSONObject result = json->asObject(); + if (!Networking::CurlJsonRequest::jsonContainsString(result, "access_token", "OneDriveStorage") || + !Networking::CurlJsonRequest::jsonContainsString(result, "user_id", "OneDriveStorage") || + !Networking::CurlJsonRequest::jsonContainsString(result, "refresh_token", "OneDriveStorage")) { + warning("OneDriveStorage: bad response, no token or user_id passed"); + debug(9, "%s", json->stringify().c_str()); + if (callback) + (*callback)(BoolResponse(nullptr, false)); + } else { + _token = result.getVal("access_token")->asString(); + _uid = result.getVal("user_id")->asString(); + _refreshToken = result.getVal("refresh_token")->asString(); + CloudMan.save(); //ask CloudManager to save our new refreshToken + if (callback) + (*callback)(BoolResponse(nullptr, true)); + } + delete json; + delete callback; +} + +void OneDriveStorage::codeFlowComplete(BoolResponse response) { + if (!response.value) { + warning("OneDriveStorage: failed to get access token through code flow"); + CloudMan.removeStorage(this); + return; + } + + ConfMan.removeKey("onedrive_code", ConfMan.kCloudDomain); + CloudMan.replaceStorage(this, kStorageOneDriveId); + ConfMan.flushToDisk(); +} + +void OneDriveStorage::codeFlowFailed(Networking::ErrorResponse error) { + debug(9, "OneDriveStorage: code flow failed (%s, %ld):", (error.failed ? "failed" : "interrupted"), error.httpResponseCode); + debug(9, "%s", error.response.c_str()); + CloudMan.removeStorage(this); +} + +void OneDriveStorage::saveConfig(Common::String keyPrefix) { + ConfMan.set(keyPrefix + "access_token", _token, ConfMan.kCloudDomain); + ConfMan.set(keyPrefix + "user_id", _uid, ConfMan.kCloudDomain); + ConfMan.set(keyPrefix + "refresh_token", _refreshToken, ConfMan.kCloudDomain); +} + +Common::String OneDriveStorage::name() const { + return "OneDrive"; +} + +void OneDriveStorage::infoInnerCallback(StorageInfoCallback outerCallback, Networking::JsonResponse response) { + Common::JSONValue *json = response.value; + if (!json) { + warning("OneDriveStorage::infoInnerCallback: NULL passed instead of JSON"); + delete outerCallback; + return; + } + + if (!Networking::CurlJsonRequest::jsonIsObject(json, "OneDriveStorage::infoInnerCallback")) { + delete json; + delete outerCallback; + return; + } + + Common::JSONObject info = json->asObject(); + + Common::String uid, name, email; + uint64 quotaUsed = 0, quotaAllocated = 26843545600L; // 25 GB, because I actually don't know any way to find out the real one + + if (Networking::CurlJsonRequest::jsonContainsObject(info, "createdBy", "OneDriveStorage::infoInnerCallback")) { + Common::JSONObject createdBy = info.getVal("createdBy")->asObject(); + if (Networking::CurlJsonRequest::jsonContainsObject(createdBy, "user", "OneDriveStorage::infoInnerCallback")) { + Common::JSONObject user = createdBy.getVal("user")->asObject(); + if (Networking::CurlJsonRequest::jsonContainsString(user, "id", "OneDriveStorage::infoInnerCallback")) + uid = user.getVal("id")->asString(); + if (Networking::CurlJsonRequest::jsonContainsString(user, "displayName", "OneDriveStorage::infoInnerCallback")) + name = user.getVal("displayName")->asString(); + } + } + + if (Networking::CurlJsonRequest::jsonContainsIntegerNumber(info, "size", "OneDriveStorage::infoInnerCallback")) { + quotaUsed = info.getVal("size")->asIntegerNumber(); + } + + Common::String username = email; + if (username == "") + username = name; + if (username == "") + username = uid; + CloudMan.setStorageUsername(kStorageOneDriveId, username); + + if (outerCallback) { + (*outerCallback)(StorageInfoResponse(nullptr, StorageInfo(uid, name, email, quotaUsed, quotaAllocated))); + delete outerCallback; + } + + delete json; +} + +void OneDriveStorage::fileInfoCallback(Networking::NetworkReadStreamCallback outerCallback, Networking::JsonResponse response) { + Common::JSONValue *json = response.value; + if (!json) { + warning("OneDriveStorage::fileInfoCallback: NULL passed instead of JSON"); + if (outerCallback) + (*outerCallback)(Networking::NetworkReadStreamResponse(response.request, nullptr)); + delete outerCallback; + return; + } + + if (!Networking::CurlJsonRequest::jsonIsObject(json, "OneDriveStorage::fileInfoCallback")) { + if (outerCallback) + (*outerCallback)(Networking::NetworkReadStreamResponse(response.request, nullptr)); + delete json; + delete outerCallback; + return; + } + + Common::JSONObject result = response.value->asObject(); + if (!Networking::CurlJsonRequest::jsonContainsString(result, "@content.downloadUrl", "OneDriveStorage::fileInfoCallback")) { + warning("OneDriveStorage: downloadUrl not found in passed JSON"); + debug(9, "%s", response.value->stringify().c_str()); + if (outerCallback) + (*outerCallback)(Networking::NetworkReadStreamResponse(response.request, nullptr)); + delete json; + delete outerCallback; + return; + } + + const char *url = result.getVal("@content.downloadUrl")->asString().c_str(); + if (outerCallback) + (*outerCallback)(Networking::NetworkReadStreamResponse( + response.request, + new Networking::NetworkReadStream(url, nullptr, "") + )); + + delete json; + delete outerCallback; +} + +Networking::Request *OneDriveStorage::listDirectory(Common::String path, ListDirectoryCallback callback, Networking::ErrorCallback errorCallback, bool recursive) { + return addRequest(new OneDriveListDirectoryRequest(this, path, callback, errorCallback, recursive)); +} + +Networking::Request *OneDriveStorage::upload(Common::String path, Common::SeekableReadStream *contents, UploadCallback callback, Networking::ErrorCallback errorCallback) { + return addRequest(new OneDriveUploadRequest(this, path, contents, callback, errorCallback)); +} + +Networking::Request *OneDriveStorage::streamFileById(Common::String path, Networking::NetworkReadStreamCallback outerCallback, Networking::ErrorCallback errorCallback) { + Common::String url = ONEDRIVE_API_SPECIAL_APPROOT_ID + ConnMan.urlEncode(path); + Networking::JsonCallback innerCallback = new Common::CallbackBridge<OneDriveStorage, Networking::NetworkReadStreamResponse, Networking::JsonResponse>(this, &OneDriveStorage::fileInfoCallback, outerCallback); + Networking::CurlJsonRequest *request = new OneDriveTokenRefresher(this, innerCallback, errorCallback, url.c_str()); + request->addHeader("Authorization: Bearer " + _token); + return addRequest(request); +} + +Networking::Request *OneDriveStorage::createDirectory(Common::String path, BoolCallback callback, Networking::ErrorCallback errorCallback) { + if (!errorCallback) + errorCallback = getErrorPrintingCallback(); + return addRequest(new OneDriveCreateDirectoryRequest(this, path, callback, errorCallback)); +} + +Networking::Request *OneDriveStorage::info(StorageInfoCallback callback, Networking::ErrorCallback errorCallback) { + Networking::JsonCallback innerCallback = new Common::CallbackBridge<OneDriveStorage, StorageInfoResponse, Networking::JsonResponse>(this, &OneDriveStorage::infoInnerCallback, callback); + Networking::CurlJsonRequest *request = new OneDriveTokenRefresher(this, innerCallback, errorCallback, ONEDRIVE_API_SPECIAL_APPROOT); + request->addHeader("Authorization: bearer " + _token); + return addRequest(request); +} + +Common::String OneDriveStorage::savesDirectoryPath() { return "saves/"; } + +OneDriveStorage *OneDriveStorage::loadFromConfig(Common::String keyPrefix) { + loadKeyAndSecret(); + + if (!ConfMan.hasKey(keyPrefix + "access_token", ConfMan.kCloudDomain)) { + warning("OneDriveStorage: no access_token found"); + return nullptr; + } + + if (!ConfMan.hasKey(keyPrefix + "user_id", ConfMan.kCloudDomain)) { + warning("OneDriveStorage: no user_id found"); + return nullptr; + } + + if (!ConfMan.hasKey(keyPrefix + "refresh_token", ConfMan.kCloudDomain)) { + warning("OneDriveStorage: no refresh_token found"); + return nullptr; + } + + Common::String accessToken = ConfMan.get(keyPrefix + "access_token", ConfMan.kCloudDomain); + Common::String userId = ConfMan.get(keyPrefix + "user_id", ConfMan.kCloudDomain); + Common::String refreshToken = ConfMan.get(keyPrefix + "refresh_token", ConfMan.kCloudDomain); + return new OneDriveStorage(accessToken, userId, refreshToken); +} + +} // End of namespace OneDrive +} // End of namespace Cloud diff --git a/backends/cloud/onedrive/onedrivestorage.h b/backends/cloud/onedrive/onedrivestorage.h new file mode 100644 index 0000000000..59c61074e3 --- /dev/null +++ b/backends/cloud/onedrive/onedrivestorage.h @@ -0,0 +1,113 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_ONEDRIVE_ONEDRIVESTORAGE_H +#define BACKENDS_CLOUD_ONEDRIVE_ONEDRIVESTORAGE_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/curljsonrequest.h" + +namespace Cloud { +namespace OneDrive { + +class OneDriveStorage: public Cloud::Storage { + static char *KEY, *SECRET; + + static void loadKeyAndSecret(); + + Common::String _token, _uid, _refreshToken; + + /** This private constructor is called from loadFromConfig(). */ + OneDriveStorage(Common::String token, Common::String uid, Common::String refreshToken); + + void tokenRefreshed(BoolCallback callback, Networking::JsonResponse response); + void codeFlowComplete(BoolResponse response); + void codeFlowFailed(Networking::ErrorResponse error); + + /** Constructs StorageInfo based on JSON response from cloud. */ + void infoInnerCallback(StorageInfoCallback outerCallback, Networking::JsonResponse json); + + void fileInfoCallback(Networking::NetworkReadStreamCallback outerCallback, Networking::JsonResponse response); +public: + /** This constructor uses OAuth code flow to get tokens. */ + OneDriveStorage(Common::String code); + virtual ~OneDriveStorage(); + + /** + * Storage methods, which are used by CloudManager to save + * storage in configuration file. + */ + + /** + * Save storage data using ConfMan. + * @param keyPrefix all saved keys must start with this prefix. + * @note every Storage must write keyPrefix + "type" key + * with common value (e.g. "Dropbox"). + */ + virtual void saveConfig(Common::String keyPrefix); + + /** + * Return unique storage name. + * @returns some unique storage name (for example, "Dropbox (user@example.com)") + */ + virtual Common::String name() const; + + /** Public Cloud API comes down there. */ + + /** Returns ListDirectoryStatus struct with list of files. */ + virtual Networking::Request *listDirectory(Common::String path, ListDirectoryCallback callback, Networking::ErrorCallback errorCallback, bool recursive = false); + + /** Returns UploadStatus struct with info about uploaded file. */ + virtual Networking::Request *upload(Common::String path, Common::SeekableReadStream *contents, UploadCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns pointer to Networking::NetworkReadStream. */ + virtual Networking::Request *streamFileById(Common::String path, Networking::NetworkReadStreamCallback callback, Networking::ErrorCallback errorCallback); + + /** Calls the callback when finished. */ + virtual Networking::Request *createDirectory(Common::String path, BoolCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns the StorageInfo struct. */ + virtual Networking::Request *info(StorageInfoCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns storage's saves directory path with the trailing slash. */ + virtual Common::String savesDirectoryPath(); + + /** + * Load token and user id from configs and return OneDriveStorage for those. + * @return pointer to the newly created OneDriveStorage or 0 if some problem occured. + */ + static OneDriveStorage *loadFromConfig(Common::String keyPrefix); + + /** + * Gets new access_token. If <code> passed is "", refresh_token is used. + * Use "" in order to refresh token and pass a callback, so you could + * continue your work when new token is available. + */ + void getAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback = nullptr, Common::String code = ""); + + Common::String accessToken() const { return _token; } +}; + +} // End of namespace OneDrive +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/onedrive/onedrivetokenrefresher.cpp b/backends/cloud/onedrive/onedrivetokenrefresher.cpp new file mode 100644 index 0000000000..ce7895f41c --- /dev/null +++ b/backends/cloud/onedrive/onedrivetokenrefresher.cpp @@ -0,0 +1,130 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ +#define FORBIDDEN_SYMBOL_ALLOW_ALL + +#include "backends/cloud/onedrive/onedrivetokenrefresher.h" +#include "backends/cloud/onedrive/onedrivestorage.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/debug.h" +#include "common/json.h" +#include <curl/curl.h> + +namespace Cloud { +namespace OneDrive { + +OneDriveTokenRefresher::OneDriveTokenRefresher(OneDriveStorage *parent, Networking::JsonCallback callback, Networking::ErrorCallback ecb, const char *url): + CurlJsonRequest(callback, ecb, url), _parentStorage(parent) {} + +OneDriveTokenRefresher::~OneDriveTokenRefresher() {} + +void OneDriveTokenRefresher::tokenRefreshed(Storage::BoolResponse response) { + if (!response.value) { + //failed to refresh token, notify user with NULL in original callback + warning("OneDriveTokenRefresher: failed to refresh token"); + finishError(Networking::ErrorResponse(this, false, true, "", -1)); + return; + } + + //update headers: first change header with token, then pass those to request + for (uint32 i = 0; i < _headers.size(); ++i) { + if (_headers[i].contains("Authorization")) { + _headers[i] = "Authorization: bearer " + _parentStorage->accessToken(); + } + } + setHeaders(_headers); + + //successfully received refreshed token, can restart the original request now + retry(0); +} + +void OneDriveTokenRefresher::finishJson(Common::JSONValue *json) { + if (!json) { + //that's probably not an error (200 OK) + CurlJsonRequest::finishJson(nullptr); + return; + } + + if (jsonIsObject(json, "OneDriveTokenRefresher")) { + Common::JSONObject result = json->asObject(); + long httpResponseCode = -1; + if (result.contains("error") && jsonIsObject(result.getVal("error"), "OneDriveTokenRefresher")) { + //new token needed => request token & then retry original request + if (_stream) { + httpResponseCode = _stream->httpResponseCode(); + debug(9, "OneDriveTokenRefresher: code = %ld", httpResponseCode); + } + + Common::JSONObject error = result.getVal("error")->asObject(); + bool irrecoverable = true; + + Common::String code, message; + if (jsonContainsString(error, "code", "OneDriveTokenRefresher")) { + code = error.getVal("code")->asString(); + debug(9, "OneDriveTokenRefresher: code = %s", code.c_str()); + } + + if (jsonContainsString(error, "message", "OneDriveTokenRefresher")) { + message = error.getVal("message")->asString(); + debug(9, "OneDriveTokenRefresher: message = %s", message.c_str()); + } + + //determine whether token refreshing would help in this situation + if (code == "itemNotFound") { + if (message.contains("application ID")) + irrecoverable = false; + } + + if (code == "unauthenticated") + irrecoverable = false; + + if (irrecoverable) { + finishError(Networking::ErrorResponse(this, false, true, json->stringify(true), httpResponseCode)); + delete json; + return; + } + + pause(); + delete json; + _parentStorage->getAccessToken(new Common::Callback<OneDriveTokenRefresher, Storage::BoolResponse>(this, &OneDriveTokenRefresher::tokenRefreshed)); + return; + } + } + + //notify user of success + CurlJsonRequest::finishJson(json); +} + +void OneDriveTokenRefresher::setHeaders(Common::Array<Common::String> &headers) { + _headers = headers; + curl_slist_free_all(_headersList); + _headersList = 0; + for (uint32 i = 0; i < headers.size(); ++i) + CurlJsonRequest::addHeader(headers[i]); +} + +void OneDriveTokenRefresher::addHeader(Common::String header) { + _headers.push_back(header); + CurlJsonRequest::addHeader(header); +} + +} // End of namespace OneDrive +} // End of namespace Cloud diff --git a/backends/cloud/onedrive/onedrivetokenrefresher.h b/backends/cloud/onedrive/onedrivetokenrefresher.h new file mode 100644 index 0000000000..77e34d4e03 --- /dev/null +++ b/backends/cloud/onedrive/onedrivetokenrefresher.h @@ -0,0 +1,52 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_ONEDRIVE_ONEDRIVETOKENREFRESHER_H +#define BACKENDS_CLOUD_ONEDRIVE_ONEDRIVETOKENREFRESHER_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/curljsonrequest.h" + +namespace Cloud { +namespace OneDrive { + +class OneDriveStorage; + +class OneDriveTokenRefresher: public Networking::CurlJsonRequest { + OneDriveStorage *_parentStorage; + Common::Array<Common::String> _headers; + + void tokenRefreshed(Storage::BoolResponse response); + + virtual void finishJson(Common::JSONValue *json); +public: + OneDriveTokenRefresher(OneDriveStorage *parent, Networking::JsonCallback callback, Networking::ErrorCallback ecb, const char *url); + virtual ~OneDriveTokenRefresher(); + + virtual void setHeaders(Common::Array<Common::String> &headers); + virtual void addHeader(Common::String header); +}; + +} // End of namespace OneDrive +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/onedrive/onedriveuploadrequest.cpp b/backends/cloud/onedrive/onedriveuploadrequest.cpp new file mode 100644 index 0000000000..41e6e2a37b --- /dev/null +++ b/backends/cloud/onedrive/onedriveuploadrequest.cpp @@ -0,0 +1,191 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/cloud/onedrive/onedriveuploadrequest.h" +#include "backends/cloud/onedrive/onedrivestorage.h" +#include "backends/cloud/iso8601.h" +#include "backends/cloud/storage.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/json.h" +#include "onedrivetokenrefresher.h" + +namespace Cloud { +namespace OneDrive { + +#define ONEDRIVE_API_SPECIAL_APPROOT_UPLOAD "https://api.onedrive.com/v1.0/drive/special/approot:/%s:/upload.createSession" +#define ONEDRIVE_API_SPECIAL_APPROOT_CONTENT "https://api.onedrive.com/v1.0/drive/special/approot:/%s:/content" + +OneDriveUploadRequest::OneDriveUploadRequest(OneDriveStorage *storage, Common::String path, Common::SeekableReadStream *contents, Storage::UploadCallback callback, Networking::ErrorCallback ecb): + Networking::Request(nullptr, ecb), _storage(storage), _savePath(path), _contentsStream(contents), _uploadCallback(callback), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +OneDriveUploadRequest::~OneDriveUploadRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _contentsStream; + delete _uploadCallback; +} + +void OneDriveUploadRequest::start() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + if (_contentsStream == nullptr) { + warning("OneDriveUploadRequest: cannot restart because no stream given"); + finishError(Networking::ErrorResponse(this, false, true, "No stream given", -1)); + return; + } + if (!_contentsStream->seek(0)) { + warning("OneDriveUploadRequest: cannot restart because stream couldn't seek(0)"); + finishError(Networking::ErrorResponse(this, false, true, "", -1)); + return; + } + _ignoreCallback = false; + + uploadNextPart(); +} + +void OneDriveUploadRequest::uploadNextPart() { + const uint32 UPLOAD_PER_ONE_REQUEST = 10 * 1024 * 1024; + + if (_uploadUrl == "" && (uint32)_contentsStream->size() > UPLOAD_PER_ONE_REQUEST) { + Common::String url = Common::String::format(ONEDRIVE_API_SPECIAL_APPROOT_UPLOAD, ConnMan.urlEncode(_savePath).c_str()); //folder must exist + Networking::JsonCallback callback = new Common::Callback<OneDriveUploadRequest, Networking::JsonResponse>(this, &OneDriveUploadRequest::partUploadedCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<OneDriveUploadRequest, Networking::ErrorResponse>(this, &OneDriveUploadRequest::partUploadedErrorCallback); + Networking::CurlJsonRequest *request = new OneDriveTokenRefresher(_storage, callback, failureCallback, url.c_str()); + request->addHeader("Authorization: Bearer " + _storage->accessToken()); + request->setBuffer(new byte[1], 0); //use POST + _workingRequest = ConnMan.addRequest(request); + return; + } + + Common::String url; + if (_uploadUrl == "") { + url = Common::String::format(ONEDRIVE_API_SPECIAL_APPROOT_CONTENT, ConnMan.urlEncode(_savePath).c_str()); + } else { + url = _uploadUrl; + } + + Networking::JsonCallback callback = new Common::Callback<OneDriveUploadRequest, Networking::JsonResponse>(this, &OneDriveUploadRequest::partUploadedCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<OneDriveUploadRequest, Networking::ErrorResponse>(this, &OneDriveUploadRequest::partUploadedErrorCallback); + Networking::CurlJsonRequest *request = new OneDriveTokenRefresher(_storage, callback, failureCallback, url.c_str()); + request->addHeader("Authorization: Bearer " + _storage->accessToken()); + request->usePut(); + + uint32 oldPos = _contentsStream->pos(); + + byte *buffer = new byte[UPLOAD_PER_ONE_REQUEST]; + uint32 size = _contentsStream->read(buffer, UPLOAD_PER_ONE_REQUEST); + request->setBuffer(buffer, size); + + if (_uploadUrl != "") { + request->addHeader(Common::String::format("Content-Range: bytes %u-%u/%u", oldPos, _contentsStream->pos() - 1, _contentsStream->size())); + } else if (_contentsStream->size() == 0) { + warning("\"Sorry, OneDrive can't upload empty files\""); + finishUpload(StorageFile(_savePath, 0, 0, false)); + delete request; + return; + } + + _workingRequest = ConnMan.addRequest(request); +} + +void OneDriveUploadRequest::partUploadedCallback(Networking::JsonResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + Networking::ErrorResponse error(this, false, true, "", -1); + Networking::CurlJsonRequest *rq = (Networking::CurlJsonRequest *)response.request; + if (rq && rq->getNetworkReadStream()) + error.httpResponseCode = rq->getNetworkReadStream()->httpResponseCode(); + + Common::JSONValue *json = response.value; + if (json == nullptr) { + error.response = "Failed to parse JSON, null passed!"; + finishError(error); + return; + } + + if (json->isObject()) { + Common::JSONObject object = json->asObject(); + + if (object.contains("error")) { + warning("OneDriveUploadRequest: error: %s", json->stringify(true).c_str()); + error.response = json->stringify(true); + finishError(error); + delete json; + return; + } + + if (Networking::CurlJsonRequest::jsonContainsString(object, "id", "OneDriveUploadRequest") && + Networking::CurlJsonRequest::jsonContainsString(object, "name", "OneDriveUploadRequest") && + Networking::CurlJsonRequest::jsonContainsIntegerNumber(object, "size", "OneDriveUploadRequest") && + Networking::CurlJsonRequest::jsonContainsString(object, "lastModifiedDateTime", "OneDriveUploadRequest")) { + //finished + Common::String path = _savePath; + uint32 size = object.getVal("size")->asIntegerNumber(); + uint32 timestamp = ISO8601::convertToTimestamp(object.getVal("lastModifiedDateTime")->asString()); + finishUpload(StorageFile(path, size, timestamp, false)); + return; + } + + if (_uploadUrl == "") { + if (Networking::CurlJsonRequest::jsonContainsString(object, "uploadUrl", "OneDriveUploadRequest")) + _uploadUrl = object.getVal("uploadUrl")->asString(); + } + } + + if (_contentsStream->eos() || _contentsStream->pos() >= _contentsStream->size() - 1) { + warning("OneDriveUploadRequest: no file info to return"); + finishUpload(StorageFile(_savePath, 0, 0, false)); + } else { + uploadNextPart(); + } + + delete json; +} + +void OneDriveUploadRequest::partUploadedErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + finishError(error); +} + +void OneDriveUploadRequest::handle() {} + +void OneDriveUploadRequest::restart() { start(); } + +void OneDriveUploadRequest::finishUpload(StorageFile file) { + Request::finishSuccess(); + if (_uploadCallback) + (*_uploadCallback)(Storage::UploadResponse(this, file)); +} + +} // End of namespace OneDrive +} // End of namespace Cloud diff --git a/backends/cloud/onedrive/onedriveuploadrequest.h b/backends/cloud/onedrive/onedriveuploadrequest.h new file mode 100644 index 0000000000..4e2cb24a7f --- /dev/null +++ b/backends/cloud/onedrive/onedriveuploadrequest.h @@ -0,0 +1,61 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_ONEDRIVE_ONEDRIVEUPLOADREQUEST_H +#define BACKENDS_CLOUD_ONEDRIVE_ONEDRIVEUPLOADREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/request.h" +#include "common/callback.h" + +namespace Cloud { +namespace OneDrive { +class OneDriveStorage; + +class OneDriveUploadRequest: public Networking::Request { + OneDriveStorage *_storage; + Common::String _savePath; + Common::SeekableReadStream *_contentsStream; + Storage::UploadCallback _uploadCallback; + Request *_workingRequest; + bool _ignoreCallback; + Common::String _uploadUrl; + + void start(); + void uploadNextPart(); + void partUploadedCallback(Networking::JsonResponse response); + void partUploadedErrorCallback(Networking::ErrorResponse error); + void finishUpload(StorageFile status); + +public: + OneDriveUploadRequest(OneDriveStorage *storage, Common::String path, Common::SeekableReadStream *contents, Storage::UploadCallback callback, Networking::ErrorCallback ecb); + virtual ~OneDriveUploadRequest(); + + virtual void handle(); + virtual void restart(); +}; + +} // End of namespace OneDrive +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/savessyncrequest.cpp b/backends/cloud/savessyncrequest.cpp new file mode 100644 index 0000000000..fff46c3a83 --- /dev/null +++ b/backends/cloud/savessyncrequest.cpp @@ -0,0 +1,403 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/cloud/savessyncrequest.h" +#include "backends/cloud/cloudmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/saves/default/default-saves.h" +#include "common/config-manager.h" +#include "common/debug.h" +#include "common/file.h" +#include "common/json.h" +#include "common/savefile.h" +#include "common/system.h" +#include "gui/saveload-dialog.h" + +namespace Cloud { + +SavesSyncRequest::SavesSyncRequest(Storage *storage, Storage::BoolCallback callback, Networking::ErrorCallback ecb): + Request(nullptr, ecb), CommandSender(nullptr), _storage(storage), _boolCallback(callback), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +SavesSyncRequest::~SavesSyncRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _boolCallback; +} + +void SavesSyncRequest::start() { + //cleanup + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _currentDownloadingFile = StorageFile(); + _currentUploadingFile = ""; + _filesToDownload.clear(); + _filesToUpload.clear(); + _localFilesTimestamps.clear(); + _totalFilesToHandle = 0; + _ignoreCallback = false; + + //load timestamps + _localFilesTimestamps = DefaultSaveFileManager::loadTimestamps(); + + //list saves directory + Common::String dir = _storage->savesDirectoryPath(); + if (dir.lastChar() == '/') + dir.deleteLastChar(); + _workingRequest = _storage->listDirectory( + dir, + new Common::Callback<SavesSyncRequest, Storage::ListDirectoryResponse>(this, &SavesSyncRequest::directoryListedCallback), + new Common::Callback<SavesSyncRequest, Networking::ErrorResponse>(this, &SavesSyncRequest::directoryListedErrorCallback) + ); + if (!_workingRequest) finishError(Networking::ErrorResponse(this)); +} + +void SavesSyncRequest::directoryListedCallback(Storage::ListDirectoryResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + if (response.request) _date = response.request->date(); + + Common::HashMap<Common::String, bool> localFileNotAvailableInCloud; + for (Common::HashMap<Common::String, uint32>::iterator i = _localFilesTimestamps.begin(); i != _localFilesTimestamps.end(); ++i) { + localFileNotAvailableInCloud[i->_key] = true; + } + + //determine which files to download and which files to upload + Common::Array<StorageFile> &remoteFiles = response.value; + uint64 totalSize = 0; + for (uint32 i = 0; i < remoteFiles.size(); ++i) { + StorageFile &file = remoteFiles[i]; + if (file.isDirectory()) + continue; + totalSize += file.size(); + if (file.name() == DefaultSaveFileManager::TIMESTAMPS_FILENAME) + continue; + + Common::String name = file.name(); + if (!_localFilesTimestamps.contains(name)) { + _filesToDownload.push_back(file); + } else { + localFileNotAvailableInCloud[name] = false; + + if (_localFilesTimestamps[name] == file.timestamp()) + continue; + + //we actually can have some files not only with timestamp < remote + //but also with timestamp > remote (when we have been using ANOTHER CLOUD and then switched back) + if (_localFilesTimestamps[name] > file.timestamp() || _localFilesTimestamps[name] == DefaultSaveFileManager::INVALID_TIMESTAMP) + _filesToUpload.push_back(file.name()); + else + _filesToDownload.push_back(file); + } + } + + CloudMan.setStorageUsedSpace(CloudMan.getStorageIndex(), totalSize); + + //upload files which are unavailable in cloud + for (Common::HashMap<Common::String, bool>::iterator i = localFileNotAvailableInCloud.begin(); i != localFileNotAvailableInCloud.end(); ++i) { + if (i->_key == DefaultSaveFileManager::TIMESTAMPS_FILENAME) + continue; + if (i->_value) + _filesToUpload.push_back(i->_key); + } + + debug(9, "\nSavesSyncRequest: download files:"); + for (uint32 i = 0; i < _filesToDownload.size(); ++i) { + debug(9, "%s", _filesToDownload[i].name().c_str()); + } + debug(9, "\nSavesSyncRequest: upload files:"); + for (uint32 i = 0; i < _filesToUpload.size(); ++i) { + debug(9, "%s", _filesToUpload[i].c_str()); + } + _totalFilesToHandle = _filesToDownload.size() + _filesToUpload.size(); + + //start downloading files + downloadNextFile(); +} + +void SavesSyncRequest::directoryListedErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + bool irrecoverable = error.interrupted || error.failed; + if (error.failed) { + Common::JSONValue *value = Common::JSON::parse(error.response.c_str()); + if (value) { + if (value->isObject()) { + Common::JSONObject object = value->asObject(); + + //Dropbox-related error: + if (object.contains("error_summary") && object.getVal("error_summary")->isString()) { + Common::String summary = object.getVal("error_summary")->asString(); + if (summary.contains("not_found")) { + irrecoverable = false; + } + } + + //OneDrive-related error: + if (object.contains("error") && object.getVal("error")->isObject()) { + Common::JSONObject errorNode = object.getVal("error")->asObject(); + if (Networking::CurlJsonRequest::jsonContainsString(errorNode, "code", "SavesSyncRequest")) { + Common::String code = errorNode.getVal("code")->asString(); + if (code == "itemNotFound") { + irrecoverable = false; + } + } + } + } + delete value; + } + + //Google Drive and Box-related ScummVM-based error + if (error.response.contains("subdirectory not found")) { + irrecoverable = false; //base "/ScummVM/" folder not found + } else if (error.response.contains("no such file found in its parent directory")) { + irrecoverable = false; //"Saves" folder within "/ScummVM/" not found + } + } + + if (irrecoverable) { + finishError(error); + return; + } + + //we're lucky - user just lacks his "/cloud/" folder - let's create one + Common::String dir = _storage->savesDirectoryPath(); + if (dir.lastChar() == '/') + dir.deleteLastChar(); + debug(9, "SavesSyncRequest: creating %s", dir.c_str()); + _workingRequest = _storage->createDirectory( + dir, + new Common::Callback<SavesSyncRequest, Storage::BoolResponse>(this, &SavesSyncRequest::directoryCreatedCallback), + new Common::Callback<SavesSyncRequest, Networking::ErrorResponse>(this, &SavesSyncRequest::directoryCreatedErrorCallback) + ); + if (!_workingRequest) + finishError(Networking::ErrorResponse(this)); +} + +void SavesSyncRequest::directoryCreatedCallback(Storage::BoolResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + //stop syncing if failed to create saves directory + if (!response.value) { + finishError(Networking::ErrorResponse(this, false, true, "", -1)); + return; + } + + //continue with empty files list + Common::Array<StorageFile> files; + directoryListedCallback(Storage::ListDirectoryResponse(response.request, files)); +} + +void SavesSyncRequest::directoryCreatedErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + //stop syncing if failed to create saves directory + finishError(error); +} + +void SavesSyncRequest::downloadNextFile() { + if (_filesToDownload.empty()) { + _currentDownloadingFile = StorageFile("", 0, 0, false); //so getFilesToDownload() would return an empty array + sendCommand(GUI::kSavesSyncEndedCmd, 0); + uploadNextFile(); + return; + } + + _currentDownloadingFile = _filesToDownload.back(); + _filesToDownload.pop_back(); + + sendCommand(GUI::kSavesSyncProgressCmd, (int)(getDownloadingProgress() * 100)); + + debug(9, "SavesSyncRequest: downloading %s (%d %%)", _currentDownloadingFile.name().c_str(), (int)(getProgress() * 100)); + _workingRequest = _storage->downloadById( + _currentDownloadingFile.id(), + DefaultSaveFileManager::concatWithSavesPath(_currentDownloadingFile.name()), + new Common::Callback<SavesSyncRequest, Storage::BoolResponse>(this, &SavesSyncRequest::fileDownloadedCallback), + new Common::Callback<SavesSyncRequest, Networking::ErrorResponse>(this, &SavesSyncRequest::fileDownloadedErrorCallback) + ); + if (!_workingRequest) + finishError(Networking::ErrorResponse(this)); +} + +void SavesSyncRequest::fileDownloadedCallback(Storage::BoolResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + //stop syncing if download failed + if (!response.value) { + //delete the incomplete file + g_system->getSavefileManager()->removeSavefile(_currentDownloadingFile.name()); + finishError(Networking::ErrorResponse(this, false, true, "", -1)); + return; + } + + //update local timestamp for downloaded file + _localFilesTimestamps = DefaultSaveFileManager::loadTimestamps(); + _localFilesTimestamps[_currentDownloadingFile.name()] = _currentDownloadingFile.timestamp(); + DefaultSaveFileManager::saveTimestamps(_localFilesTimestamps); + + //continue downloading files + downloadNextFile(); +} + +void SavesSyncRequest::fileDownloadedErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + //stop syncing if download failed + finishError(error); +} + +void SavesSyncRequest::uploadNextFile() { + if (_filesToUpload.empty()) { + finishSync(true); + return; + } + + _currentUploadingFile = _filesToUpload.back(); + _filesToUpload.pop_back(); + + debug(9, "SavesSyncRequest: uploading %s (%d %%)", _currentUploadingFile.c_str(), (int)(getProgress() * 100)); + if (_storage->uploadStreamSupported()) { + _workingRequest = _storage->upload( + _storage->savesDirectoryPath() + _currentUploadingFile, + g_system->getSavefileManager()->openRawFile(_currentUploadingFile), + new Common::Callback<SavesSyncRequest, Storage::UploadResponse>(this, &SavesSyncRequest::fileUploadedCallback), + new Common::Callback<SavesSyncRequest, Networking::ErrorResponse>(this, &SavesSyncRequest::fileUploadedErrorCallback) + ); + } else { + _workingRequest = _storage->upload( + _storage->savesDirectoryPath() + _currentUploadingFile, + DefaultSaveFileManager::concatWithSavesPath(_currentUploadingFile), + new Common::Callback<SavesSyncRequest, Storage::UploadResponse>(this, &SavesSyncRequest::fileUploadedCallback), + new Common::Callback<SavesSyncRequest, Networking::ErrorResponse>(this, &SavesSyncRequest::fileUploadedErrorCallback) + ); + } + if (!_workingRequest) finishError(Networking::ErrorResponse(this)); +} + +void SavesSyncRequest::fileUploadedCallback(Storage::UploadResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + //update local timestamp for the uploaded file + _localFilesTimestamps = DefaultSaveFileManager::loadTimestamps(); + _localFilesTimestamps[_currentUploadingFile] = response.value.timestamp(); + DefaultSaveFileManager::saveTimestamps(_localFilesTimestamps); + + //continue uploading files + uploadNextFile(); +} + +void SavesSyncRequest::fileUploadedErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + //stop syncing if upload failed + finishError(error); +} + +void SavesSyncRequest::handle() {} + +void SavesSyncRequest::restart() { start(); } + +double SavesSyncRequest::getDownloadingProgress() const { + if (_totalFilesToHandle == 0) { + if (_state == Networking::FINISHED) + return 1; //nothing to upload and download => Request ends soon + return 0; //directory not listed yet + } + + if (_totalFilesToHandle == _filesToUpload.size()) + return 1; //nothing to download => download complete + + uint32 totalFilesToDownload = _totalFilesToHandle - _filesToUpload.size(); + uint32 filesLeftToDownload = _filesToDownload.size() + (_currentDownloadingFile.name() != "" ? 1 : 0); + return (double)(totalFilesToDownload - filesLeftToDownload) / (double)(totalFilesToDownload); +} + +double SavesSyncRequest::getProgress() const { + if (_totalFilesToHandle == 0) { + if (_state == Networking::FINISHED) + return 1; //nothing to upload and download => Request ends soon + return 0; //directory not listed yet + } + + return (double)(_totalFilesToHandle - _filesToDownload.size() - _filesToUpload.size()) / (double)(_totalFilesToHandle); +} + +Common::Array<Common::String> SavesSyncRequest::getFilesToDownload() { + Common::Array<Common::String> result; + for (uint32 i = 0; i < _filesToDownload.size(); ++i) + result.push_back(_filesToDownload[i].name()); + if (_currentDownloadingFile.name() != "") + result.push_back(_currentDownloadingFile.name()); + return result; +} + +void SavesSyncRequest::finishError(Networking::ErrorResponse error) { + debug(9, "SavesSync::finishError"); + //if we were downloading a file - remember the name + //and make the Request close() it, so we can delete it + Common::String name = _currentDownloadingFile.name(); + if (_workingRequest) { + _ignoreCallback = true; + _workingRequest->finish(); + _workingRequest = nullptr; + _ignoreCallback = false; + } + //unlock all the files by making getFilesToDownload() return empty array + _currentDownloadingFile = StorageFile(); + _filesToDownload.clear(); + //delete the incomplete file + if (name != "") + g_system->getSavefileManager()->removeSavefile(name); + Request::finishError(error); +} + +void SavesSyncRequest::finishSync(bool success) { + Request::finishSuccess(); + + //update last successful sync date + CloudMan.setStorageLastSync(CloudMan.getStorageIndex(), _date); + + if (_boolCallback) + (*_boolCallback)(Storage::BoolResponse(this, success)); +} + +} // End of namespace Cloud diff --git a/backends/cloud/savessyncrequest.h b/backends/cloud/savessyncrequest.h new file mode 100644 index 0000000000..e891e93969 --- /dev/null +++ b/backends/cloud/savessyncrequest.h @@ -0,0 +1,80 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_SAVESSYNCREQUEST_H +#define BACKENDS_CLOUD_SAVESSYNCREQUEST_H + +#include "backends/networking/curl/request.h" +#include "backends/cloud/storage.h" +#include "common/hashmap.h" +#include "common/hash-str.h" +#include "gui/object.h" + +namespace Cloud { + +class SavesSyncRequest: public Networking::Request, public GUI::CommandSender { + Storage *_storage; + Storage::BoolCallback _boolCallback; + Common::HashMap<Common::String, uint32> _localFilesTimestamps; + Common::Array<StorageFile> _filesToDownload; + Common::Array<Common::String> _filesToUpload; + StorageFile _currentDownloadingFile; + Common::String _currentUploadingFile; + Request *_workingRequest; + bool _ignoreCallback; + uint32 _totalFilesToHandle; + Common::String _date; + + void start(); + void directoryListedCallback(Storage::ListDirectoryResponse response); + void directoryListedErrorCallback(Networking::ErrorResponse error); + void directoryCreatedCallback(Storage::BoolResponse response); + void directoryCreatedErrorCallback(Networking::ErrorResponse error); + void fileDownloadedCallback(Storage::BoolResponse response); + void fileDownloadedErrorCallback(Networking::ErrorResponse error); + void fileUploadedCallback(Storage::UploadResponse response); + void fileUploadedErrorCallback(Networking::ErrorResponse error); + void downloadNextFile(); + void uploadNextFile(); + virtual void finishError(Networking::ErrorResponse error); + void finishSync(bool success); + +public: + SavesSyncRequest(Storage *storage, Storage::BoolCallback callback, Networking::ErrorCallback ecb); + virtual ~SavesSyncRequest(); + + virtual void handle(); + virtual void restart(); + + /** Returns a number in range [0, 1], where 1 is "complete". */ + double getDownloadingProgress() const; + + /** Returns a number in range [0, 1], where 1 is "complete". */ + double getProgress() const; + + /** Returns an array of saves names which are not downloaded yet. */ + Common::Array<Common::String> getFilesToDownload(); +}; + +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/storage.cpp b/backends/cloud/storage.cpp new file mode 100644 index 0000000000..910d80d153 --- /dev/null +++ b/backends/cloud/storage.cpp @@ -0,0 +1,342 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/cloud/storage.h" +#include "backends/cloud/downloadrequest.h" +#include "backends/cloud/folderdownloadrequest.h" +#include "backends/cloud/savessyncrequest.h" +#include "backends/networking/curl/connectionmanager.h" +#include "common/debug.h" +#include "common/file.h" +#include <common/translation.h> + +namespace Cloud { + +Storage::Storage(): + _runningRequestsCount(0), _savesSyncRequest(nullptr), _syncRestartRequestsed(false), + _downloadFolderRequest(nullptr) {} + +Storage::~Storage() {} + +Networking::ErrorCallback Storage::getErrorPrintingCallback() { + return new Common::Callback<Storage, Networking::ErrorResponse>(this, &Storage::printErrorResponse); +} + +void Storage::printErrorResponse(Networking::ErrorResponse error) { + debug(9, "Storage: error response (%s, %ld):", (error.failed ? "failed" : "interrupted"), error.httpResponseCode); + debug(9, "%s", error.response.c_str()); +} + +Networking::Request *Storage::addRequest(Networking::Request *request) { + _runningRequestsMutex.lock(); + ++_runningRequestsCount; + if (_runningRequestsCount == 1) + debug(9, "Storage is working now"); + _runningRequestsMutex.unlock(); + return ConnMan.addRequest(request, new Common::Callback<Storage, Networking::Request *>(this, &Storage::requestFinishedCallback)); +} + +void Storage::requestFinishedCallback(Networking::Request *invalidRequestPointer) { + bool restartSync = false; + + _runningRequestsMutex.lock(); + if (invalidRequestPointer == _savesSyncRequest) + _savesSyncRequest = nullptr; + --_runningRequestsCount; + if (_syncRestartRequestsed) + restartSync = true; + if (_runningRequestsCount == 0 && !restartSync) + debug(9, "Storage is not working now"); + _runningRequestsMutex.unlock(); + + if (restartSync) + syncSaves(nullptr, nullptr); +} + +Networking::Request *Storage::upload(Common::String remotePath, Common::String localPath, UploadCallback callback, Networking::ErrorCallback errorCallback) { + if (!errorCallback) errorCallback = getErrorPrintingCallback(); + + Common::File *f = new Common::File(); + if (!f->open(localPath)) { + warning("Storage: unable to open file to upload from"); + if (errorCallback) + (*errorCallback)(Networking::ErrorResponse(nullptr, false, true, "", -1)); + delete errorCallback; + delete callback; + delete f; + return nullptr; + } + + return upload(remotePath, f, callback, errorCallback); +} + +bool Storage::uploadStreamSupported() { + return true; +} + +Networking::Request *Storage::streamFile(Common::String path, Networking::NetworkReadStreamCallback callback, Networking::ErrorCallback errorCallback) { + //most Storages use paths instead of ids, so this should work + return streamFileById(path, callback, errorCallback); +} + +Networking::Request *Storage::download(Common::String remotePath, Common::String localPath, BoolCallback callback, Networking::ErrorCallback errorCallback) { + //most Storages use paths instead of ids, so this should work + return downloadById(remotePath, localPath, callback, errorCallback); +} + +Networking::Request *Storage::downloadById(Common::String remoteId, Common::String localPath, BoolCallback callback, Networking::ErrorCallback errorCallback) { + if (!errorCallback) errorCallback = getErrorPrintingCallback(); + + Common::DumpFile *f = new Common::DumpFile(); + if (!f->open(localPath, true)) { + warning("Storage: unable to open file to download into"); + if (errorCallback) (*errorCallback)(Networking::ErrorResponse(nullptr, false, true, "", -1)); + delete errorCallback; + delete callback; + delete f; + return nullptr; + } + + return addRequest(new DownloadRequest(this, callback, errorCallback, remoteId, f)); +} + +Networking::Request *Storage::downloadFolder(Common::String remotePath, Common::String localPath, FileArrayCallback callback, Networking::ErrorCallback errorCallback, bool recursive) { + if (!errorCallback) + errorCallback = getErrorPrintingCallback(); + return addRequest(new FolderDownloadRequest(this, callback, errorCallback, remotePath, localPath, recursive)); +} + +SavesSyncRequest *Storage::syncSaves(BoolCallback callback, Networking::ErrorCallback errorCallback) { + _runningRequestsMutex.lock(); + if (_savesSyncRequest) { + warning("Storage::syncSaves: there is a sync in progress already"); + _syncRestartRequestsed = true; + _runningRequestsMutex.unlock(); + return _savesSyncRequest; + } + if (!callback) + callback = new Common::Callback<Storage, BoolResponse>(this, &Storage::savesSyncDefaultCallback); + if (!errorCallback) + errorCallback = new Common::Callback<Storage, Networking::ErrorResponse>(this, &Storage::savesSyncDefaultErrorCallback); + _savesSyncRequest = new SavesSyncRequest(this, callback, errorCallback); + _syncRestartRequestsed = false; + _runningRequestsMutex.unlock(); + return (SavesSyncRequest *)addRequest(_savesSyncRequest); //who knows what that ConnMan could return in the future +} + +bool Storage::isWorking() { + _runningRequestsMutex.lock(); + bool working = _runningRequestsCount > 0; + _runningRequestsMutex.unlock(); + return working; +} + +///// SavesSyncRequest-related ///// + +bool Storage::isSyncing() { + _runningRequestsMutex.lock(); + bool syncing = _savesSyncRequest != nullptr; + _runningRequestsMutex.unlock(); + return syncing; +} + +double Storage::getSyncDownloadingProgress() { + double result = 1; + _runningRequestsMutex.lock(); + if (_savesSyncRequest) + result = _savesSyncRequest->getDownloadingProgress(); + _runningRequestsMutex.unlock(); + return result; +} + +double Storage::getSyncProgress() { + double result = 1; + _runningRequestsMutex.lock(); + if (_savesSyncRequest) + result = _savesSyncRequest->getProgress(); + _runningRequestsMutex.unlock(); + return result; +} + +Common::Array<Common::String> Storage::getSyncingFiles() { + Common::Array<Common::String> result; + _runningRequestsMutex.lock(); + if (_savesSyncRequest) + result = _savesSyncRequest->getFilesToDownload(); + _runningRequestsMutex.unlock(); + return result; +} + +void Storage::cancelSync() { + _runningRequestsMutex.lock(); + if (_savesSyncRequest) + _savesSyncRequest->finish(); + _runningRequestsMutex.unlock(); +} + +void Storage::setSyncTarget(GUI::CommandReceiver *target) { + _runningRequestsMutex.lock(); + if (_savesSyncRequest) + _savesSyncRequest->setTarget(target); + _runningRequestsMutex.unlock(); +} + +void Storage::savesSyncDefaultCallback(BoolResponse response) { + _runningRequestsMutex.lock(); + _savesSyncRequest = nullptr; + _runningRequestsMutex.unlock(); + + if (!response.value) + warning("SavesSyncRequest called success callback with `false` argument"); + g_system->displayMessageOnOSD(_("Saves sync complete.")); +} + +void Storage::savesSyncDefaultErrorCallback(Networking::ErrorResponse error) { + _runningRequestsMutex.lock(); + _savesSyncRequest = nullptr; + _runningRequestsMutex.unlock(); + + printErrorResponse(error); + + if (error.interrupted) + g_system->displayMessageOnOSD(_("Saves sync was cancelled.")); + else + g_system->displayMessageOnOSD(_("Saves sync failed.\nCheck your Internet connection.")); +} + +///// DownloadFolderRequest-related ///// + +bool Storage::startDownload(Common::String remotePath, Common::String localPath) { + _runningRequestsMutex.lock(); + if (_downloadFolderRequest) { + warning("Storage::startDownload: there is a download in progress already"); + _runningRequestsMutex.unlock(); + return false; + } + _downloadFolderRequest = (FolderDownloadRequest *)downloadFolder( + remotePath, localPath, + new Common::Callback<Storage, FileArrayResponse>(this, &Storage::directoryDownloadedCallback), + new Common::Callback<Storage, Networking::ErrorResponse>(this, &Storage::directoryDownloadedErrorCallback), + true + ); + _runningRequestsMutex.unlock(); + return true; +} + +void Storage::cancelDownload() { + _runningRequestsMutex.lock(); + if (_downloadFolderRequest) + _downloadFolderRequest->finish(); + _runningRequestsMutex.unlock(); +} + +void Storage::setDownloadTarget(GUI::CommandReceiver *target) { + _runningRequestsMutex.lock(); + if (_downloadFolderRequest) + _downloadFolderRequest->setTarget(target); + _runningRequestsMutex.unlock(); +} + +bool Storage::isDownloading() { + _runningRequestsMutex.lock(); + bool syncing = _downloadFolderRequest != nullptr; + _runningRequestsMutex.unlock(); + return syncing; +} + +double Storage::getDownloadingProgress() { + double result = 1; + _runningRequestsMutex.lock(); + if (_downloadFolderRequest) + result = _downloadFolderRequest->getProgress(); + _runningRequestsMutex.unlock(); + return result; +} + +uint64 Storage::getDownloadBytesNumber() { + uint64 result = 0; + _runningRequestsMutex.lock(); + if (_downloadFolderRequest) + result = _downloadFolderRequest->getDownloadedBytes(); + _runningRequestsMutex.unlock(); + return result; +} + +uint64 Storage::getDownloadTotalBytesNumber() { + uint64 result = 0; + _runningRequestsMutex.lock(); + if (_downloadFolderRequest) + result = _downloadFolderRequest->getTotalBytesToDownload(); + _runningRequestsMutex.unlock(); + return result; +} + +uint64 Storage::getDownloadSpeed() { + uint64 result = 0; + _runningRequestsMutex.lock(); + if (_downloadFolderRequest) + result = _downloadFolderRequest->getDownloadSpeed(); + _runningRequestsMutex.unlock(); + return result; +} + +Common::String Storage::getDownloadRemoteDirectory() { + Common::String result = ""; + _runningRequestsMutex.lock(); + if (_downloadFolderRequest) + result = _downloadFolderRequest->getRemotePath(); + _runningRequestsMutex.unlock(); + return result; +} + +Common::String Storage::getDownloadLocalDirectory() { + Common::String result = ""; + _runningRequestsMutex.lock(); + if (_downloadFolderRequest) + result = _downloadFolderRequest->getLocalPath(); + _runningRequestsMutex.unlock(); + return result; +} + +void Storage::directoryDownloadedCallback(FileArrayResponse response) { + _runningRequestsMutex.lock(); + _downloadFolderRequest = nullptr; + _runningRequestsMutex.unlock(); + + Common::String message; + if (response.value.size()) { + message = Common::String::format(_("Download complete.\nFailed to download %u files."), response.value.size()); + } else { + message = _("Download complete."); + } + g_system->displayMessageOnOSD(message.c_str()); +} + +void Storage::directoryDownloadedErrorCallback(Networking::ErrorResponse error) { + _runningRequestsMutex.lock(); + _downloadFolderRequest = nullptr; + _runningRequestsMutex.unlock(); + + g_system->displayMessageOnOSD(_("Download failed.")); +} + +} // End of namespace Cloud diff --git a/backends/cloud/storage.h b/backends/cloud/storage.h new file mode 100644 index 0000000000..6a5765f13a --- /dev/null +++ b/backends/cloud/storage.h @@ -0,0 +1,238 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_STORAGE_H +#define BACKENDS_CLOUD_STORAGE_H + +#include "backends/cloud/storagefile.h" +#include "backends/cloud/storageinfo.h" +#include "backends/networking/curl/request.h" +#include "backends/networking/curl/curlrequest.h" +#include "common/array.h" +#include "common/callback.h" +#include "common/mutex.h" +#include "common/stream.h" +#include "common/str.h" + +namespace GUI { + +class CommandReceiver; + +} + +namespace Cloud { + +class SavesSyncRequest; +class FolderDownloadRequest; + +class Storage { +public: + typedef Networking::Response<Common::Array<StorageFile>&> FileArrayResponse; + typedef Networking::Response<StorageInfo> StorageInfoResponse; + typedef Networking::Response<bool> BoolResponse; + typedef Networking::Response<StorageFile> UploadResponse; + typedef Networking::Response<Common::Array<StorageFile> &> ListDirectoryResponse; + + typedef Common::BaseCallback<FileArrayResponse> *FileArrayCallback; + typedef Common::BaseCallback<StorageInfoResponse> *StorageInfoCallback; + typedef Common::BaseCallback<BoolResponse> *BoolCallback; + typedef Common::BaseCallback<UploadResponse> *UploadCallback; + typedef Common::BaseCallback<ListDirectoryResponse> *ListDirectoryCallback; + +protected: + /** Keeps track of running requests. */ + uint32 _runningRequestsCount; + Common::Mutex _runningRequestsMutex; + + /** SavesSyncRequest-related */ + SavesSyncRequest *_savesSyncRequest; + bool _syncRestartRequestsed; + + /** FolderDownloadRequest-related */ + FolderDownloadRequest *_downloadFolderRequest; + + /** Returns default error callback (printErrorResponse). */ + virtual Networking::ErrorCallback getErrorPrintingCallback(); + + /** Prints ErrorResponse contents with debug(). */ + virtual void printErrorResponse(Networking::ErrorResponse error); + + /** + * Adds request to the ConnMan, but also increases _runningRequestsCount. + * This method should be used by Storage implementations instead of + * direct ConnMan.addRequest() call. + * + * @return the same Request pointer, just as a shortcut + */ + virtual Networking::Request *addRequest(Networking::Request *request); + + /** + * Decreases _runningRequestCount. It's called from ConnMan automatically. + * Passed pointer is dangling, but one can use the address to determine + * some special Requests (which addresses were remembered somewhere). + */ + virtual void requestFinishedCallback(Networking::Request *invalidRequestPointer); + +public: + Storage(); + virtual ~Storage(); + + /** + * Storage methods, which are used by CloudManager to save + * storage in configuration file. + */ + + /** + * Save storage data using ConfMan. + * @param keyPrefix all saved keys must start with this prefix. + * @note every Storage must write keyPrefix + "type" key + * with common value (e.g. "Dropbox"). + */ + virtual void saveConfig(Common::String keyPrefix) = 0; + + /** + * Return unique storage name. + * @returns some unique storage name (for example, "Dropbox (user@example.com)") + */ + virtual Common::String name() const = 0; + + /** + * Public Cloud API comes down there. + * + * All Cloud API methods return Networking::Request *, which + * might be used to control request. All methods also accept + * a callback, which is called, when request is complete. + */ + + /** Returns ListDirectoryResponse with list of files. */ + virtual Networking::Request *listDirectory(Common::String path, ListDirectoryCallback callback, Networking::ErrorCallback errorCallback, bool recursive = false) = 0; + + /** Returns StorageFile with info about uploaded file. */ + virtual Networking::Request *upload(Common::String path, Common::SeekableReadStream *contents, UploadCallback callback, Networking::ErrorCallback errorCallback) = 0; + virtual Networking::Request *upload(Common::String remotePath, Common::String localPath, UploadCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns whether Storage supports upload(ReadStream). */ + virtual bool uploadStreamSupported(); + + /** Returns pointer to Networking::NetworkReadStream. */ + virtual Networking::Request *streamFile(Common::String path, Networking::NetworkReadStreamCallback callback, Networking::ErrorCallback errorCallback); + virtual Networking::Request *streamFileById(Common::String id, Networking::NetworkReadStreamCallback callback, Networking::ErrorCallback errorCallback) = 0; + + /** Calls the callback when finished. */ + virtual Networking::Request *download(Common::String remotePath, Common::String localPath, BoolCallback callback, Networking::ErrorCallback errorCallback); + virtual Networking::Request *downloadById(Common::String remoteId, Common::String localPath, BoolCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns Common::Array<StorageFile> with list of files, which were not downloaded. */ + virtual Networking::Request *downloadFolder(Common::String remotePath, Common::String localPath, FileArrayCallback callback, Networking::ErrorCallback errorCallback, bool recursive = false); + + /** Calls the callback when finished. */ + virtual SavesSyncRequest *syncSaves(BoolCallback callback, Networking::ErrorCallback errorCallback); + + /** Calls the callback when finished. */ + virtual Networking::Request *createDirectory(Common::String path, BoolCallback callback, Networking::ErrorCallback errorCallback) = 0; + + /** + * Returns the StorageInfo struct via <callback>. + * Calls the <errorCallback> if failed to get information. + * + * @note on success Storage should also call + * CloudMan.setStorageUsername(). + */ + virtual Networking::Request *info(StorageInfoCallback callback, Networking::ErrorCallback errorCallback) = 0; + + /** Returns storage's saves directory path with the trailing slash. */ + virtual Common::String savesDirectoryPath() = 0; + + /** Returns whether there are any requests running. */ + virtual bool isWorking(); + + ///// SavesSyncRequest-related ///// + + /** Returns whether there is a SavesSyncRequest running. */ + virtual bool isSyncing(); + + /** Returns a number in [0, 1] range which represents current sync progress (1 = complete). */ + virtual double getSyncDownloadingProgress(); + + /** Returns a number in [0, 1] range which represents current sync progress (1 = complete). */ + virtual double getSyncProgress(); + + /** Returns an array of saves names which are not yet synced (thus cannot be used). */ + virtual Common::Array<Common::String> getSyncingFiles(); + + /** Cancels running sync. */ + virtual void cancelSync(); + + /** Sets SavesSyncRequest's target to given CommandReceiver. */ + virtual void setSyncTarget(GUI::CommandReceiver *target); + +protected: + /** Finishes the sync. Shows an OSD message. */ + virtual void savesSyncDefaultCallback(BoolResponse response); + + /** Finishes the sync. Shows an OSD message. */ + virtual void savesSyncDefaultErrorCallback(Networking::ErrorResponse error); + +public: + ///// DownloadFolderRequest-related ///// + + /** Starts a folder download. */ + virtual bool startDownload(Common::String remotePath, Common::String localPath); + + /** Cancels running download. */ + virtual void cancelDownload(); + + /** Sets FolderDownloadRequest's target to given CommandReceiver. */ + virtual void setDownloadTarget(GUI::CommandReceiver *target); + + /** Returns whether there is a FolderDownloadRequest running. */ + virtual bool isDownloading(); + + /** Returns a number in [0, 1] range which represents current download progress (1 = complete). */ + virtual double getDownloadingProgress(); + + /** Returns a number of bytes that is downloaded in current download progress. */ + virtual uint64 getDownloadBytesNumber(); + + /** Returns a total number of bytes to be downloaded in current download progress. */ + virtual uint64 getDownloadTotalBytesNumber(); + + /** Returns download speed of current download progress. */ + virtual uint64 getDownloadSpeed(); + + /** Returns remote directory path. */ + virtual Common::String getDownloadRemoteDirectory(); + + /** Returns local directory path. */ + virtual Common::String getDownloadLocalDirectory(); + +protected: + /** Finishes the download. Shows an OSD message. */ + virtual void directoryDownloadedCallback(FileArrayResponse response); + + /** Finishes the download. Shows an OSD message. */ + virtual void directoryDownloadedErrorCallback(Networking::ErrorResponse error); +}; + +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/storagefile.cpp b/backends/cloud/storagefile.cpp new file mode 100644 index 0000000000..62d492292d --- /dev/null +++ b/backends/cloud/storagefile.cpp @@ -0,0 +1,68 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/cloud/storagefile.h" + +namespace Cloud { + +StorageFile::StorageFile() { + _id = ""; + _path = ""; + _name = ""; + _size = 0; + _timestamp = 0; + _isDirectory = false; +} + +StorageFile::StorageFile(Common::String pth, uint32 sz, uint32 ts, bool dir) { + _id = pth; + _path = pth; + + _name = pth; + if (_name.size() != 0) { + uint32 i = _name.size() - 1; + while (true) { + if (_name[i] == '/' || _name[i] == '\\') { + _name.erase(0, i + 1); + break; + } + if (i == 0) + break; + --i; + } + } + + _size = sz; + _timestamp = ts; + _isDirectory = dir; +} + +StorageFile::StorageFile(Common::String id, Common::String path, Common::String name, uint32 sz, uint32 ts, bool dir) { + _id = id; + _path = path; + _name = name; + _size = sz; + _timestamp = ts; + _isDirectory = dir; +} + +} // End of namespace Cloud diff --git a/backends/cloud/storagefile.h b/backends/cloud/storagefile.h new file mode 100644 index 0000000000..1183524f48 --- /dev/null +++ b/backends/cloud/storagefile.h @@ -0,0 +1,65 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_STORAGEFILE_H +#define BACKENDS_CLOUD_STORAGEFILE_H + +#include "common/str.h" + +namespace Cloud { + +/** + * StorageFile represents a file storaged on remote cloud storage. + * It contains basic information about a file, and might be used + * when listing directories or syncing files. + * + * Some storages (Google Drive, for example) don't have an actual + * path notation to address files. Instead, they are using ids. + * As resolving id by path is not a fast operation, it's required + * to use ids if they are known, but user-friendly paths are + * necessary too, because these are used by Requests. + * + * If storage supports path notation, id would actually contain path. + */ +class StorageFile { + Common::String _id, _path, _name; + uint32 _size, _timestamp; + bool _isDirectory; + +public: + StorageFile(); //invalid empty file + StorageFile(Common::String pth, uint32 sz, uint32 ts, bool dir); + StorageFile(Common::String id, Common::String path, Common::String name, uint32 sz, uint32 ts, bool dir); + + Common::String id() const { return _id; } + Common::String path() const { return _path; } + Common::String name() const { return _name; } + uint32 size() const { return _size; } + uint32 timestamp() const { return _timestamp; } + bool isDirectory() const { return _isDirectory; } + + void setPath(Common::String path_) { _path = path_; } +}; + +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/storageinfo.h b/backends/cloud/storageinfo.h new file mode 100644 index 0000000000..f1fb540974 --- /dev/null +++ b/backends/cloud/storageinfo.h @@ -0,0 +1,53 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_CLOUD_STORAGEINFO_H +#define BACKENDS_CLOUD_STORAGEINFO_H + +#include "common/str.h" + +namespace Cloud { + +/** +* StorageInfo contains information about remote cloud storage. +* It's disk quota usage, owner name, and such. +*/ + +class StorageInfo { + Common::String _uid, _name, _email; + uint64 _usedBytes, _allocatedBytes; + +public: + StorageInfo(Common::String uid_, Common::String name_, Common::String email_, uint64 used_, uint64 allocated): + _uid(uid_), _name(name_), _email(email_), _usedBytes(used_), _allocatedBytes(allocated) {} + + Common::String uid() const { return _uid; } + Common::String name() const { return _name; } + Common::String email() const { return _email; } + uint64 used() const { return _usedBytes; } + uint64 available() const { return _allocatedBytes; } + +}; + +} // End of namespace Cloud + +#endif diff --git a/backends/fs/abstract-fs.h b/backends/fs/abstract-fs.h index dcfdc08975..24286b649d 100644 --- a/backends/fs/abstract-fs.h +++ b/backends/fs/abstract-fs.h @@ -191,6 +191,15 @@ public: * @return pointer to the stream object, 0 in case of a failure */ virtual Common::WriteStream *createWriteStream() = 0; + + /** + * Creates a file referred by this node. + * + * @param isDirectory true if created file must be a directory + * + * @return true if file is created successfully + */ + virtual bool create(bool isDirectory) = 0; }; diff --git a/backends/fs/amigaos4/amigaos4-fs.cpp b/backends/fs/amigaos4/amigaos4-fs.cpp index 6d5b099736..24a8fb98ad 100644 --- a/backends/fs/amigaos4/amigaos4-fs.cpp +++ b/backends/fs/amigaos4/amigaos4-fs.cpp @@ -443,4 +443,9 @@ Common::WriteStream *AmigaOSFilesystemNode::createWriteStream() { return StdioStream::makeFromPath(getPath(), true); } +bool AmigaOSFilesystemNode::create(bool isDirectory) { + error("Not supported"); + return false; +} + #endif //defined(__amigaos4__) diff --git a/backends/fs/amigaos4/amigaos4-fs.h b/backends/fs/amigaos4/amigaos4-fs.h index 408354888e..3ed45d3f77 100644 --- a/backends/fs/amigaos4/amigaos4-fs.h +++ b/backends/fs/amigaos4/amigaos4-fs.h @@ -116,6 +116,7 @@ public: virtual Common::SeekableReadStream *createReadStream(); virtual Common::WriteStream *createWriteStream(); + virtual bool create(bool isDirectory); }; diff --git a/backends/fs/chroot/chroot-fs.cpp b/backends/fs/chroot/chroot-fs.cpp index 2cbb4af9d6..f5f7c84570 100644 --- a/backends/fs/chroot/chroot-fs.cpp +++ b/backends/fs/chroot/chroot-fs.cpp @@ -108,6 +108,11 @@ Common::WriteStream *ChRootFilesystemNode::createWriteStream() { return _realNode->createWriteStream(); } +bool ChRootFilesystemNode::create(bool isDirectory) { + error("Not supported"); + return false; +} + Common::String ChRootFilesystemNode::addPathComponent(const Common::String &path, const Common::String &component) { const char sep = '/'; if (path.lastChar() == sep && component.firstChar() == sep) { diff --git a/backends/fs/chroot/chroot-fs.h b/backends/fs/chroot/chroot-fs.h index 9ff913be31..e0ecc1c47e 100644 --- a/backends/fs/chroot/chroot-fs.h +++ b/backends/fs/chroot/chroot-fs.h @@ -49,6 +49,7 @@ public: virtual Common::SeekableReadStream *createReadStream(); virtual Common::WriteStream *createWriteStream(); + virtual bool create(bool isDirectory); private: static Common::String addPathComponent(const Common::String &path, const Common::String &component); diff --git a/backends/fs/ds/ds-fs.cpp b/backends/fs/ds/ds-fs.cpp index 3782caf432..83b1295838 100644 --- a/backends/fs/ds/ds-fs.cpp +++ b/backends/fs/ds/ds-fs.cpp @@ -211,6 +211,11 @@ Common::WriteStream *DSFileSystemNode::createWriteStream() { return Common::wrapBufferedWriteStream(stream, WRITE_BUFFER_SIZE); } +bool DSFileSystemNode::create(bool isDirectory) { + error("Not supported"); + return false; +} + ////////////////////////////////////////////////////////////////////////// // GBAMPFileSystemNode - File system using GBA Movie Player and CF card // ////////////////////////////////////////////////////////////////////////// @@ -393,6 +398,11 @@ Common::WriteStream *GBAMPFileSystemNode::createWriteStream() { return Common::wrapBufferedWriteStream(stream, WRITE_BUFFER_SIZE); } +bool GBAMPFileSystemNode::create(bool isDirectory) { + error("Not supported"); + return false; +} + DSFileStream::DSFileStream(void *handle) : _handle(handle) { diff --git a/backends/fs/ds/ds-fs.h b/backends/fs/ds/ds-fs.h index b9ccfcbe9c..939d1a1555 100644 --- a/backends/fs/ds/ds-fs.h +++ b/backends/fs/ds/ds-fs.h @@ -91,6 +91,7 @@ public: virtual Common::SeekableReadStream *createReadStream(); virtual Common::WriteStream *createWriteStream(); + virtual bool create(bool isDirectory); /** * Returns the zip file this node points to. @@ -156,6 +157,7 @@ public: virtual Common::SeekableReadStream *createReadStream(); virtual Common::WriteStream *createWriteStream(); + virtual bool create(bool isDirectory); }; struct fileHandle { diff --git a/backends/fs/n64/n64-fs.cpp b/backends/fs/n64/n64-fs.cpp index fe37dad467..d568500d30 100644 --- a/backends/fs/n64/n64-fs.cpp +++ b/backends/fs/n64/n64-fs.cpp @@ -160,4 +160,9 @@ Common::WriteStream *N64FilesystemNode::createWriteStream() { return RomfsStream::makeFromPath(getPath(), true); } +bool N64FilesystemNode::create(bool isDirectory) { + error("Not supported"); + return false; +} + #endif //#ifdef __N64__ diff --git a/backends/fs/n64/n64-fs.h b/backends/fs/n64/n64-fs.h index d503a6601e..d520ad5be6 100644 --- a/backends/fs/n64/n64-fs.h +++ b/backends/fs/n64/n64-fs.h @@ -73,6 +73,7 @@ public: virtual Common::SeekableReadStream *createReadStream(); virtual Common::WriteStream *createWriteStream(); + virtual bool create(bool isDirectory); }; #endif diff --git a/backends/fs/posix/posix-fs.cpp b/backends/fs/posix/posix-fs.cpp index 1a6c4e40a5..b38a07637c 100644 --- a/backends/fs/posix/posix-fs.cpp +++ b/backends/fs/posix/posix-fs.cpp @@ -39,6 +39,7 @@ #include <dirent.h> #include <stdio.h> #include <errno.h> +#include <fcntl.h> #ifdef __OS2__ #define INCL_DOS @@ -252,6 +253,30 @@ Common::WriteStream *POSIXFilesystemNode::createWriteStream() { return StdioStream::makeFromPath(getPath(), true); } +bool POSIXFilesystemNode::create(bool isDirectory) { + bool success; + + if (isDirectory) { + success = mkdir(_path.c_str(), 0755) == 0; + } else { + success = creat(_path.c_str(), 0755) != -1; + } + + if (success) { + setFlags(); + if (_isValid) { + if (_isDirectory != isDirectory) warning("failed to create %s: got %s", isDirectory ? "directory" : "file", _isDirectory ? "directory" : "file"); + return _isDirectory == isDirectory; + } + + warning("POSIXFilesystemNode: %s() was a success, but stat indicates there is no such %s", + isDirectory ? "mkdir" : "creat", isDirectory ? "directory" : "file"); + return false; + } + + return false; +} + namespace Posix { bool assureDirectoryExists(const Common::String &dir, const char *prefix) { diff --git a/backends/fs/posix/posix-fs.h b/backends/fs/posix/posix-fs.h index 0703ac5bf5..4ebce7e9d9 100644 --- a/backends/fs/posix/posix-fs.h +++ b/backends/fs/posix/posix-fs.h @@ -73,6 +73,7 @@ public: virtual Common::SeekableReadStream *createReadStream(); virtual Common::WriteStream *createWriteStream(); + virtual bool create(bool isDirectory); private: /** diff --git a/backends/fs/ps2/ps2-fs.cpp b/backends/fs/ps2/ps2-fs.cpp index 9b6e1270f1..8391e063a0 100644 --- a/backends/fs/ps2/ps2-fs.cpp +++ b/backends/fs/ps2/ps2-fs.cpp @@ -441,4 +441,9 @@ Common::WriteStream *Ps2FilesystemNode::createWriteStream() { return PS2FileStream::makeFromPath(getPath(), true); } +bool Ps2FilesystemNode::create(bool isDirectory) { + error("Not supported"); + return false; +} + #endif diff --git a/backends/fs/ps2/ps2-fs.h b/backends/fs/ps2/ps2-fs.h index 63b866ba5b..c9da434535 100644 --- a/backends/fs/ps2/ps2-fs.h +++ b/backends/fs/ps2/ps2-fs.h @@ -96,6 +96,7 @@ public: virtual Common::SeekableReadStream *createReadStream(); virtual Common::WriteStream *createWriteStream(); + virtual bool create(bool isDirectory); int getDev() { return 0; } }; diff --git a/backends/fs/psp/psp-fs.cpp b/backends/fs/psp/psp-fs.cpp index e8aad9fa98..6bd5e93435 100644 --- a/backends/fs/psp/psp-fs.cpp +++ b/backends/fs/psp/psp-fs.cpp @@ -239,4 +239,9 @@ Common::WriteStream *PSPFilesystemNode::createWriteStream() { return Common::wrapBufferedWriteStream(stream, WRITE_BUFFER_SIZE); } +bool PSPFilesystemNode::create(bool isDirectory) { + error("Not supported"); + return false; +} + #endif //#ifdef __PSP__ diff --git a/backends/fs/psp/psp-fs.h b/backends/fs/psp/psp-fs.h index 1bb4543d19..648544650b 100644 --- a/backends/fs/psp/psp-fs.h +++ b/backends/fs/psp/psp-fs.h @@ -65,6 +65,7 @@ public: virtual Common::SeekableReadStream *createReadStream(); virtual Common::WriteStream *createWriteStream(); + virtual bool create(bool isDirectory); }; #endif diff --git a/backends/fs/symbian/symbian-fs.cpp b/backends/fs/symbian/symbian-fs.cpp index 8fbc3a402a..e4163caa33 100644 --- a/backends/fs/symbian/symbian-fs.cpp +++ b/backends/fs/symbian/symbian-fs.cpp @@ -231,4 +231,10 @@ Common::SeekableReadStream *SymbianFilesystemNode::createReadStream() { Common::WriteStream *SymbianFilesystemNode::createWriteStream() { return SymbianStdioStream::makeFromPath(getPath(), true); } + +bool SymbianFilesystemNode::create(bool isDirectory) { + error("Not supported"); + return false; +} + #endif //#if defined(__SYMBIAN32__) diff --git a/backends/fs/symbian/symbian-fs.h b/backends/fs/symbian/symbian-fs.h index 339e998a28..1327f9f2e9 100644 --- a/backends/fs/symbian/symbian-fs.h +++ b/backends/fs/symbian/symbian-fs.h @@ -66,6 +66,7 @@ public: virtual Common::SeekableReadStream *createReadStream(); virtual Common::WriteStream *createWriteStream(); + virtual bool create(bool isDirectory); }; #endif diff --git a/backends/fs/wii/wii-fs.cpp b/backends/fs/wii/wii-fs.cpp index 43f4f592b7..46041bf499 100644 --- a/backends/fs/wii/wii-fs.cpp +++ b/backends/fs/wii/wii-fs.cpp @@ -213,4 +213,9 @@ Common::WriteStream *WiiFilesystemNode::createWriteStream() { return StdioStream::makeFromPath(getPath(), true); } +bool WiiFilesystemNode::create(bool isDirectory) { + error("Not supported"); + return false; +} + #endif //#if defined(__WII__) diff --git a/backends/fs/wii/wii-fs.h b/backends/fs/wii/wii-fs.h index c77c543dae..affb765884 100644 --- a/backends/fs/wii/wii-fs.h +++ b/backends/fs/wii/wii-fs.h @@ -69,6 +69,7 @@ public: virtual Common::SeekableReadStream *createReadStream(); virtual Common::WriteStream *createWriteStream(); + virtual bool create(bool isDirectory); }; #endif diff --git a/backends/fs/windows/windows-fs.cpp b/backends/fs/windows/windows-fs.cpp index 49549b83cb..b43686f911 100644 --- a/backends/fs/windows/windows-fs.cpp +++ b/backends/fs/windows/windows-fs.cpp @@ -244,4 +244,36 @@ Common::WriteStream *WindowsFilesystemNode::createWriteStream() { return StdioStream::makeFromPath(getPath(), true); } +bool WindowsFilesystemNode::create(bool isDirectory) { + bool success; + + if (isDirectory) { + success = CreateDirectory(toUnicode(_path.c_str()), NULL) != 0; + } else { + success = CreateFile(toUnicode(_path.c_str()), GENERIC_READ|GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL) != INVALID_HANDLE_VALUE; + } + + if (success) { + //this piece is copied from constructor, it checks that file exists and detects whether it's a directory + DWORD fileAttribs = GetFileAttributes(toUnicode(_path.c_str())); + if (fileAttribs != INVALID_FILE_ATTRIBUTES) { + _isDirectory = ((fileAttribs & FILE_ATTRIBUTE_DIRECTORY) != 0); + _isValid = true; + // Add a trailing slash, if necessary. + if (_isDirectory && _path.lastChar() != '\\') { + _path += '\\'; + } + + if (_isDirectory != isDirectory) warning("failed to create %s: got %s", isDirectory ? "directory" : "file", _isDirectory ? "directory" : "file"); + return _isDirectory == isDirectory; + } + + warning("WindowsFilesystemNode: Create%s() was a success, but GetFileAttributes() indicates there is no such %s", + isDirectory ? "Directory" : "File", isDirectory ? "directory" : "file"); + return false; + } + + return false; +} + #endif //#ifdef WIN32 diff --git a/backends/fs/windows/windows-fs.h b/backends/fs/windows/windows-fs.h index d06044603a..7c9f2c1e1a 100644 --- a/backends/fs/windows/windows-fs.h +++ b/backends/fs/windows/windows-fs.h @@ -88,6 +88,7 @@ public: virtual Common::SeekableReadStream *createReadStream(); virtual Common::WriteStream *createWriteStream(); + virtual bool create(bool isDirectory); private: /** diff --git a/backends/graphics/graphics.h b/backends/graphics/graphics.h index 3671b9f0b9..921dfca61c 100644 --- a/backends/graphics/graphics.h +++ b/backends/graphics/graphics.h @@ -84,6 +84,10 @@ public: virtual void setCursorPalette(const byte *colors, uint start, uint num) = 0; virtual void displayMessageOnOSD(const char *msg) {} + virtual void copyRectToOSD(const void *buf, int pitch, int x, int y, int w, int h) {} + virtual void clearOSD() {} + virtual Graphics::PixelFormat getOSDFormat() { return Graphics::PixelFormat(); } + // Graphics::PaletteManager interface //virtual void setPalette(const byte *colors, uint start, uint num) = 0; diff --git a/backends/graphics/opengl/opengl-graphics.cpp b/backends/graphics/opengl/opengl-graphics.cpp index 4d6a00a3b3..c491b03f1f 100644 --- a/backends/graphics/opengl/opengl-graphics.cpp +++ b/backends/graphics/opengl/opengl-graphics.cpp @@ -751,6 +751,31 @@ void OpenGLGraphicsManager::displayMessageOnOSD(const char *msg) { #endif } +void OpenGLGraphicsManager::copyRectToOSD(const void *buf, int pitch, int x, int y, int w, int h) { +#ifdef USE_OSD + warning("implement copyRectToOSD"); //TODO +#endif +} + +void OpenGLGraphicsManager::clearOSD() { +#ifdef USE_OSD + // HACK: Actually no client code should use graphics functions from + // another thread. But the MT-32 emulator still does, thus we need to + // make sure this doesn't happen while a updateScreen call is done. + Common::StackLock lock(_osdMutex); + + Graphics::Surface *dst = _osd->getSurface(); + _osd->fill(0); + _osd->flagDirty(); + + // Init the OSD display parameters. + _osdAlpha = kOSDInitialAlpha; + _osdFadeStartTime = g_system->getMillis() + kOSDFadeOutDelay; +#endif +} + +Graphics::PixelFormat OpenGLGraphicsManager::getOSDFormat() { return Graphics::PixelFormat(); } //TODO + void OpenGLGraphicsManager::setPalette(const byte *colors, uint start, uint num) { assert(_gameScreen->hasPalette()); diff --git a/backends/graphics/opengl/opengl-graphics.h b/backends/graphics/opengl/opengl-graphics.h index 35435c156e..55d2c5c826 100644 --- a/backends/graphics/opengl/opengl-graphics.h +++ b/backends/graphics/opengl/opengl-graphics.h @@ -115,6 +115,9 @@ public: virtual void setCursorPalette(const byte *colors, uint start, uint num); virtual void displayMessageOnOSD(const char *msg); + virtual void copyRectToOSD(const void *buf, int pitch, int x, int y, int w, int h); + virtual void clearOSD(); + virtual Graphics::PixelFormat getOSDFormat(); // PaletteManager interface virtual void setPalette(const byte *colors, uint start, uint num); diff --git a/backends/graphics/surfacesdl/surfacesdl-graphics.cpp b/backends/graphics/surfacesdl/surfacesdl-graphics.cpp index 5b591e77ff..b4b0d33b85 100644 --- a/backends/graphics/surfacesdl/surfacesdl-graphics.cpp +++ b/backends/graphics/surfacesdl/surfacesdl-graphics.cpp @@ -121,7 +121,7 @@ SurfaceSdlGraphicsManager::SurfaceSdlGraphicsManager(SdlEventSource *sdlEventSou : SdlGraphicsManager(sdlEventSource, window), #ifdef USE_OSD - _osdSurface(0), _osdAlpha(SDL_ALPHA_TRANSPARENT), _osdFadeStartTime(0), + _osdSurface(0), _osdMessageSurface(nullptr), _osdMessageAlpha(SDL_ALPHA_TRANSPARENT), _osdMessageFadeStartTime(0), #endif _hwscreen(0), #if SDL_VERSION_ATLEAST(2, 0, 0) @@ -883,13 +883,16 @@ bool SurfaceSdlGraphicsManager::loadGFXMode() { _osdSurface = SDL_CreateRGBSurface(SDL_SWSURFACE | SDL_RLEACCEL | SDL_SRCCOLORKEY | SDL_SRCALPHA, _hwscreen->w, _hwscreen->h, - 16, - _hwscreen->format->Rmask, - _hwscreen->format->Gmask, - _hwscreen->format->Bmask, - _hwscreen->format->Amask); + 32, + 0xFF000000, + 0x00FF0000, + 0x0000FF00, + 0x000000FF + ); if (_osdSurface == NULL) error("allocating _osdSurface failed"); + + _osdFormat = getSurfaceFormat(_osdSurface); SDL_SetColorKey(_osdSurface, SDL_RLEACCEL | SDL_SRCCOLORKEY | SDL_SRCALPHA, kOSDColorKey); #endif @@ -946,6 +949,11 @@ void SurfaceSdlGraphicsManager::unloadGFXMode() { SDL_FreeSurface(_osdSurface); _osdSurface = NULL; } + + if (_osdMessageSurface) { + SDL_FreeSurface(_osdMessageSurface); + _osdMessageSurface = NULL; + } #endif DestroyScalers(); @@ -1059,21 +1067,33 @@ void SurfaceSdlGraphicsManager::internUpdateScreen() { #ifdef USE_OSD // OSD visible (i.e. non-transparent)? - if (_osdAlpha != SDL_ALPHA_TRANSPARENT) { + if (_osdMessageAlpha != SDL_ALPHA_TRANSPARENT) { // Updated alpha value - const int diff = SDL_GetTicks() - _osdFadeStartTime; + const int diff = SDL_GetTicks() - _osdMessageFadeStartTime; if (diff > 0) { if (diff >= kOSDFadeOutDuration) { // Back to full transparency - _osdAlpha = SDL_ALPHA_TRANSPARENT; + _osdMessageAlpha = SDL_ALPHA_TRANSPARENT; } else { // Do a linear fade out... const int startAlpha = SDL_ALPHA_TRANSPARENT + kOSDInitialAlpha * (SDL_ALPHA_OPAQUE - SDL_ALPHA_TRANSPARENT) / 100; - _osdAlpha = startAlpha + diff * (SDL_ALPHA_TRANSPARENT - startAlpha) / kOSDFadeOutDuration; + _osdMessageAlpha = startAlpha + diff * (SDL_ALPHA_TRANSPARENT - startAlpha) / kOSDFadeOutDuration; } - SDL_SetAlpha(_osdSurface, SDL_RLEACCEL | SDL_SRCCOLORKEY | SDL_SRCALPHA, _osdAlpha); _forceFull = true; } + + if (_osdMessageAlpha == SDL_ALPHA_TRANSPARENT) { + removeOSDMessage(); + } else { + if (_osdMessageSurface && _osdSurface) { + SDL_Rect dstRect; + dstRect.x = (_osdSurface->w - _osdMessageSurface->w) / 2; + dstRect.y = (_osdSurface->h - _osdMessageSurface->h) / 2; + dstRect.w = _osdMessageSurface->w; + dstRect.h = _osdMessageSurface->h; + blitOSDMessage(dstRect); + } + } } #endif @@ -1179,9 +1199,7 @@ void SurfaceSdlGraphicsManager::internUpdateScreen() { drawMouse(); #ifdef USE_OSD - if (_osdAlpha != SDL_ALPHA_TRANSPARENT) { - SDL_BlitSurface(_osdSurface, 0, _hwscreen, 0); - } + SDL_BlitSurface(_osdSurface, 0, _hwscreen, 0); #endif #ifdef USE_SDL_DEBUG_FOCUSRECT @@ -2121,26 +2139,11 @@ void SurfaceSdlGraphicsManager::displayMessageOnOSD(const char *msg) { Common::StackLock lock(_graphicsMutex); // Lock the mutex until this function ends - uint i; - - // Lock the OSD surface for drawing - if (SDL_LockSurface(_osdSurface)) - error("displayMessageOnOSD: SDL_LockSurface failed: %s", SDL_GetError()); - - Graphics::Surface dst; - dst.init(_osdSurface->w, _osdSurface->h, _osdSurface->pitch, _osdSurface->pixels, - Graphics::PixelFormat(_osdSurface->format->BytesPerPixel, - 8 - _osdSurface->format->Rloss, 8 - _osdSurface->format->Gloss, - 8 - _osdSurface->format->Bloss, 8 - _osdSurface->format->Aloss, - _osdSurface->format->Rshift, _osdSurface->format->Gshift, - _osdSurface->format->Bshift, _osdSurface->format->Ashift)); + removeOSDMessage(); // The font we are going to use: const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kLocalizedFont); - // Clear everything with the "transparent" color, i.e. the colorkey - SDL_FillRect(_osdSurface, 0, kOSDColorKey); - // Split the message into separate lines. Common::Array<Common::String> lines; const char *ptr; @@ -2159,44 +2162,184 @@ void SurfaceSdlGraphicsManager::displayMessageOnOSD(const char *msg) { const int lineHeight = font->getFontHeight() + 2 * lineSpacing; int width = 0; int height = lineHeight * lines.size() + 2 * vOffset; + uint i; for (i = 0; i < lines.size(); i++) { width = MAX(width, font->getStringWidth(lines[i]) + 14); } // Clip the rect - if (width > dst.w) - width = dst.w; - if (height > dst.h) - height = dst.h; + if (width > _osdSurface->w) + width = _osdSurface->w; + if (height > _osdSurface->h) + height = _osdSurface->h; + + _osdMessageSurface = SDL_CreateRGBSurface( + SDL_SWSURFACE | SDL_RLEACCEL | SDL_SRCCOLORKEY | SDL_SRCALPHA, + width, height, 32, 0xFF000000, 0x00FF0000, 0x0000FF00, 0x000000FF + ); + + // Lock the surface + if (SDL_LockSurface(_osdMessageSurface)) + error("displayMessageOnOSD: SDL_LockSurface failed: %s", SDL_GetError()); // Draw a dark gray rect - // TODO: Rounded corners ? Border? - SDL_Rect osdRect; - osdRect.x = (dst.w - width) / 2; - osdRect.y = (dst.h - height) / 2; - osdRect.w = width; - osdRect.h = height; - SDL_FillRect(_osdSurface, &osdRect, SDL_MapRGB(_osdSurface->format, 64, 64, 64)); + // TODO: Rounded corners ? Border? + SDL_FillRect(_osdMessageSurface, nullptr, SDL_MapRGB(_osdMessageSurface->format, 64, 64, 64)); + + Graphics::Surface dst; + dst.init(_osdMessageSurface->w, _osdMessageSurface->h, _osdMessageSurface->pitch, _osdMessageSurface->pixels, + Graphics::PixelFormat(_osdMessageSurface->format->BytesPerPixel, + 8 - _osdMessageSurface->format->Rloss, 8 - _osdMessageSurface->format->Gloss, + 8 - _osdMessageSurface->format->Bloss, 8 - _osdMessageSurface->format->Aloss, + _osdMessageSurface->format->Rshift, _osdMessageSurface->format->Gshift, + _osdMessageSurface->format->Bshift, _osdMessageSurface->format->Ashift)); // Render the message, centered, and in white for (i = 0; i < lines.size(); i++) { font->drawString(&dst, lines[i], - osdRect.x, osdRect.y + i * lineHeight + vOffset + lineSpacing, osdRect.w, - SDL_MapRGB(_osdSurface->format, 255, 255, 255), - Graphics::kTextAlignCenter); + 0, 0 + i * lineHeight + vOffset + lineSpacing, width, + SDL_MapRGB(_osdMessageSurface->format, 255, 255, 255), + Graphics::kTextAlignCenter); + } + + // Finished drawing, so unlock the OSD message surface + SDL_UnlockSurface(_osdMessageSurface); + + // Init the OSD display parameters, and the fade out + _osdMessageAlpha = SDL_ALPHA_TRANSPARENT + kOSDInitialAlpha * (SDL_ALPHA_OPAQUE - SDL_ALPHA_TRANSPARENT) / 100; + _osdMessageFadeStartTime = SDL_GetTicks() + kOSDFadeOutDelay; + + // Ensure a full redraw takes place next time the screen is updated + _forceFull = true; +} + +void SurfaceSdlGraphicsManager::copyRectToOSD(const void *buf, int pitch, int x, int y, int w, int h) { + assert(_transactionMode == kTransactionNone); + assert(buf); + + Common::StackLock lock(_graphicsMutex); // Lock the mutex until this function ends + + // Lock the OSD surface for drawing + if (SDL_LockSurface(_osdSurface)) + error("displayMessageOnOSD: SDL_LockSurface failed: %s", SDL_GetError()); + + // Mark that area as "dirty" + addDirtyRect(x, y, w, h, true); + +#ifdef USE_RGB_COLOR + byte *dst = (byte *)_osdSurface->pixels + y * _osdSurface->pitch + x * _osdSurface->format->BytesPerPixel; + if (_videoMode.screenWidth == w && pitch == _osdSurface->pitch) { + memcpy(dst, buf, h*pitch); + } else { + const byte *src = (const byte *)buf; + do { + memcpy(dst, src, w * _osdSurface->format->BytesPerPixel); + src += pitch; + dst += _osdSurface->pitch; + } while (--h); } +#else + byte *dst = (byte *)_osdSurface->pixels + y * _osdSurface->pitch + x; + if (_osdSurface->pitch == pitch && pitch == w) { + memcpy(dst, buf, h*w); + } else { + const byte *src = (const byte *)buf; + do { + memcpy(dst, src, w); + src += pitch; + dst += _osdSurface->pitch; + } while (--h); + } +#endif // Finished drawing, so unlock the OSD surface again SDL_UnlockSurface(_osdSurface); +} - // Init the OSD display parameters, and the fade out - _osdAlpha = SDL_ALPHA_TRANSPARENT + kOSDInitialAlpha * (SDL_ALPHA_OPAQUE - SDL_ALPHA_TRANSPARENT) / 100; - _osdFadeStartTime = SDL_GetTicks() + kOSDFadeOutDelay; - SDL_SetAlpha(_osdSurface, SDL_RLEACCEL | SDL_SRCCOLORKEY | SDL_SRCALPHA, _osdAlpha); +void SurfaceSdlGraphicsManager::clearOSD() { + assert(_transactionMode == kTransactionNone); + + Common::StackLock lock(_graphicsMutex); // Lock the mutex until this function ends + + // Lock the OSD surface for drawing + if (SDL_LockSurface(_osdSurface)) + error("displayMessageOnOSD: SDL_LockSurface failed: %s", SDL_GetError()); + + // Clear everything with the "transparent" color, i.e. the colorkey + SDL_FillRect(_osdSurface, 0, kOSDColorKey); + + // Finished drawing, so unlock the OSD surface again + SDL_UnlockSurface(_osdSurface); + + // Remove OSD message as well + removeOSDMessage(); // Ensure a full redraw takes place next time the screen is updated _forceFull = true; } + +void SurfaceSdlGraphicsManager::removeOSDMessage() { + // Lock the OSD surface for drawing + if (SDL_LockSurface(_osdSurface)) + error("displayMessageOnOSD: SDL_LockSurface failed: %s", SDL_GetError()); + + //remove previous message + if (_osdMessageSurface) { + SDL_Rect osdRect; + osdRect.x = (_osdSurface->w - _osdMessageSurface->w) / 2; + osdRect.y = (_osdSurface->h - _osdMessageSurface->h) / 2; + osdRect.w = _osdMessageSurface->w; + osdRect.h = _osdMessageSurface->h; + SDL_FillRect(_osdSurface, &osdRect, kOSDColorKey); + SDL_FreeSurface(_osdMessageSurface); + } + + _osdMessageSurface = NULL; + _osdMessageAlpha = SDL_ALPHA_TRANSPARENT; + + // Finished drawing, so unlock the OSD surface again + SDL_UnlockSurface(_osdSurface); +} + +void SurfaceSdlGraphicsManager::blitOSDMessage(SDL_Rect dstRect) { + SDL_Surface *src = _osdMessageSurface; + SDL_Surface *dst = _osdSurface; + Graphics::PixelFormat srcFormat = getSurfaceFormat(src); + Graphics::PixelFormat dstFormat = _osdFormat; + for (int y = 0; y < dstRect.h; y++) { + const byte *srcRow = (const byte *)((const byte *)(src->pixels) + y * src->pitch); //src (x, y) == (0, 0) + byte *dstRow = (byte *)((const byte *)(dst->pixels) + (dstRect.y + y) * dst->pitch + dstRect.x * dstFormat.bytesPerPixel); + + for (int x = 0; x < dstRect.w; x++) { + uint32 srcColor; + if (dst->format->BytesPerPixel == 2) + srcColor = READ_UINT16(srcRow); + else if (dst->format->BytesPerPixel == 3) + srcColor = READ_UINT24(srcRow); + else + srcColor = READ_UINT32(srcRow); + + srcRow += srcFormat.bytesPerPixel; + + // Convert that color to the new format + byte r, g, b, a; + srcFormat.colorToARGB(srcColor, a, r, g, b); + a = _osdMessageAlpha; //this is the important line, because apart from that this is plain surface copying + uint32 color = dstFormat.ARGBToColor(a, r, g, b); + + if (dstFormat.bytesPerPixel == 2) + *((uint16 *)dstRow) = color; + else + *((uint32 *)dstRow) = color; + + dstRow += dstFormat.bytesPerPixel; + } + } +} + +Graphics::PixelFormat SurfaceSdlGraphicsManager::getOSDFormat() { + return _osdFormat; +} #endif bool SurfaceSdlGraphicsManager::handleScalerHotkeys(Common::KeyCode key) { @@ -2492,4 +2635,22 @@ void SurfaceSdlGraphicsManager::SDL_UpdateRects(SDL_Surface *screen, int numrect } #endif // SDL_VERSION_ATLEAST(2, 0, 0) +Graphics::PixelFormat SurfaceSdlGraphicsManager::getSurfaceFormat(SDL_Surface *surface) { + Graphics::PixelFormat format; + if (surface) { + format.bytesPerPixel = surface->format->BytesPerPixel; + + format.rLoss = surface->format->Rloss; + format.gLoss = surface->format->Gloss; + format.bLoss = surface->format->Bloss; + format.aLoss = surface->format->Aloss; + + format.rShift = surface->format->Rshift; + format.gShift = surface->format->Gshift; + format.bShift = surface->format->Bshift; + format.aShift = surface->format->Ashift; + } + return format; +} + #endif diff --git a/backends/graphics/surfacesdl/surfacesdl-graphics.h b/backends/graphics/surfacesdl/surfacesdl-graphics.h index 25d6ff041c..d8f826aca0 100644 --- a/backends/graphics/surfacesdl/surfacesdl-graphics.h +++ b/backends/graphics/surfacesdl/surfacesdl-graphics.h @@ -145,6 +145,9 @@ public: #ifdef USE_OSD virtual void displayMessageOnOSD(const char *msg); + virtual void copyRectToOSD(const void *buf, int pitch, int x, int y, int w, int h); + virtual void clearOSD(); + virtual Graphics::PixelFormat getOSDFormat(); #endif // Override from Common::EventObserver @@ -160,12 +163,14 @@ public: protected: #ifdef USE_OSD - /** Surface containing the OSD message */ + /** Surface containing the OSD */ SDL_Surface *_osdSurface; - /** Transparency level of the OSD */ - uint8 _osdAlpha; + /** Surface containing the OSD message */ + SDL_Surface *_osdMessageSurface; + /** Transparency level of the OSD message */ + uint8 _osdMessageAlpha; /** When to start the fade out */ - uint32 _osdFadeStartTime; + uint32 _osdMessageFadeStartTime; /** Enum with OSD options */ enum { kOSDFadeOutDelay = 2 * 1000, /** < Delay before the OSD is faded out (in milliseconds) */ @@ -173,6 +178,11 @@ protected: kOSDColorKey = 1, /** < Transparent color key */ kOSDInitialAlpha = 80 /** < Initial alpha level, in percent */ }; + /** OSD pixel format */ + Graphics::PixelFormat _osdFormat; + + void removeOSDMessage(); + void blitOSDMessage(SDL_Rect dstRect); #endif /** Hardware screen */ @@ -358,6 +368,8 @@ protected: Common::Rect _focusRect; #endif + static Graphics::PixelFormat getSurfaceFormat(SDL_Surface *surface); + virtual void addDirtyRect(int x, int y, int w, int h, bool realCoordinates = false); virtual void drawMouse(); diff --git a/backends/modular-backend.cpp b/backends/modular-backend.cpp index d8be9ca7ed..e1bdf15571 100644 --- a/backends/modular-backend.cpp +++ b/backends/modular-backend.cpp @@ -241,6 +241,18 @@ void ModularBackend::displayMessageOnOSD(const char *msg) { _graphicsManager->displayMessageOnOSD(msg); } +void ModularBackend::copyRectToOSD(const void *buf, int pitch, int x, int y, int w, int h) { + _graphicsManager->copyRectToOSD(buf, pitch, x, y, w, h); +} + +void ModularBackend::clearOSD() { + _graphicsManager->clearOSD(); +} + +Graphics::PixelFormat ModularBackend::getOSDFormat() { + return _graphicsManager->getOSDFormat(); +} + void ModularBackend::quit() { exit(0); } diff --git a/backends/modular-backend.h b/backends/modular-backend.h index 20e8b7357d..9cde27915f 100644 --- a/backends/modular-backend.h +++ b/backends/modular-backend.h @@ -127,6 +127,9 @@ public: virtual void quit(); virtual void displayMessageOnOSD(const char *msg); + virtual void copyRectToOSD(const void *buf, int pitch, int x, int y, int w, int h); + virtual void clearOSD(); + virtual Graphics::PixelFormat getOSDFormat(); //@} diff --git a/backends/module.mk b/backends/module.mk index 4c1ca42f06..c402a10a90 100644 --- a/backends/module.mk +++ b/backends/module.mk @@ -19,6 +19,65 @@ MODULE_OBJS := \ saves/default/default-saves.o \ timer/default/default-timer.o +ifdef USE_LIBCURL +MODULE_OBJS += \ + cloud/cloudmanager.o \ + cloud/iso8601.o \ + cloud/storage.o \ + cloud/storagefile.o \ + cloud/downloadrequest.o \ + cloud/folderdownloadrequest.o \ + cloud/savessyncrequest.o \ + cloud/box/boxstorage.o \ + cloud/box/boxlistdirectorybyidrequest.o \ + cloud/box/boxtokenrefresher.o \ + cloud/box/boxuploadrequest.o \ + cloud/dropbox/dropboxstorage.o \ + cloud/dropbox/dropboxcreatedirectoryrequest.o \ + cloud/dropbox/dropboxinforequest.o \ + cloud/dropbox/dropboxlistdirectoryrequest.o \ + cloud/dropbox/dropboxuploadrequest.o \ + cloud/googledrive/googledrivelistdirectorybyidrequest.o \ + cloud/googledrive/googledrivestorage.o \ + cloud/googledrive/googledrivetokenrefresher.o \ + cloud/googledrive/googledriveuploadrequest.o \ + cloud/id/idstorage.o \ + cloud/id/idcreatedirectoryrequest.o \ + cloud/id/iddownloadrequest.o \ + cloud/id/idlistdirectoryrequest.o \ + cloud/id/idresolveidrequest.o \ + cloud/id/idstreamfilerequest.o \ + cloud/onedrive/onedrivestorage.o \ + cloud/onedrive/onedrivecreatedirectoryrequest.o \ + cloud/onedrive/onedrivetokenrefresher.o \ + cloud/onedrive/onedrivelistdirectoryrequest.o \ + cloud/onedrive/onedriveuploadrequest.o \ + networking/curl/connectionmanager.o \ + networking/curl/networkreadstream.o \ + networking/curl/cloudicon.o \ + networking/curl/curlrequest.o \ + networking/curl/curljsonrequest.o \ + networking/curl/request.o +endif + +ifdef USE_SDL_NET +MODULE_OBJS += \ + networking/sdl_net/client.o \ + networking/sdl_net/getclienthandler.o \ + networking/sdl_net/handlers/createdirectoryhandler.o \ + networking/sdl_net/handlers/downloadfilehandler.o \ + networking/sdl_net/handlers/filesajaxpagehandler.o \ + networking/sdl_net/handlers/filesbasehandler.o \ + networking/sdl_net/handlers/filespagehandler.o \ + networking/sdl_net/handlers/indexpagehandler.o \ + networking/sdl_net/handlers/listajaxhandler.o \ + networking/sdl_net/handlers/resourcehandler.o \ + networking/sdl_net/handlers/uploadfilehandler.o \ + networking/sdl_net/handlerutils.o \ + networking/sdl_net/localwebserver.o \ + networking/sdl_net/reader.o \ + networking/sdl_net/uploadfileclienthandler.o +endif ifdef USE_ELF_LOADER MODULE_OBJS += \ @@ -91,6 +150,42 @@ MODULE_OBJS += \ endif endif +# openUrl +ifeq ($(BACKEND),android) +MODULE_OBJS += \ + networking/browser/openurl-android.o +else +ifdef MACOSX +MODULE_OBJS += \ + networking/browser/openurl-osx.o +else +ifdef WIN32 +MODULE_OBJS += \ + networking/browser/openurl-windows.o +else + ifdef POSIX + MODULE_OBJS += \ + networking/browser/openurl-posix.o + else + # create_project doesn't know something about `else` + ifndef WIN32 + MODULE_OBJS += \ + networking/browser/openurl-default.o + endif + endif +endif +endif +endif + +# Connection::isLimited +ifeq ($(BACKEND),android) +MODULE_OBJS += \ + networking/connection/islimited-android.o +else +MODULE_OBJS += \ + networking/connection/islimited-default.o +endif + ifdef POSIX MODULE_OBJS += \ fs/posix/posix-fs.o \ diff --git a/backends/networking/browser/openurl-android.cpp b/backends/networking/browser/openurl-android.cpp new file mode 100644 index 0000000000..64e683238b --- /dev/null +++ b/backends/networking/browser/openurl-android.cpp @@ -0,0 +1,35 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "backends/networking/browser/openurl.h" +#include "backends/platform/android/jni.h" + +namespace Networking { +namespace Browser { + +bool openUrl(const Common::String &url) { + return JNI::openUrl(url.c_str()); +} + +} // End of namespace Browser +} // End of namespace Networking + diff --git a/backends/networking/browser/openurl-default.cpp b/backends/networking/browser/openurl-default.cpp new file mode 100644 index 0000000000..c430953196 --- /dev/null +++ b/backends/networking/browser/openurl-default.cpp @@ -0,0 +1,36 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "backends/networking/browser/openurl.h" +#include "common/textconsole.h" + +namespace Networking { +namespace Browser { + +bool openUrl(const Common::String &url) { + warning("Networking::Browser::openUrl(): not implemented"); + return false; +} + +} // End of namespace Browser +} // End of namespace Networking + diff --git a/backends/networking/browser/openurl-osx.cpp b/backends/networking/browser/openurl-osx.cpp new file mode 100644 index 0000000000..8d786d7fd2 --- /dev/null +++ b/backends/networking/browser/openurl-osx.cpp @@ -0,0 +1,49 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#define FORBIDDEN_SYMBOL_ALLOW_ALL + +#include "backends/networking/browser/openurl.h" +#include <CoreFoundation/CFBundle.h> +#include <ApplicationServices/ApplicationServices.h> + +namespace Networking { +namespace Browser { + +using namespace std; + +bool openUrl(const Common::String &url) { + CFURLRef urlRef = CFURLCreateWithBytes ( + NULL, + (UInt8*)url.c_str(), + url.size(), + kCFStringEncodingASCII, + NULL + ); + int result = LSOpenCFURLRef(urlRef, 0); + CFRelease(urlRef); + return result == 0; +} + +} // End of namespace Browser +} // End of namespace Networking + diff --git a/backends/networking/browser/openurl-posix.cpp b/backends/networking/browser/openurl-posix.cpp new file mode 100644 index 0000000000..429a379fcf --- /dev/null +++ b/backends/networking/browser/openurl-posix.cpp @@ -0,0 +1,77 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#define FORBIDDEN_SYMBOL_ALLOW_ALL + +#include "backends/networking/browser/openurl.h" +#include "common/textconsole.h" +#include <stdlib.h> + +namespace Networking { +namespace Browser { + +namespace { +bool launch(const Common::String client, const Common::String &url) { + // FIXME: system's input must be heavily escaped + // well, when url's specified by user + // it's OK now (urls are hardcoded somewhere in GUI) + Common::String cmd = client + " " + url; + return (system(cmd.c_str()) != -1); +} +} + +bool openUrl(const Common::String &url) { + // inspired by Qt's "qdesktopservices_x11.cpp" + + // try "standards" + if (launch("xdg-open", url)) + return true; + if (launch(getenv("DEFAULT_BROWSER"), url)) + return true; + if (launch(getenv("BROWSER"), url)) + return true; + + // try desktop environment specific tools + if (launch("gnome-open", url)) // gnome + return true; + if (launch("kfmclient openURL", url)) // kde + return true; + if (launch("exo-open", url)) // xfce + return true; + + // try browser names + if (launch("firefox", url)) + return true; + if (launch("mozilla", url)) + return true; + if (launch("netscape", url)) + return true; + if (launch("opera", url)) + return true; + + warning("Networking::Browser::openUrl() (POSIX) failed to open URL"); + return false; +} + +} // End of namespace Browser +} // End of namespace Networking + diff --git a/backends/networking/browser/openurl-windows.cpp b/backends/networking/browser/openurl-windows.cpp new file mode 100644 index 0000000000..53d76f076b --- /dev/null +++ b/backends/networking/browser/openurl-windows.cpp @@ -0,0 +1,43 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "backends/networking/browser/openurl.h" +#include "common/textconsole.h" +#include <windows.h> +#include <Shellapi.h> + +namespace Networking { +namespace Browser { + +bool openUrl(const Common::String &url) { + const uint64 result = (uint64)ShellExecute(0, 0, /*(wchar_t*)nativeFilePath.utf16()*/url.c_str(), 0, 0, SW_SHOWNORMAL); + // ShellExecute returns a value greater than 32 if successful + if (result <= 32) { + warning("ShellExecute failed: error = %u", result); + return false; + } + return true; +} + +} // End of namespace Browser +} // End of namespace Networking + diff --git a/backends/networking/browser/openurl.h b/backends/networking/browser/openurl.h new file mode 100644 index 0000000000..15b4bf385b --- /dev/null +++ b/backends/networking/browser/openurl.h @@ -0,0 +1,41 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef NETWORKING_BROWSER_OPENURL_H +#define NETWORKING_BROWSER_OPENURL_H + +#include "common/str.h" + +namespace Networking { +namespace Browser { + +/** + * Opens URL in default browser (if available on the target system). + * + * Returns true on success. + */ +bool openUrl(const Common::String &url); + +} // End of namespace Browser +} // End of namespace Networking + +#endif /*NETWORKING_BROWSER_OPENURL_H*/ diff --git a/backends/networking/connection/islimited-android.cpp b/backends/networking/connection/islimited-android.cpp new file mode 100644 index 0000000000..8989f218ec --- /dev/null +++ b/backends/networking/connection/islimited-android.cpp @@ -0,0 +1,35 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "backends/networking/connection/islimited.h" +#include "backends/platform/android/jni.h" + +namespace Networking { +namespace Connection { + +bool isLimited() { + return JNI::isConnectionLimited(); +} + +} // End of namespace Connection +} // End of namespace Networking + diff --git a/backends/networking/connection/islimited-default.cpp b/backends/networking/connection/islimited-default.cpp new file mode 100644 index 0000000000..a993077fff --- /dev/null +++ b/backends/networking/connection/islimited-default.cpp @@ -0,0 +1,36 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "backends/networking/connection/islimited.h" +#include "common/textconsole.h" + +namespace Networking { +namespace Connection { + +bool isLimited() { + warning("Networking::Connection::isLimited(): not limited by default"); + return false; +} + +} // End of namespace Connection +} // End of namespace Networking + diff --git a/backends/networking/connection/islimited.h b/backends/networking/connection/islimited.h new file mode 100644 index 0000000000..b23d31d157 --- /dev/null +++ b/backends/networking/connection/islimited.h @@ -0,0 +1,39 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef NETWORKING_CONNECTION_ISLIMITED_H +#define NETWORKING_CONNECTION_ISLIMITED_H + +namespace Networking { +namespace Connection { + +/** +* Returns whether connection's limited (if available on the target system). +* +* Returns true if connection seems limited. +*/ +bool isLimited(); + +} // End of namespace Connection +} // End of namespace Networking + +#endif /*NETWORKING_CONNECTION_ISLIMITED_H*/ diff --git a/backends/networking/curl/cloudicon.cpp b/backends/networking/curl/cloudicon.cpp new file mode 100644 index 0000000000..1c1ecf2f85 --- /dev/null +++ b/backends/networking/curl/cloudicon.cpp @@ -0,0 +1,171 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/networking/curl/cloudicon.h" +#include "backends/cloud/cloudmanager.h" +#include "common/memstream.h" +#include "gui/ThemeEngine.h" +#include "gui/gui-manager.h" +#include "image/png.h" + +namespace Networking { + +const float CloudIcon::ALPHA_STEP = 0.025; +const float CloudIcon::ALPHA_MAX = 1; +const float CloudIcon::ALPHA_MIN = 0.6; + +CloudIcon::CloudIcon(): + _wasVisible(false), _iconsInited(false), _showingDisabled(false), + _currentAlpha(0), _alphaRising(true), _disabledFrames(0) { + initIcons(); +} + +CloudIcon::~CloudIcon() {} + +bool CloudIcon::draw() { + bool stop = false; + initIcons(); + + if (CloudMan.isWorking() || _disabledFrames > 0) { + if (g_system) { + if (!_wasVisible) { + g_system->clearOSD(); + _wasVisible = true; + } + --_disabledFrames; + if (_alphaRising) { + if (_currentAlpha < ALPHA_MIN) + _currentAlpha += 5 * ALPHA_STEP; + else + _currentAlpha += ALPHA_STEP; + if (_currentAlpha > ALPHA_MAX) { + _currentAlpha = ALPHA_MAX; + _alphaRising = false; + } + } else { + _currentAlpha -= ALPHA_STEP; + if (_currentAlpha < ALPHA_MIN) { + _currentAlpha = ALPHA_MIN; + _alphaRising = true; + } + } + } else { + _wasVisible = false; + } + } else { + _wasVisible = false; + _currentAlpha -= 5 * ALPHA_STEP; + if (_currentAlpha <= 0) { + _currentAlpha = 0; + stop = true; + } + } + + if (g_system) { + Graphics::TransparentSurface *surface = &_icon; + makeAlphaIcon((_showingDisabled ? _disabledIcon : _icon), _currentAlpha); + if (_alphaIcon.getPixels()) + surface = &_alphaIcon; + if (surface && surface->getPixels()) { + int x = g_system->getOverlayWidth() - surface->w - 10, y = 10; + g_system->copyRectToOSD(surface->getPixels(), surface->pitch, x, y, surface->w, surface->h); + } + } + + if (stop) + _showingDisabled = false; + return stop; +} + +void CloudIcon::showDisabled() { + _showingDisabled = true; + _disabledFrames = 20 * 3; //3 seconds 20 fps +} + +#include "backends/networking/curl/cloudicon_data.h" +#include "backends/networking/curl/cloudicon_disabled_data.h" + +void CloudIcon::initIcons() { + if (_iconsInited) + return; + loadIcon(_icon, cloudicon_data, ARRAYSIZE(cloudicon_data)); + loadIcon(_disabledIcon, cloudicon_disabled_data, ARRAYSIZE(cloudicon_disabled_data)); + _iconsInited = true; +} + +void CloudIcon::loadIcon(Graphics::TransparentSurface &icon, byte *data, uint32 size) { + Image::PNGDecoder decoder; + Common::MemoryReadStream stream(data, size); + if (!decoder.loadStream(stream)) + error("CloudIcon::loadIcon: error decoding PNG"); + + Graphics::TransparentSurface *s = new Graphics::TransparentSurface(*decoder.getSurface(), true); + if (s) { + Graphics::PixelFormat f = g_system->getOSDFormat(); + if (f != s->format) { + Graphics::TransparentSurface *s2 = s->convertTo(f); + if (s2) + icon.copyFrom(*s2); + else + warning("CloudIcon::loadIcon: failed converting TransparentSurface"); + delete s2; + } else { + icon.copyFrom(*s); + } + delete s; + } else { + warning("CloudIcon::loadIcon: failed reading TransparentSurface from PNGDecoder"); + } +} + +void CloudIcon::makeAlphaIcon(Graphics::TransparentSurface &icon, float alpha) { + _alphaIcon.copyFrom(icon); + + byte *pixels = (byte *)_alphaIcon.getPixels(); + for (int y = 0; y < _alphaIcon.h; y++) { + byte *row = pixels + y * _alphaIcon.pitch; + for (int x = 0; x < _alphaIcon.w; x++) { + uint32 srcColor; + if (_alphaIcon.format.bytesPerPixel == 2) + srcColor = READ_UINT16(row); + else if (_alphaIcon.format.bytesPerPixel == 3) + srcColor = READ_UINT24(row); + else + srcColor = READ_UINT32(row); + + // Update color's alpha + byte r, g, b, a; + _alphaIcon.format.colorToARGB(srcColor, a, r, g, b); + a = (byte)(a * alpha); + uint32 color = _alphaIcon.format.ARGBToColor(a, r, g, b); + + if (_alphaIcon.format.bytesPerPixel == 2) + *((uint16 *)row) = color; + else + *((uint32 *)row) = color; + + row += _alphaIcon.format.bytesPerPixel; + } + } +} + +} // End of namespace Networking diff --git a/backends/networking/curl/cloudicon.h b/backends/networking/curl/cloudicon.h new file mode 100644 index 0000000000..7e4d7387a3 --- /dev/null +++ b/backends/networking/curl/cloudicon.h @@ -0,0 +1,70 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_NETWORKING_CURL_CLOUDICON_H +#define BACKENDS_NETWORKING_CURL_CLOUDICON_H + +#include "graphics/transparent_surface.h" + +namespace Networking { + +class CloudIcon { + static const float ALPHA_STEP, ALPHA_MAX, ALPHA_MIN; + + bool _wasVisible, _iconsInited, _showingDisabled; + Graphics::TransparentSurface _icon, _disabledIcon, _alphaIcon; + float _currentAlpha; + bool _alphaRising; + int _disabledFrames; + + void initIcons(); + void loadIcon(Graphics::TransparentSurface &icon, byte *data, uint32 size); + void makeAlphaIcon(Graphics::TransparentSurface &icon, float alpha); + +public: + CloudIcon(); + ~CloudIcon(); + + /** + * This method is called from ConnectionManager every time + * its own timer calls the handle() method. The primary + * responsibility of this draw() method is to draw cloud icon + * on ScummVM's OSD when current cloud Storage is working. + * + * As we don't want ConnectionManager to work when no + * Requests are running, we'd like to stop the timer. But then + * this icon wouldn't have time to disappear smoothly. So, + * in order to do that, ConnectionManager stop its timer + * only when this draw() method returns true, indicating that + * the CloudIcon has disappeared and the timer could be stopped. + * + * @return true if ConnMan's timer could be stopped. + */ + bool draw(); + + /** Draw a "cloud disabled" icon instead of "cloud syncing" one. */ + void showDisabled(); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/curl/cloudicon_data.h b/backends/networking/curl/cloudicon_data.h new file mode 100644 index 0000000000..21d88182a3 --- /dev/null +++ b/backends/networking/curl/cloudicon_data.h @@ -0,0 +1,111 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +// This is a PNG file dumped into array. +// $ recode data..d1 <dists/cloudicon.png >cloudicon_data.h +// The tool is from https://github.com/pinard/Recode + +byte cloudicon_data[] = { + 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, + 82, 0, 0, 0, 32, 0, 0, 0, 32, 8, 6, 0, 0, 0, 115, + 122, 122, 244, 0, 0, 0, 4, 115, 66, 73, 84, 8, 8, 8, 8, + 124, 8, 100, 136, 0, 0, 0, 9, 112, 72, 89, 115, 0, 0, 11, + 18, 0, 0, 11, 18, 1, 210, 221, 126, 252, 0, 0, 0, 22, 116, + 69, 88, 116, 67, 114, 101, 97, 116, 105, 111, 110, 32, 84, 105, 109, + 101, 0, 48, 54, 47, 48, 51, 47, 49, 54, 159, 192, 233, 192, 0, + 0, 0, 28, 116, 69, 88, 116, 83, 111, 102, 116, 119, 97, 114, 101, + 0, 65, 100, 111, 98, 101, 32, 70, 105, 114, 101, 119, 111, 114, 107, + 115, 32, 67, 83, 54, 232, 188, 178, 140, 0, 0, 4, 50, 73, 68, + 65, 84, 88, 133, 197, 151, 109, 104, 150, 101, 20, 199, 127, 247, 227, + 179, 105, 51, 23, 65, 181, 150, 224, 154, 214, 132, 194, 249, 33, 165, + 22, 189, 231, 194, 210, 250, 16, 171, 180, 55, 42, 152, 68, 65, 100, + 52, 233, 5, 146, 144, 144, 26, 249, 169, 62, 164, 80, 89, 152, 25, + 18, 226, 42, 49, 87, 88, 180, 94, 96, 96, 246, 234, 180, 70, 50, + 66, 214, 55, 247, 22, 133, 247, 255, 244, 225, 58, 247, 158, 107, 143, + 219, 243, 60, 186, 192, 3, 135, 251, 220, 215, 117, 223, 231, 127, 238, + 235, 252, 239, 235, 58, 39, 105, 250, 206, 56, 147, 146, 55, 85, 252, + 108, 13, 112, 59, 176, 12, 88, 2, 204, 7, 230, 248, 220, 48, 208, + 15, 244, 2, 221, 64, 23, 48, 86, 137, 211, 228, 146, 158, 178, 43, + 80, 7, 172, 3, 218, 129, 179, 43, 241, 9, 140, 0, 155, 129, 87, + 128, 193, 146, 15, 47, 248, 178, 100, 0, 237, 64, 39, 112, 14, 96, + 174, 20, 217, 153, 228, 162, 0, 50, 25, 2, 58, 128, 45, 83, 1, + 228, 53, 121, 10, 170, 129, 183, 128, 213, 126, 47, 215, 49, 224, 39, + 224, 19, 160, 7, 56, 2, 204, 0, 154, 128, 27, 128, 229, 110, 215, + 120, 64, 181, 132, 149, 184, 30, 120, 4, 248, 183, 24, 40, 185, 248, + 243, 147, 86, 160, 154, 144, 195, 91, 252, 43, 5, 140, 2, 31, 2, + 27, 129, 195, 83, 125, 141, 75, 19, 240, 2, 129, 47, 179, 61, 144, + 4, 216, 7, 172, 44, 14, 34, 105, 232, 62, 41, 128, 109, 192, 189, + 14, 126, 2, 24, 0, 30, 3, 246, 150, 1, 46, 150, 54, 15, 184, + 1, 200, 251, 216, 123, 192, 253, 19, 2, 152, 183, 119, 66, 0, 237, + 192, 27, 110, 159, 0, 250, 128, 187, 128, 67, 167, 8, 158, 201, 98, + 224, 3, 160, 209, 131, 72, 128, 53, 68, 156, 200, 153, 192, 181, 206, + 68, 167, 219, 50, 49, 96, 226, 78, 19, 135, 162, 103, 138, 117, 169, + 137, 46, 19, 163, 38, 82, 19, 63, 152, 120, 220, 196, 12, 159, 63, + 104, 98, 149, 137, 99, 238, 211, 28, 163, 46, 243, 145, 147, 192, 117, + 157, 68, 173, 219, 195, 18, 143, 74, 28, 137, 230, 139, 181, 77, 162, + 71, 98, 165, 68, 141, 68, 78, 98, 145, 196, 107, 18, 59, 252, 30, + 137, 3, 18, 207, 72, 140, 249, 125, 173, 99, 33, 65, 114, 209, 110, + 131, 192, 218, 65, 2, 105, 4, 108, 245, 165, 74, 125, 165, 206, 5, + 214, 2, 151, 3, 7, 128, 119, 128, 95, 253, 189, 169, 228, 105, 224, + 85, 183, 171, 128, 29, 192, 29, 4, 82, 142, 18, 246, 151, 177, 164, + 126, 151, 1, 220, 3, 188, 79, 32, 222, 40, 97, 167, 235, 243, 151, + 207, 2, 190, 5, 154, 35, 231, 223, 0, 45, 101, 242, 127, 12, 152, + 75, 97, 191, 88, 12, 124, 237, 254, 18, 96, 21, 176, 35, 227, 192, + 50, 207, 15, 38, 126, 49, 113, 56, 202, 243, 221, 38, 154, 139, 114, + 223, 82, 130, 23, 153, 214, 155, 88, 20, 221, 255, 104, 226, 104, 116, + 223, 106, 42, 144, 112, 169, 137, 196, 131, 248, 56, 10, 166, 222, 196, + 141, 21, 128, 77, 165, 223, 155, 120, 42, 34, 246, 158, 200, 247, 18, + 19, 228, 21, 178, 60, 223, 151, 41, 1, 190, 112, 251, 60, 224, 171, + 104, 238, 116, 36, 33, 240, 224, 32, 240, 25, 176, 31, 120, 194, 199, + 27, 161, 112, 26, 102, 167, 154, 1, 127, 186, 253, 98, 9, 240, 81, + 2, 97, 43, 149, 231, 61, 128, 129, 104, 108, 78, 28, 64, 44, 25, + 105, 218, 74, 56, 156, 13, 252, 76, 248, 43, 42, 145, 140, 176, 249, + 226, 137, 188, 133, 20, 12, 123, 68, 9, 225, 171, 127, 7, 46, 40, + 227, 180, 82, 112, 128, 153, 126, 189, 148, 194, 105, 57, 12, 5, 18, + 246, 71, 68, 185, 214, 237, 191, 166, 65, 190, 98, 237, 243, 235, 205, + 209, 88, 127, 188, 19, 246, 250, 53, 39, 177, 194, 237, 157, 37, 118, + 193, 83, 213, 183, 37, 102, 73, 92, 39, 145, 196, 152, 57, 75, 193, + 82, 246, 249, 21, 75, 89, 104, 41, 205, 150, 178, 222, 82, 250, 163, + 241, 211, 213, 253, 150, 178, 201, 82, 174, 180, 148, 185, 150, 146, 248, + 120, 183, 165, 133, 20, 116, 153, 24, 113, 123, 150, 137, 103, 77, 28, + 55, 113, 141, 137, 173, 38, 134, 60, 61, 167, 178, 236, 3, 38, 54, + 152, 184, 213, 255, 253, 39, 221, 55, 142, 213, 101, 42, 252, 5, 99, + 132, 202, 101, 45, 97, 175, 94, 14, 172, 0, 118, 1, 15, 185, 78, + 71, 86, 19, 138, 217, 196, 117, 179, 99, 146, 204, 126, 125, 188, 30, + 168, 35, 236, 255, 181, 132, 3, 233, 15, 224, 54, 202, 87, 64, 229, + 164, 5, 216, 9, 92, 232, 224, 67, 192, 66, 188, 88, 205, 69, 185, + 26, 180, 148, 14, 207, 81, 206, 82, 230, 89, 202, 110, 75, 185, 108, + 26, 249, 191, 202, 82, 222, 181, 148, 243, 163, 220, 119, 56, 22, 150, + 78, 172, 7, 144, 216, 34, 177, 205, 237, 188, 196, 2, 137, 61, 18, + 15, 158, 6, 243, 31, 144, 216, 37, 209, 224, 190, 18, 137, 237, 142, + 49, 254, 92, 50, 115, 211, 164, 69, 233, 71, 64, 43, 140, 23, 165, + 127, 19, 26, 142, 231, 8, 117, 192, 84, 82, 69, 56, 118, 215, 3, + 55, 17, 54, 160, 172, 40, 253, 148, 80, 168, 78, 44, 74, 171, 59, + 39, 237, 11, 170, 129, 55, 129, 251, 40, 108, 205, 2, 254, 1, 126, + 243, 96, 122, 129, 163, 62, 223, 0, 92, 237, 160, 141, 17, 112, 38, + 219, 129, 135, 139, 193, 1, 146, 170, 151, 43, 106, 76, 106, 139, 198, + 229, 192, 73, 20, 96, 12, 152, 177, 253, 56, 101, 26, 147, 36, 191, + 177, 226, 214, 108, 13, 133, 214, 172, 220, 75, 35, 14, 90, 190, 53, + 203, 189, 84, 113, 119, 156, 53, 167, 173, 192, 21, 252, 95, 205, 105, + 178, 225, 204, 182, 231, 255, 1, 200, 91, 112, 221, 160, 249, 68, 42, + 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130 +}; diff --git a/backends/networking/curl/cloudicon_disabled_data.h b/backends/networking/curl/cloudicon_disabled_data.h new file mode 100644 index 0000000000..4340a8a37c --- /dev/null +++ b/backends/networking/curl/cloudicon_disabled_data.h @@ -0,0 +1,117 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +// This is a PNG file dumped into array. +// $ recode data..d1 <dists/cloudicon_disabled.png >cloudicon_disabled_data.h +// The tool is from https://github.com/pinard/Recode + +byte cloudicon_disabled_data[] = { + 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, + 82, 0, 0, 0, 32, 0, 0, 0, 32, 8, 6, 0, 0, 0, 115, + 122, 122, 244, 0, 0, 0, 4, 115, 66, 73, 84, 8, 8, 8, 8, + 124, 8, 100, 136, 0, 0, 0, 9, 112, 72, 89, 115, 0, 0, 11, + 18, 0, 0, 11, 18, 1, 210, 221, 126, 252, 0, 0, 0, 22, 116, + 69, 88, 116, 67, 114, 101, 97, 116, 105, 111, 110, 32, 84, 105, 109, + 101, 0, 48, 54, 47, 48, 51, 47, 49, 54, 159, 192, 233, 192, 0, + 0, 0, 28, 116, 69, 88, 116, 83, 111, 102, 116, 119, 97, 114, 101, + 0, 65, 100, 111, 98, 101, 32, 70, 105, 114, 101, 119, 111, 114, 107, + 115, 32, 67, 83, 54, 232, 188, 178, 140, 0, 0, 4, 139, 73, 68, + 65, 84, 88, 133, 197, 215, 91, 168, 86, 69, 20, 7, 240, 223, 254, + 244, 120, 236, 120, 57, 26, 94, 10, 31, 34, 35, 11, 36, 203, 74, + 212, 160, 204, 212, 110, 166, 248, 208, 205, 172, 151, 144, 136, 158, 82, + 80, 168, 32, 233, 165, 160, 32, 130, 236, 165, 34, 204, 74, 18, 52, + 36, 169, 232, 34, 24, 221, 243, 218, 133, 74, 77, 195, 44, 200, 91, + 122, 188, 156, 172, 102, 159, 221, 195, 204, 246, 219, 126, 231, 226, 17, + 138, 22, 12, 123, 246, 236, 153, 245, 95, 107, 205, 127, 205, 172, 157, + 21, 254, 95, 233, 27, 122, 63, 183, 5, 179, 48, 13, 87, 98, 52, + 6, 167, 111, 71, 176, 11, 27, 177, 14, 107, 209, 222, 27, 165, 217, + 137, 211, 207, 25, 137, 197, 152, 143, 65, 189, 209, 137, 163, 120, 1, + 79, 98, 111, 143, 147, 143, 247, 172, 108, 62, 158, 194, 16, 20, 169, + 105, 232, 151, 82, 171, 24, 80, 74, 27, 22, 225, 197, 110, 13, 104, + 235, 122, 188, 31, 94, 194, 93, 21, 192, 14, 49, 172, 223, 226, 109, + 124, 130, 29, 232, 131, 49, 184, 22, 55, 166, 126, 75, 131, 65, 175, + 225, 94, 252, 213, 201, 128, 131, 93, 131, 191, 137, 27, 42, 192, 199, + 241, 6, 158, 192, 246, 238, 188, 73, 50, 6, 143, 98, 78, 50, 164, + 140, 200, 123, 34, 135, 78, 49, 34, 219, 215, 89, 193, 171, 152, 151, + 192, 3, 246, 224, 1, 188, 123, 26, 224, 170, 92, 134, 143, 49, 160, + 50, 86, 96, 5, 238, 174, 78, 108, 204, 130, 249, 98, 216, 75, 240, + 109, 184, 13, 63, 156, 1, 248, 68, 44, 21, 189, 47, 165, 67, 140, + 196, 60, 172, 87, 225, 68, 182, 167, 62, 105, 100, 2, 106, 77, 11, + 118, 139, 123, 186, 163, 59, 164, 53, 76, 16, 195, 125, 29, 250, 15, + 227, 167, 73, 12, 237, 27, 73, 91, 75, 142, 28, 21, 185, 51, 60, + 141, 181, 225, 98, 41, 59, 106, 33, 185, 26, 88, 28, 104, 77, 253, + 163, 129, 251, 3, 59, 42, 223, 79, 105, 171, 184, 53, 240, 73, 224, + 150, 64, 203, 32, 106, 99, 185, 160, 224, 236, 16, 245, 118, 4, 54, + 5, 166, 4, 22, 6, 218, 211, 218, 214, 132, 37, 32, 75, 238, 181, + 224, 55, 12, 76, 222, 191, 140, 251, 144, 39, 79, 135, 98, 1, 198, + 98, 11, 150, 227, 251, 50, 204, 67, 113, 121, 90, 156, 57, 185, 127, + 135, 154, 184, 9, 95, 160, 9, 43, 49, 59, 69, 225, 24, 206, 65, + 123, 150, 54, 247, 14, 188, 158, 214, 30, 23, 79, 186, 109, 9, 252, + 44, 124, 142, 113, 149, 232, 127, 134, 201, 196, 88, 79, 66, 115, 5, + 252, 24, 182, 114, 224, 32, 35, 230, 212, 207, 139, 75, 241, 169, 168, + 47, 195, 157, 88, 89, 110, 193, 180, 64, 145, 250, 223, 5, 182, 87, + 194, 125, 123, 96, 92, 195, 22, 76, 14, 201, 227, 177, 226, 65, 144, + 227, 111, 28, 198, 151, 113, 131, 135, 5, 46, 169, 172, 249, 38, 176, + 187, 242, 62, 35, 160, 150, 199, 197, 19, 114, 178, 156, 34, 231, 173, + 244, 180, 154, 115, 115, 166, 166, 57, 167, 180, 193, 34, 3, 7, 165, + 61, 11, 137, 105, 27, 112, 160, 62, 111, 235, 106, 22, 166, 126, 71, + 206, 59, 165, 238, 156, 43, 114, 100, 155, 98, 120, 218, 146, 206, 2, + 83, 241, 225, 26, 134, 165, 253, 27, 173, 65, 134, 138, 137, 222, 154, + 222, 139, 228, 249, 87, 233, 217, 133, 76, 159, 19, 47, 169, 89, 226, + 129, 214, 71, 188, 192, 134, 148, 231, 64, 121, 171, 21, 248, 85, 244, + 232, 177, 30, 192, 59, 90, 82, 6, 193, 9, 108, 234, 30, 28, 30, + 9, 209, 128, 74, 214, 71, 204, 190, 121, 231, 201, 5, 228, 220, 218, + 248, 97, 136, 200, 246, 102, 106, 29, 234, 73, 190, 5, 135, 186, 7, + 135, 201, 9, 167, 111, 227, 135, 50, 2, 71, 146, 69, 153, 232, 245, + 206, 192, 136, 234, 196, 86, 49, 13, 154, 162, 113, 39, 211, 101, 51, + 126, 239, 25, 28, 154, 19, 206, 133, 234, 119, 195, 17, 234, 36, 220, + 85, 33, 202, 213, 169, 191, 175, 36, 92, 139, 152, 106, 3, 212, 9, + 119, 76, 100, 251, 126, 157, 9, 218, 69, 219, 150, 158, 211, 42, 99, + 187, 114, 245, 147, 112, 83, 122, 214, 2, 51, 83, 127, 85, 16, 89, + 62, 94, 60, 61, 202, 20, 58, 36, 38, 244, 126, 93, 159, 146, 93, + 180, 101, 129, 254, 129, 107, 2, 89, 26, 219, 24, 42, 6, 188, 95, + 153, 124, 81, 202, 251, 37, 3, 249, 101, 188, 120, 114, 148, 223, 219, + 197, 212, 56, 208, 123, 240, 245, 129, 167, 3, 19, 3, 163, 42, 6, + 172, 11, 234, 36, 92, 43, 242, 105, 32, 250, 227, 161, 89, 44, 45, + 98, 196, 139, 14, 178, 146, 112, 27, 210, 179, 23, 178, 7, 203, 240, + 248, 156, 152, 251, 15, 38, 221, 146, 138, 181, 212, 73, 216, 46, 214, + 112, 11, 82, 180, 103, 138, 71, 237, 40, 145, 52, 29, 216, 221, 194, + 220, 41, 49, 0, 103, 36, 129, 185, 152, 158, 116, 101, 9, 171, 29, + 178, 85, 245, 121, 213, 235, 184, 90, 215, 229, 248, 89, 172, 112, 190, + 62, 83, 112, 209, 145, 85, 226, 229, 147, 105, 188, 142, 43, 172, 220, + 155, 179, 40, 29, 201, 85, 6, 239, 204, 153, 155, 243, 117, 47, 216, + 222, 216, 38, 229, 188, 146, 51, 188, 162, 119, 81, 194, 82, 205, 130, + 178, 189, 24, 120, 173, 97, 108, 80, 34, 102, 111, 73, 87, 182, 123, + 2, 107, 2, 231, 133, 184, 213, 89, 96, 69, 194, 56, 57, 47, 91, + 222, 57, 100, 253, 68, 130, 92, 175, 94, 148, 254, 129, 15, 240, 176, + 88, 7, 116, 39, 77, 226, 181, 187, 68, 172, 146, 154, 69, 78, 101, + 98, 77, 57, 91, 99, 81, 250, 82, 215, 138, 202, 178, 188, 44, 78, + 37, 67, 254, 196, 143, 201, 152, 141, 98, 217, 86, 224, 60, 92, 149, + 64, 207, 175, 0, 151, 178, 66, 119, 101, 249, 243, 61, 184, 163, 254, + 99, 210, 218, 48, 94, 94, 5, 101, 13, 162, 1, 176, 100, 251, 97, + 167, 249, 49, 233, 115, 179, 250, 111, 78, 23, 109, 115, 193, 178, 130, + 62, 5, 151, 20, 52, 23, 241, 76, 200, 10, 106, 149, 103, 173, 50, + 158, 21, 28, 45, 120, 174, 96, 110, 193, 71, 61, 232, 151, 61, 219, + 115, 4, 170, 82, 254, 156, 206, 16, 47, 197, 127, 231, 231, 244, 153, + 222, 27, 240, 159, 200, 63, 153, 185, 24, 191, 162, 246, 71, 153, 0, + 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130 +}; diff --git a/backends/networking/curl/connectionmanager.cpp b/backends/networking/curl/connectionmanager.cpp new file mode 100644 index 0000000000..f3dc91ad60 --- /dev/null +++ b/backends/networking/curl/connectionmanager.cpp @@ -0,0 +1,204 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#define FORBIDDEN_SYMBOL_ALLOW_ALL + +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/debug.h" +#include "common/system.h" +#include "common/timer.h" +#include <curl/curl.h> + +namespace Common { + +DECLARE_SINGLETON(Networking::ConnectionManager); + +} + +namespace Networking { + +ConnectionManager::ConnectionManager(): _multi(0), _timerStarted(false), _frame(0) { + curl_global_init(CURL_GLOBAL_ALL); + _multi = curl_multi_init(); +} + +ConnectionManager::~ConnectionManager() { + stopTimer(); + + //terminate all requests + _handleMutex.lock(); + for (Common::Array<RequestWithCallback>::iterator i = _requests.begin(); i != _requests.end(); ++i) { + Request *request = i->request; + RequestCallback callback = i->onDeleteCallback; + if (request) + request->finish(); + delete request; + if (callback) + (*callback)(request); + } + _requests.clear(); + + //cleanup + curl_multi_cleanup(_multi); + curl_global_cleanup(); + _multi = nullptr; + _handleMutex.unlock(); +} + +void ConnectionManager::registerEasyHandle(CURL *easy) const { + curl_multi_add_handle(_multi, easy); +} + +Request *ConnectionManager::addRequest(Request *request, RequestCallback callback) { + _addedRequestsMutex.lock(); + _addedRequests.push_back(RequestWithCallback(request, callback)); + if (!_timerStarted) + startTimer(); + _addedRequestsMutex.unlock(); + return request; +} + +void ConnectionManager::showCloudDisabledIcon() { + _icon.showDisabled(); + startTimer(); +} + +Common::String ConnectionManager::urlEncode(Common::String s) const { + if (!_multi) + return ""; + char *output = curl_easy_escape(_multi, s.c_str(), s.size()); + if (output) { + Common::String result = output; + curl_free(output); + return result; + } + return ""; +} + +uint32 ConnectionManager::getCloudRequestsPeriodInMicroseconds() { + return TIMER_INTERVAL * CLOUD_PERIOD; +} + +//private goes here: + +void connectionsThread(void *ignored) { + ConnMan.handle(); +} + +void ConnectionManager::startTimer(int interval) { + Common::TimerManager *manager = g_system->getTimerManager(); + if (manager->installTimerProc(connectionsThread, interval, 0, "Networking::ConnectionManager's Timer")) { + _timerStarted = true; + } else { + warning("Failed to install Networking::ConnectionManager's timer"); + } +} + +void ConnectionManager::stopTimer() { + debug(9, "timer stopped"); + Common::TimerManager *manager = g_system->getTimerManager(); + manager->removeTimerProc(connectionsThread); + _timerStarted = false; +} + +bool ConnectionManager::hasAddedRequests() { + _addedRequestsMutex.lock(); + bool hasNewRequests = !_addedRequests.empty(); + _addedRequestsMutex.unlock(); + return hasNewRequests; +} + +void ConnectionManager::handle() { + //lock mutex here (in case another handle() would be called before this one ends) + _handleMutex.lock(); + ++_frame; + if (_frame % CLOUD_PERIOD == 0) + interateRequests(); + if (_frame % CURL_PERIOD == 0) + processTransfers(); + + if (_icon.draw() && _requests.empty() && !hasAddedRequests()) + stopTimer(); + _handleMutex.unlock(); +} + +void ConnectionManager::interateRequests() { + //add new requests + _addedRequestsMutex.lock(); + for (Common::Array<RequestWithCallback>::iterator i = _addedRequests.begin(); i != _addedRequests.end(); ++i) { + _requests.push_back(*i); + } + _addedRequests.clear(); + _addedRequestsMutex.unlock(); + + //call handle() of all running requests (so they can do their work) + debug(9, "handling %d request(s)", _requests.size()); + for (Common::Array<RequestWithCallback>::iterator i = _requests.begin(); i != _requests.end();) { + Request *request = i->request; + if (request) { + if (request->state() == PROCESSING) + request->handle(); + else if (request->state() == RETRY) + request->handleRetry(); + } + + if (!request || request->state() == FINISHED) { + delete (i->request); + if (i->onDeleteCallback) + (*i->onDeleteCallback)(i->request); //that's not a mistake (we're passing an address and that method knows there is no object anymore) + _requests.erase(i); + continue; + } + + ++i; + } +} + +void ConnectionManager::processTransfers() { + if (!_multi) return; + + //check libcurl's transfers and notify requests of messages from queue (transfer completion or failure) + int transfersRunning; + curl_multi_perform(_multi, &transfersRunning); + + int messagesInQueue; + CURLMsg *curlMsg; + while ((curlMsg = curl_multi_info_read(_multi, &messagesInQueue))) { + CURL *easyHandle = curlMsg->easy_handle; + + NetworkReadStream *stream; + curl_easy_getinfo(easyHandle, CURLINFO_PRIVATE, &stream); + if (stream) + stream->finished(); + + if (curlMsg->msg == CURLMSG_DONE) { + debug(9, "ConnectionManager: SUCCESS (%d - %s)", curlMsg->data.result, curl_easy_strerror(curlMsg->data.result)); + } else { + warning("ConnectionManager: FAILURE (CURLMsg (%d))", curlMsg->msg); + } + + curl_multi_remove_handle(_multi, easyHandle); + } +} + +} // End of namespace Cloud diff --git a/backends/networking/curl/connectionmanager.h b/backends/networking/curl/connectionmanager.h new file mode 100644 index 0000000000..826bef6d36 --- /dev/null +++ b/backends/networking/curl/connectionmanager.h @@ -0,0 +1,132 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_NETWORKING_CURL_CONNECTIONMANAGER_H +#define BACKENDS_NETWORKING_CURL_CONNECTIONMANAGER_H + +#include "backends/networking/curl/cloudicon.h" +#include "backends/networking/curl/request.h" +#include "common/str.h" +#include "common/singleton.h" +#include "common/hashmap.h" +#include "common/mutex.h" + +typedef void CURL; +typedef void CURLM; +struct curl_slist; + +namespace Networking { + +class NetworkReadStream; + +class ConnectionManager : public Common::Singleton<ConnectionManager> { + static const uint32 FRAMES_PER_SECOND = 20; + static const uint32 TIMER_INTERVAL = 1000000 / FRAMES_PER_SECOND; + static const uint32 CLOUD_PERIOD = 20; //every 20th frame + static const uint32 CURL_PERIOD = 1; //every frame + + friend void connectionsThread(void *); //calls handle() + + typedef Common::BaseCallback<Request *> *RequestCallback; + + /** + * RequestWithCallback is used by ConnectionManager to + * storage the Request and a callback which should be + * called on Request delete. + * + * Usually one won't need to pass such callback, but + * in some cases you'd like to know whether Request is + * still running. + * + * For example, Cloud::Storage is keeping track of how + * many Requests are running, and thus it needs to know + * that Request was destroyed to decrease its counter. + * + * onDeleteCallback is called with *invalid* pointer. + * ConnectionManager deletes Request first and then passes + * the pointer to the callback. One may use the address + * to find it in own HashMap or Array and remove it. + * So, again, this pointer is for information only. One + * cannot use it. + */ + struct RequestWithCallback { + Request *request; + RequestCallback onDeleteCallback; + + RequestWithCallback(Request *rq = nullptr, RequestCallback cb = nullptr): request(rq), onDeleteCallback(cb) {} + }; + + CURLM *_multi; + bool _timerStarted; + Common::Array<RequestWithCallback> _requests, _addedRequests; + Common::Mutex _handleMutex, _addedRequestsMutex; + CloudIcon _icon; + uint32 _frame; + + void startTimer(int interval = TIMER_INTERVAL); + void stopTimer(); + void handle(); + void interateRequests(); + void processTransfers(); + bool hasAddedRequests(); + +public: + ConnectionManager(); + virtual ~ConnectionManager(); + + /** + * All libcurl transfers are going through this ConnectionManager. + * So, if you want to start any libcurl transfer, you must create + * an easy handle and register it using this method. + */ + void registerEasyHandle(CURL *easy) const; + + /** + * Use this method to add new Request into manager's queue. + * Manager will periodically call handle() method of these + * Requests until they set their state to FINISHED. + * + * If Request's state is RETRY, handleRetry() is called instead. + * + * The passed callback would be called after Request is deleted. + * + * @note This method starts the timer if it's not started yet. + * + * @return the same Request pointer, just as a shortcut + */ + Request *addRequest(Request *request, RequestCallback callback = nullptr); + + /** Shows a "cloud disabled" icon for a three seconds. */ + void showCloudDisabledIcon(); + + /** Return URL-encoded version of given string. */ + Common::String urlEncode(Common::String s) const; + + static uint32 getCloudRequestsPeriodInMicroseconds(); +}; + +/** Shortcut for accessing the connection manager. */ +#define ConnMan Networking::ConnectionManager::instance() + +} // End of namespace Networking + +#endif diff --git a/backends/networking/curl/curljsonrequest.cpp b/backends/networking/curl/curljsonrequest.cpp new file mode 100644 index 0000000000..1899cbd913 --- /dev/null +++ b/backends/networking/curl/curljsonrequest.cpp @@ -0,0 +1,215 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#define FORBIDDEN_SYMBOL_ALLOW_ALL + +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/debug.h" +#include "common/json.h" +#include <curl/curl.h> + +namespace Networking { + +CurlJsonRequest::CurlJsonRequest(JsonCallback cb, ErrorCallback ecb, Common::String url) : + CurlRequest(nullptr, ecb, url), _jsonCallback(cb), _contentsStream(DisposeAfterUse::YES), + _buffer(new byte[CURL_JSON_REQUEST_BUFFER_SIZE]) {} + +CurlJsonRequest::~CurlJsonRequest() { + delete _jsonCallback; + delete[] _buffer; +} + +char *CurlJsonRequest::getPreparedContents() { + //write one more byte in the end + byte zero[1] = {0}; + _contentsStream.write(zero, 1); + + //replace all "bad" bytes with '.' character + byte *result = _contentsStream.getData(); + uint32 size = _contentsStream.size(); + for (uint32 i = 0; i < size; ++i) { + if (result[i] == '\n') + result[i] = ' '; //yeah, kinda stupid + else if (result[i] < 0x20 || result[i] > 0x7f) + result[i] = '.'; + } + + //make it zero-terminated string + result[size - 1] = '\0'; + + return (char *)result; +} + +void CurlJsonRequest::handle() { + if (!_stream) _stream = makeStream(); + + if (_stream) { + uint32 readBytes = _stream->read(_buffer, CURL_JSON_REQUEST_BUFFER_SIZE); + if (readBytes != 0) + if (_contentsStream.write(_buffer, readBytes) != readBytes) + warning("CurlJsonRequest: unable to write all the bytes into MemoryWriteStreamDynamic"); + + if (_stream->eos()) { + char *contents = getPreparedContents(); + Common::JSONValue *json = Common::JSON::parse(contents); + if (json) { + finishJson(json); //it's JSON even if's not 200 OK? That's fine!.. + } else { + if (_stream->httpResponseCode() == 200) //no JSON, but 200 OK? That's fine!.. + finishJson(nullptr); + else + finishError(ErrorResponse(this, false, true, contents, _stream->httpResponseCode())); + } + } + } +} + +void CurlJsonRequest::restart() { + if (_stream) + delete _stream; + _stream = nullptr; + _contentsStream = Common::MemoryWriteStreamDynamic(DisposeAfterUse::YES); + //with no stream available next handle() will create another one +} + +void CurlJsonRequest::finishJson(Common::JSONValue *json) { + Request::finishSuccess(); + if (_jsonCallback) + (*_jsonCallback)(JsonResponse(this, json)); //potential memory leak, free it in your callbacks! + else + delete json; +} + +bool CurlJsonRequest::jsonIsObject(Common::JSONValue *item, const char *warningPrefix) { + if (item == nullptr) { + warning("%s: passed item is NULL", warningPrefix); + return false; + } + + if (item->isObject()) return true; + + warning("%s: passed item is not an object", warningPrefix); + debug(9, "%s", item->stringify(true).c_str()); + return false; +} + +bool CurlJsonRequest::jsonContainsObject(Common::JSONObject &item, const char *key, const char *warningPrefix, bool isOptional) { + if (!item.contains(key)) { + if (isOptional) { + return true; + } + + warning("%s: passed item misses the \"%s\" attribute", warningPrefix, key); + return false; + } + + if (item.getVal(key)->isObject()) return true; + + warning("%s: passed item's \"%s\" attribute is not an object", warningPrefix, key); + debug(9, "%s", item.getVal(key)->stringify(true).c_str()); + return false; +} + +bool CurlJsonRequest::jsonContainsString(Common::JSONObject &item, const char *key, const char *warningPrefix, bool isOptional) { + if (!item.contains(key)) { + if (isOptional) { + return true; + } + + warning("%s: passed item misses the \"%s\" attribute", warningPrefix, key); + return false; + } + + if (item.getVal(key)->isString()) return true; + + warning("%s: passed item's \"%s\" attribute is not a string", warningPrefix, key); + debug(9, "%s", item.getVal(key)->stringify(true).c_str()); + return false; +} + +bool CurlJsonRequest::jsonContainsIntegerNumber(Common::JSONObject &item, const char *key, const char *warningPrefix, bool isOptional) { + if (!item.contains(key)) { + if (isOptional) { + return true; + } + + warning("%s: passed item misses the \"%s\" attribute", warningPrefix, key); + return false; + } + + if (item.getVal(key)->isIntegerNumber()) return true; + + warning("%s: passed item's \"%s\" attribute is not an integer", warningPrefix, key); + debug(9, "%s", item.getVal(key)->stringify(true).c_str()); + return false; +} + +bool CurlJsonRequest::jsonContainsArray(Common::JSONObject &item, const char *key, const char *warningPrefix, bool isOptional) { + if (!item.contains(key)) { + if (isOptional) { + return true; + } + + warning("%s: passed item misses the \"%s\" attribute", warningPrefix, key); + return false; + } + + if (item.getVal(key)->isArray()) return true; + + warning("%s: passed item's \"%s\" attribute is not an array", warningPrefix, key); + debug(9, "%s", item.getVal(key)->stringify(true).c_str()); + return false; +} + +bool CurlJsonRequest::jsonContainsStringOrIntegerNumber(Common::JSONObject &item, const char *key, const char *warningPrefix, bool isOptional) { + if (!item.contains(key)) { + if (isOptional) { + return true; + } + + warning("%s: passed item misses the \"%s\" attribute", warningPrefix, key); + return false; + } + + if (item.getVal(key)->isString() || item.getVal(key)->isIntegerNumber()) return true; + + warning("%s: passed item's \"%s\" attribute is neither a string or an integer", warningPrefix, key); + debug(9, "%s", item.getVal(key)->stringify(true).c_str()); + return false; +} + +bool CurlJsonRequest::jsonContainsAttribute(Common::JSONObject &item, const char *key, const char *warningPrefix, bool isOptional) { + if (!item.contains(key)) { + if (isOptional) { + return true; + } + + warning("%s: passed item misses the \"%s\" attribute", warningPrefix, key); + return false; + } + + return true; +} + +} // End of namespace Networking diff --git a/backends/networking/curl/curljsonrequest.h b/backends/networking/curl/curljsonrequest.h new file mode 100644 index 0000000000..edd523015a --- /dev/null +++ b/backends/networking/curl/curljsonrequest.h @@ -0,0 +1,67 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_NETWORKING_CURL_CURLJSONREQUEST_H +#define BACKENDS_NETWORKING_CURL_CURLJSONREQUEST_H + +#include "backends/networking/curl/curlrequest.h" +#include "common/memstream.h" +#include "common/json.h" + +namespace Networking { + +typedef Response<Common::JSONValue *> JsonResponse; +typedef Common::BaseCallback<JsonResponse> *JsonCallback; + +#define CURL_JSON_REQUEST_BUFFER_SIZE 512 * 1024 + +class CurlJsonRequest: public CurlRequest { +protected: + JsonCallback _jsonCallback; + Common::MemoryWriteStreamDynamic _contentsStream; + byte *_buffer; + + /** Prepares raw bytes from _contentsStream to be parsed with Common::JSON::parse(). */ + char *getPreparedContents(); + + /** Sets FINISHED state and passes the JSONValue * into user's callback in JsonResponse. */ + virtual void finishJson(Common::JSONValue *json); + +public: + CurlJsonRequest(JsonCallback cb, ErrorCallback ecb, Common::String url); + virtual ~CurlJsonRequest(); + + virtual void handle(); + virtual void restart(); + + static bool jsonIsObject(Common::JSONValue *item, const char *warningPrefix); + static bool jsonContainsObject(Common::JSONObject &item, const char *key, const char *warningPrefix, bool isOptional = false); + static bool jsonContainsString(Common::JSONObject &item, const char *key, const char *warningPrefix, bool isOptional = false); + static bool jsonContainsIntegerNumber(Common::JSONObject &item, const char *key, const char *warningPrefix, bool isOptional = false); + static bool jsonContainsArray(Common::JSONObject &item, const char *key, const char *warningPrefix, bool isOptional = false); + static bool jsonContainsStringOrIntegerNumber(Common::JSONObject &item, const char *key, const char *warningPrefix, bool isOptional = false); + static bool jsonContainsAttribute(Common::JSONObject &item, const char *key, const char *warningPrefix, bool isOptional = false); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/curl/curlrequest.cpp b/backends/networking/curl/curlrequest.cpp new file mode 100644 index 0000000000..64fa347023 --- /dev/null +++ b/backends/networking/curl/curlrequest.cpp @@ -0,0 +1,162 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#define FORBIDDEN_SYMBOL_ALLOW_ALL + +#include "backends/networking/curl/curlrequest.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/textconsole.h" +#include <curl/curl.h> + +namespace Networking { + +CurlRequest::CurlRequest(DataCallback cb, ErrorCallback ecb, Common::String url): + Request(cb, ecb), _url(url), _stream(nullptr), _headersList(nullptr), _bytesBuffer(nullptr), + _bytesBufferSize(0), _uploading(false), _usingPatch(false) {} + +CurlRequest::~CurlRequest() { + delete _stream; + delete _bytesBuffer; +} + +NetworkReadStream *CurlRequest::makeStream() { + if (_bytesBuffer) + return new NetworkReadStream(_url.c_str(), _headersList, _bytesBuffer, _bytesBufferSize, _uploading, _usingPatch, true); + if (!_formFields.empty() || !_formFiles.empty()) + return new NetworkReadStream(_url.c_str(), _headersList, _formFields, _formFiles); + return new NetworkReadStream(_url.c_str(), _headersList, _postFields, _uploading, _usingPatch); +} + +void CurlRequest::handle() { + if (!_stream) _stream = makeStream(); + + if (_stream && _stream->eos()) { + if (_stream->httpResponseCode() != 200) { + warning("CurlRequest: HTTP response code is not 200 OK (it's %ld)", _stream->httpResponseCode()); + ErrorResponse error(this, false, true, "", _stream->httpResponseCode()); + finishError(error); + return; + } + + finishSuccess(); //note that this Request doesn't call its callback on success (that's because it has nothing to return) + } +} + +void CurlRequest::restart() { + if (_stream) + delete _stream; + _stream = nullptr; + //with no stream available next handle() will create another one +} + +Common::String CurlRequest::date() const { + if (_stream) { + Common::String headers = _stream->responseHeaders(); + const char *cstr = headers.c_str(); + const char *position = strstr(cstr, "Date: "); + + if (position) { + Common::String result = ""; + char c; + for (const char *i = position + 6; c = *i, c != 0; ++i) { + if (c == '\n' || c == '\r') + break; + result += c; + } + return result; + } + } + return ""; +} + +void CurlRequest::setHeaders(Common::Array<Common::String> &headers) { + curl_slist_free_all(_headersList); + _headersList = nullptr; + for (uint32 i = 0; i < headers.size(); ++i) + addHeader(headers[i]); +} + +void CurlRequest::addHeader(Common::String header) { + _headersList = curl_slist_append(_headersList, header.c_str()); +} + +void CurlRequest::addPostField(Common::String keyValuePair) { + if (_bytesBuffer) + warning("CurlRequest: added POST fields would be ignored, because there is buffer present"); + + if (!_formFields.empty() || !_formFiles.empty()) + warning("CurlRequest: added POST fields would be ignored, because there are form fields/files present"); + + if (_postFields == "") + _postFields = keyValuePair; + else + _postFields += "&" + keyValuePair; +} + +void CurlRequest::addFormField(Common::String name, Common::String value) { + if (_bytesBuffer) + warning("CurlRequest: added POST form fields would be ignored, because there is buffer present"); + + if (_formFields.contains(name)) + warning("CurlRequest: form field '%s' already had a value", name.c_str()); + + _formFields[name] = value; +} + +void CurlRequest::addFormFile(Common::String name, Common::String filename) { + if (_bytesBuffer) + warning("CurlRequest: added POST form files would be ignored, because there is buffer present"); + + if (_formFields.contains(name)) + warning("CurlRequest: form file field '%s' already had a value", name.c_str()); + + _formFiles[name] = filename; +} + +void CurlRequest::setBuffer(byte *buffer, uint32 size) { + if (_postFields != "") + warning("CurlRequest: added POST fields would be ignored, because buffer added"); + + if (_bytesBuffer) + delete _bytesBuffer; + + _bytesBuffer = buffer; + _bytesBufferSize = size; +} + +void CurlRequest::usePut() { _uploading = true; } + +void CurlRequest::usePatch() { _usingPatch = true; } + +NetworkReadStreamResponse CurlRequest::execute() { + if (!_stream) { + _stream = makeStream(); + ConnMan.addRequest(this); + } + + return NetworkReadStreamResponse(this, _stream); +} + +const NetworkReadStream *CurlRequest::getNetworkReadStream() const { return _stream; } + +} // End of namespace Networking diff --git a/backends/networking/curl/curlrequest.h b/backends/networking/curl/curlrequest.h new file mode 100644 index 0000000000..6ce94f8983 --- /dev/null +++ b/backends/networking/curl/curlrequest.h @@ -0,0 +1,100 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_NETWORKING_CURL_CURLREQUEST_H +#define BACKENDS_NETWORKING_CURL_CURLREQUEST_H + +#include "backends/networking/curl/request.h" +#include "common/str.h" +#include "common/array.h" +#include "common/hashmap.h" +#include "common/hash-str.h" + +struct curl_slist; + +namespace Networking { + +class NetworkReadStream; + +typedef Response<NetworkReadStream *> NetworkReadStreamResponse; +typedef Common::BaseCallback<NetworkReadStreamResponse> *NetworkReadStreamCallback; + +class CurlRequest: public Request { +protected: + Common::String _url; + NetworkReadStream *_stream; + curl_slist *_headersList; + Common::String _postFields; + Common::HashMap<Common::String, Common::String> _formFields; + Common::HashMap<Common::String, Common::String> _formFiles; + byte *_bytesBuffer; + uint32 _bytesBufferSize; + bool _uploading; //using PUT method + bool _usingPatch; //using PATCH method + + virtual NetworkReadStream *makeStream(); + +public: + CurlRequest(DataCallback cb, ErrorCallback ecb, Common::String url); + virtual ~CurlRequest(); + + virtual void handle(); + virtual void restart(); + virtual Common::String date() const; + + /** Replaces all headers with the passed array of headers. */ + virtual void setHeaders(Common::Array<Common::String> &headers); + + /** Adds a header into headers list. */ + virtual void addHeader(Common::String header); + + /** Adds a post field (key=value pair). */ + virtual void addPostField(Common::String field); + + /** Adds a form/multipart field (name, value). */ + virtual void addFormField(Common::String name, Common::String value); + + /** Adds a form/multipart file (field name, file name). */ + virtual void addFormFile(Common::String name, Common::String filename); + + /** Sets bytes buffer. */ + virtual void setBuffer(byte *buffer, uint32 size); + + /** Remembers to use PUT method when it would create NetworkReadStream. */ + virtual void usePut(); + + /** Remembers to use PATCH method when it would create NetworkReadStream. */ + virtual void usePatch(); + + /** + * Starts this Request with ConnMan. + * @return its NetworkReadStream in NetworkReadStreamResponse. + */ + virtual NetworkReadStreamResponse execute(); + + /** Returns Request's NetworkReadStream. */ + const NetworkReadStream *getNetworkReadStream() const; +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/curl/networkreadstream.cpp b/backends/networking/curl/networkreadstream.cpp new file mode 100644 index 0000000000..032aef87be --- /dev/null +++ b/backends/networking/curl/networkreadstream.cpp @@ -0,0 +1,257 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#define FORBIDDEN_SYMBOL_ALLOW_ALL + +#include "backends/networking/curl/networkreadstream.h" +#include "backends/networking/curl/connectionmanager.h" +#include "base/version.h" +#include <curl/curl.h> + +namespace Networking { + +static size_t curlDataCallback(char *d, size_t n, size_t l, void *p) { + NetworkReadStream *stream = (NetworkReadStream *)p; + if (stream) + return stream->write(d, n * l); + return 0; +} + +static size_t curlReadDataCallback(char *d, size_t n, size_t l, void *p) { + NetworkReadStream *stream = (NetworkReadStream *)p; + if (stream) + return stream->fillWithSendingContents(d, n * l); + return 0; +} + +static size_t curlHeadersCallback(char *d, size_t n, size_t l, void *p) { + NetworkReadStream *stream = (NetworkReadStream *)p; + if (stream) + return stream->addResponseHeaders(d, n * l); + return 0; +} + +static int curlProgressCallback(void *p, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) { + NetworkReadStream *stream = (NetworkReadStream *)p; + if (stream) + stream->setProgress(dlnow, dltotal); + return 0; +} + +static int curlProgressCallbackOlder(void *p, double dltotal, double dlnow, double ultotal, double ulnow) { + // for libcurl older than 7.32.0 (CURLOPT_PROGRESSFUNCTION) + return curlProgressCallback(p, (curl_off_t)dltotal, (curl_off_t)dlnow, (curl_off_t)ultotal, (curl_off_t)ulnow); +} + +void NetworkReadStream::init(const char *url, curl_slist *headersList, const byte *buffer, uint32 bufferSize, bool uploading, bool usingPatch, bool post) { + _eos = _requestComplete = false; + _sendingContentsBuffer = nullptr; + _sendingContentsSize = _sendingContentsPos = 0; + _progressDownloaded = _progressTotal = 0; + + _easy = curl_easy_init(); + curl_easy_setopt(_easy, CURLOPT_WRITEFUNCTION, curlDataCallback); + curl_easy_setopt(_easy, CURLOPT_WRITEDATA, this); //so callback can call us + curl_easy_setopt(_easy, CURLOPT_PRIVATE, this); //so ConnectionManager can call us when request is complete + curl_easy_setopt(_easy, CURLOPT_HEADER, 0L); + curl_easy_setopt(_easy, CURLOPT_HEADERDATA, this); + curl_easy_setopt(_easy, CURLOPT_HEADERFUNCTION, curlHeadersCallback); + curl_easy_setopt(_easy, CURLOPT_URL, url); + curl_easy_setopt(_easy, CURLOPT_VERBOSE, 0L); + curl_easy_setopt(_easy, CURLOPT_FOLLOWLOCATION, 1L); //probably it's OK to have it always on + curl_easy_setopt(_easy, CURLOPT_HTTPHEADER, headersList); + curl_easy_setopt(_easy, CURLOPT_USERAGENT, gScummVMFullVersion); + curl_easy_setopt(_easy, CURLOPT_NOPROGRESS, 0L); + curl_easy_setopt(_easy, CURLOPT_PROGRESSFUNCTION, curlProgressCallbackOlder); + curl_easy_setopt(_easy, CURLOPT_PROGRESSDATA, this); +#if LIBCURL_VERSION_NUM >= 0x072000 + // CURLOPT_XFERINFOFUNCTION introduced in libcurl 7.32.0 + // CURLOPT_PROGRESSFUNCTION is used as a backup plan in case older version is used + curl_easy_setopt(_easy, CURLOPT_XFERINFOFUNCTION, curlProgressCallback); + curl_easy_setopt(_easy, CURLOPT_XFERINFODATA, this); +#endif + if (uploading) { + curl_easy_setopt(_easy, CURLOPT_UPLOAD, 1L); + curl_easy_setopt(_easy, CURLOPT_READDATA, this); + curl_easy_setopt(_easy, CURLOPT_READFUNCTION, curlReadDataCallback); + _sendingContentsBuffer = buffer; + _sendingContentsSize = bufferSize; + } else if (usingPatch) { + curl_easy_setopt(_easy, CURLOPT_CUSTOMREQUEST, "PATCH"); + } else { + if (post || bufferSize != 0) { + curl_easy_setopt(_easy, CURLOPT_POSTFIELDSIZE, bufferSize); + curl_easy_setopt(_easy, CURLOPT_COPYPOSTFIELDS, buffer); + } + } + ConnMan.registerEasyHandle(_easy); +} + +void NetworkReadStream::init(const char *url, curl_slist *headersList, Common::HashMap<Common::String, Common::String> formFields, Common::HashMap<Common::String, Common::String> formFiles) { + _eos = _requestComplete = false; + _sendingContentsBuffer = nullptr; + _sendingContentsSize = _sendingContentsPos = 0; + _progressDownloaded = _progressTotal = 0; + + _easy = curl_easy_init(); + curl_easy_setopt(_easy, CURLOPT_WRITEFUNCTION, curlDataCallback); + curl_easy_setopt(_easy, CURLOPT_WRITEDATA, this); //so callback can call us + curl_easy_setopt(_easy, CURLOPT_PRIVATE, this); //so ConnectionManager can call us when request is complete + curl_easy_setopt(_easy, CURLOPT_HEADER, 0L); + curl_easy_setopt(_easy, CURLOPT_HEADERDATA, this); + curl_easy_setopt(_easy, CURLOPT_HEADERFUNCTION, curlHeadersCallback); + curl_easy_setopt(_easy, CURLOPT_URL, url); + curl_easy_setopt(_easy, CURLOPT_VERBOSE, 0L); + curl_easy_setopt(_easy, CURLOPT_FOLLOWLOCATION, 1L); //probably it's OK to have it always on + curl_easy_setopt(_easy, CURLOPT_HTTPHEADER, headersList); + curl_easy_setopt(_easy, CURLOPT_USERAGENT, gScummVMFullVersion); + curl_easy_setopt(_easy, CURLOPT_NOPROGRESS, 0L); + curl_easy_setopt(_easy, CURLOPT_PROGRESSFUNCTION, curlProgressCallbackOlder); + curl_easy_setopt(_easy, CURLOPT_PROGRESSDATA, this); +#if LIBCURL_VERSION_NUM >= 0x072000 + // CURLOPT_XFERINFOFUNCTION introduced in libcurl 7.32.0 + // CURLOPT_PROGRESSFUNCTION is used as a backup plan in case older version is used + curl_easy_setopt(_easy, CURLOPT_XFERINFOFUNCTION, curlProgressCallback); + curl_easy_setopt(_easy, CURLOPT_XFERINFODATA, this); +#endif + + // set POST multipart upload form fields/files + struct curl_httppost *formpost = nullptr; + struct curl_httppost *lastptr = nullptr; + + for (Common::HashMap<Common::String, Common::String>::iterator i = formFields.begin(); i != formFields.end(); ++i) { + CURLFORMcode code = curl_formadd( + &formpost, + &lastptr, + CURLFORM_COPYNAME, i->_key.c_str(), + CURLFORM_COPYCONTENTS, i->_value.c_str(), + CURLFORM_END + ); + + if (code != CURL_FORMADD_OK) + warning("NetworkReadStream: field curl_formadd('%s') failed", i->_key.c_str()); + } + + for (Common::HashMap<Common::String, Common::String>::iterator i = formFiles.begin(); i != formFiles.end(); ++i) { + CURLFORMcode code = curl_formadd( + &formpost, + &lastptr, + CURLFORM_COPYNAME, i->_key.c_str(), + CURLFORM_FILE, i->_value.c_str(), + CURLFORM_END + ); + + if (code != CURL_FORMADD_OK) + warning("NetworkReadStream: file curl_formadd('%s') failed", i->_key.c_str()); + } + + curl_easy_setopt(_easy, CURLOPT_HTTPPOST, formpost); + + ConnMan.registerEasyHandle(_easy); +} + +NetworkReadStream::NetworkReadStream(const char *url, curl_slist *headersList, Common::String postFields, bool uploading, bool usingPatch) { + init(url, headersList, (const byte *)postFields.c_str(), postFields.size(), uploading, usingPatch, false); +} + +NetworkReadStream::NetworkReadStream(const char *url, curl_slist *headersList, Common::HashMap<Common::String, Common::String> formFields, Common::HashMap<Common::String, Common::String> formFiles) { + init(url, headersList, formFields, formFiles); +} + +NetworkReadStream::NetworkReadStream(const char *url, curl_slist *headersList, const byte *buffer, uint32 bufferSize, bool uploading, bool usingPatch, bool post) { + init(url, headersList, buffer, bufferSize, uploading, usingPatch, post); +} + +NetworkReadStream::~NetworkReadStream() { + if (_easy) + curl_easy_cleanup(_easy); +} + +bool NetworkReadStream::eos() const { + return _eos; +} + +uint32 NetworkReadStream::read(void *dataPtr, uint32 dataSize) { + uint32 actuallyRead = MemoryReadWriteStream::read(dataPtr, dataSize); + + if (actuallyRead == 0) { + if (_requestComplete) + _eos = true; + return 0; + } + + return actuallyRead; +} + +void NetworkReadStream::finished() { + _requestComplete = true; +} + +long NetworkReadStream::httpResponseCode() const { + long responseCode = -1; + if (_easy) + curl_easy_getinfo(_easy, CURLINFO_RESPONSE_CODE, &responseCode); + return responseCode; +} + +Common::String NetworkReadStream::currentLocation() const { + Common::String result = ""; + if (_easy) { + char *pointer; + curl_easy_getinfo(_easy, CURLINFO_EFFECTIVE_URL, &pointer); + result = Common::String(pointer); + } + return result; +} + +Common::String NetworkReadStream::responseHeaders() const { + return _responseHeaders; +} + +uint32 NetworkReadStream::fillWithSendingContents(char *bufferToFill, uint32 maxSize) { + uint32 size = _sendingContentsSize - _sendingContentsPos; + if (size > maxSize) + size = maxSize; + for (uint32 i = 0; i < size; ++i) { + bufferToFill[i] = _sendingContentsBuffer[_sendingContentsPos + i]; + } + _sendingContentsPos += size; + return size; +} + +uint32 NetworkReadStream::addResponseHeaders(char *buffer, uint32 size) { + _responseHeaders += Common::String(buffer, size); + return size; +} + +double NetworkReadStream::getProgress() const { + if (_progressTotal < 1) + return 0; + return (double)_progressDownloaded / (double)_progressTotal; +} + +void NetworkReadStream::setProgress(uint64 downloaded, uint64 total) { + _progressDownloaded = downloaded; + _progressTotal = total; +} + +} // End of namespace Cloud diff --git a/backends/networking/curl/networkreadstream.h b/backends/networking/curl/networkreadstream.h new file mode 100644 index 0000000000..2be6d591cb --- /dev/null +++ b/backends/networking/curl/networkreadstream.h @@ -0,0 +1,142 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_NETWORKING_CURL_NETWORKREADSTREAM_H +#define BACKENDS_NETWORKING_CURL_NETWORKREADSTREAM_H + +#include "common/memstream.h" +#include "common/stream.h" +#include "common/str.h" +#include "common/hashmap.h" +#include "common/hash-str.h" + +typedef void CURL; +struct curl_slist; + +namespace Networking { + +class NetworkReadStream: public Common::MemoryReadWriteStream { + CURL *_easy; + bool _eos, _requestComplete; + const byte *_sendingContentsBuffer; + uint32 _sendingContentsSize; + uint32 _sendingContentsPos; + Common::String _responseHeaders; + uint64 _progressDownloaded, _progressTotal; + void init(const char *url, curl_slist *headersList, const byte *buffer, uint32 bufferSize, bool uploading, bool usingPatch, bool post); + void init(const char *url, curl_slist *headersList, Common::HashMap<Common::String, Common::String> formFields, Common::HashMap<Common::String, Common::String> formFiles); + +public: + /** Send <postFields>, using POST by default. */ + NetworkReadStream(const char *url, curl_slist *headersList, Common::String postFields, bool uploading = false, bool usingPatch = false); + /** Send <formFields>, <formFiles>, using POST multipart/form. */ + NetworkReadStream( + const char *url, curl_slist *headersList, + Common::HashMap<Common::String, Common::String> formFields, + Common::HashMap<Common::String, Common::String> formFiles); + /** Send <buffer, using POST by default. */ + NetworkReadStream(const char *url, curl_slist *headersList, const byte *buffer, uint32 bufferSize, bool uploading = false, bool usingPatch = false, bool post = true); + virtual ~NetworkReadStream(); + + /** + * Returns true if a read failed because the stream end has been reached. + * This flag is cleared by clearErr(). + * For a SeekableReadStream, it is also cleared by a successful seek. + * + * @note The semantics of any implementation of this method are + * supposed to match those of ISO C feof(). In particular, in a stream + * with N bytes, reading exactly N bytes from the start should *not* + * set eos; only reading *beyond* the available data should set it. + */ + virtual bool eos() const; + + /** + * Read data from the stream. Subclasses must implement this + * method; all other read methods are implemented using it. + * + * @note The semantics of any implementation of this method are + * supposed to match those of ISO C fread(), in particular where + * it concerns setting error and end of file/stream flags. + * + * @param dataPtr pointer to a buffer into which the data is read + * @param dataSize number of bytes to be read + * @return the number of bytes which were actually read. + */ + virtual uint32 read(void *dataPtr, uint32 dataSize); + + /** + * This method is called by ConnectionManager to indicate + * that transfer is finished. + * + * @note It's called on failure too. + */ + void finished(); + + /** + * Returns HTTP response code from inner CURL handle. + * It returns -1 to indicate there is no inner handle. + * + * @note This method should be called when eos() == true. + */ + long httpResponseCode() const; + + /** + * Return current location URL from inner CURL handle. + * "" is returned to indicate there is no inner handle. + * + * @note This method should be called when eos() == true. + */ + Common::String currentLocation() const; + + /** + * Return response headers. + * + * @note This method should be called when eos() == true. + */ + Common::String responseHeaders() const; + + /** + * Fills the passed buffer with _sendingContentsBuffer contents. + * It works similarly to read(), expect it's not for reading + * Stream's contents, but for sending our own data to the server. + * + * @returns how many bytes were actually read (filled in) + */ + uint32 fillWithSendingContents(char *bufferToFill, uint32 maxSize); + + /** + * Remembers headers returned to CURL in server's response. + * + * @returns how many bytes were actually read + */ + uint32 addResponseHeaders(char *buffer, uint32 size); + + /** Returns a number in range [0, 1], where 1 is "complete". */ + double getProgress() const; + + /** Used in curl progress callback to pass current downloaded/total values. */ + void setProgress(uint64 downloaded, uint64 total); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/curl/request.cpp b/backends/networking/curl/request.cpp new file mode 100644 index 0000000000..30af48a478 --- /dev/null +++ b/backends/networking/curl/request.cpp @@ -0,0 +1,74 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/networking/curl/request.h" + +namespace Networking { + +ErrorResponse::ErrorResponse(Request *rq): + request(rq), interrupted(false), failed(true), response(""), httpResponseCode(-1) {} + +ErrorResponse::ErrorResponse(Request *rq, bool interrupt, bool failure, Common::String resp, long httpCode): + request(rq), interrupted(interrupt), failed(failure), response(resp), httpResponseCode(httpCode) {} + +Request::Request(DataCallback cb, ErrorCallback ecb): + _callback(cb), _errorCallback(ecb), _state(PROCESSING), _retryInSeconds(0) {} + +Request::~Request() { + delete _callback; + delete _errorCallback; +} + +void Request::handleRetry() { + if (_retryInSeconds > 0) { + --_retryInSeconds; + } else { + _state = PROCESSING; + restart(); + } +} + +void Request::pause() { _state = PAUSED; } + +void Request::finish() { + ErrorResponse error(this, true, false, "", -1); + finishError(error); +} + +void Request::retry(uint32 seconds) { + _state = RETRY; + _retryInSeconds = seconds; +} + +RequestState Request::state() const { return _state; } + +Common::String Request::date() const { return ""; } + +void Request::finishError(ErrorResponse error) { + _state = FINISHED; + if (_errorCallback) + (*_errorCallback)(error); +} + +void Request::finishSuccess() { _state = FINISHED; } + +} // End of namespace Networking diff --git a/backends/networking/curl/request.h b/backends/networking/curl/request.h new file mode 100644 index 0000000000..9b366ea40c --- /dev/null +++ b/backends/networking/curl/request.h @@ -0,0 +1,203 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_NETWORKING_CURL_REQUEST_H +#define BACKENDS_NETWORKING_CURL_REQUEST_H + +#include "common/callback.h" +#include "common/scummsys.h" +#include "common/str.h" + +namespace Networking { + +class Request; + +/** + * Response<T> is a struct to be returned from Request + * to user's callbacks. It's a type safe way to indicate + * which "return value" Request has and user awaits. + * + * It just keeps a Request pointer together with + * some T value (which might be a pointer, a reference + * or a plain type (copied by value)). + * + * To make it more convenient, typedefs are used. + * For example, Response<void *> is called DataResponse + * and corresponding callback pointer is DataCallback. + */ + +template<typename T> struct Response { + Request *request; + T value; + + Response(Request *rq, T v) : request(rq), value(v) {} +}; + +/** + * ErrorResponse is a struct to be returned from Request + * to user's failure callbacks. + * + * It keeps a Request pointer together with some useful + * information fields, which would explain why failure + * callback was called. + * + * <interrupted> flag is set when Request was interrupted, + * i.e. finished by user with finish() call. + * + * <failed> flag is set when Request has failed because of + * some error (bad server response, for example). + * + * <response> contains server's original response. + * + * <httpResponseCode> contains server's HTTP response code. + */ + +struct ErrorResponse { + Request *request; + bool interrupted; + bool failed; + Common::String response; + long httpResponseCode; + + ErrorResponse(Request *rq); + ErrorResponse(Request *rq, bool interrupt, bool failure, Common::String resp, long httpCode); +}; + +typedef Response<void *> DataReponse; +typedef Common::BaseCallback<DataReponse> *DataCallback; +typedef Common::BaseCallback<ErrorResponse> *ErrorCallback; + +/** + * RequestState is used to indicate current Request state. + * ConnectionManager uses it to decide what to do with the Request. + * + * PROCESSING state indicates that Request is working. + * ConnectionManager calls handle() method of Requests in that state. + * + * PAUSED state indicates that Request is not working. + * ConnectionManager keeps Requests in that state and doesn't call any methods of those. + * + * RETRY state indicates that Request must restart after a few seconds. + * ConnectionManager calls handleRetry() method of Requests in that state. + * Default handleRetry() implementation decreases _retryInSeconds value + * until it reaches zero. When it does, Request's restart() method is called. + * + * FINISHED state indicates that Request did the work and might be deleted. + * ConnectionManager deletes Requests in that state. + * After this state is set, but before ConnectionManager deletes the Request, + * Request calls user's callback. User can ask Request to change its state + * by calling retry() or pause() methods and Request won't be deleted. + * + * Request get a success and failure callbacks. Request must call one + * (and only one!) of these callbacks when it sets FINISHED state. + */ +enum RequestState { + PROCESSING, + PAUSED, + RETRY, + FINISHED +}; + +class Request { +protected: + /** + * Callback, which should be called when Request is finished. + * That's the way Requests pass the result to the code which asked to create this request. + * + * @note some Requests use their own callbacks to return something but void *. + * @note callback must be called in finish() or similar method. + */ + DataCallback _callback; + + /** + * Callback, which should be called when Request is failed/interrupted. + * That's the way Requests pass error information to the code which asked to create this request. + * @note callback must be called in finish() or similar method. + */ + ErrorCallback _errorCallback; + + /** + * Request state, which is used by ConnectionManager to determine + * whether request might be deleted or it's still working. + * + * State might be changed from outside with finish(), pause() or + * retry() methods. Override these if you want to react to these + * changes correctly. + */ + RequestState _state; + + /** In RETRY state this indicates whether it's time to call restart(). */ + uint32 _retryInSeconds; + + /** Sets FINISHED state and calls the _errorCallback with given error. */ + virtual void finishError(ErrorResponse error); + + /** Sets FINISHED state. Implementations might extend it if needed. */ + virtual void finishSuccess(); + +public: + Request(DataCallback cb, ErrorCallback ecb); + virtual ~Request(); + + /** Method, which does actual work. Depends on what this Request is doing. */ + virtual void handle() = 0; + + /** Method, which is called by ConnectionManager when Request's state is RETRY. */ + virtual void handleRetry(); + + /** Method, which is used to restart the Request. */ + virtual void restart() = 0; + + /** Method, which is called to pause the Request. */ + virtual void pause(); + + /** + * Method, which is called to *interrupt* the Request. + * When it's called, Request must stop its work and + * call the failure callback to notify user. + */ + virtual void finish(); + + /** Method, which is called to retry the Request. */ + virtual void retry(uint32 seconds); + + /** Returns Request's current state. */ + RequestState state() const; + + /** + * Return date this Request received from server. + * It could be extracted from "Date" header, + * which is kept in NetworkReadStream. + * + * @note not all Requests do that, so "" is returned + * to indicate the date is unknown. That's also true + * if no server response available or no "Date" header + * was passed. + * + * @returns date from "Date" response header. + */ + virtual Common::String date() const; +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/make_archive.py b/backends/networking/make_archive.py new file mode 100644 index 0000000000..64d314bedd --- /dev/null +++ b/backends/networking/make_archive.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +# encoding: utf-8 +import sys +import re +import os +import zipfile + +ARCHIVE_FILE_EXTENSIONS = ('.html', '.css', '.js', '.ico', '.png') + +def buildArchive(archiveName): + if not os.path.isdir(archiveName): + print ("Invalid archive name: " + archiveName) + return + + zf = zipfile.ZipFile(archiveName + ".zip", 'w') + + print ("Building '" + archiveName + "' archive:") + os.chdir(archiveName) + + directories = ['.', './icons'] + for d in directories: + filenames = os.listdir(d) + filenames.sort() + for filename in filenames: + if os.path.isfile(d + '/' + filename) and filename.endswith(ARCHIVE_FILE_EXTENSIONS): + zf.write(d + '/' + filename, d + '/' + filename) + print (" Adding file: " + d + '/' + filename) + + os.chdir('../') + + zf.close() + +def main(): + buildArchive("wwwroot") + +if __name__ == "__main__": + sys.exit(main()) diff --git a/backends/networking/sdl_net/client.cpp b/backends/networking/sdl_net/client.cpp new file mode 100644 index 0000000000..dab38ba5c0 --- /dev/null +++ b/backends/networking/sdl_net/client.cpp @@ -0,0 +1,190 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#define FORBIDDEN_SYMBOL_ALLOW_ALL + +#include "backends/networking/sdl_net/client.h" +#include "backends/networking/sdl_net/localwebserver.h" +#include "common/memstream.h" +#include <SDL/SDL_net.h> + +namespace Networking { + +Client::Client(): + _state(INVALID), _set(nullptr), _socket(nullptr), _handler(nullptr), + _previousHandler(nullptr), _stream(nullptr), _buffer(new byte[CLIENT_BUFFER_SIZE]) {} + +Client::Client(SDLNet_SocketSet set, TCPsocket socket): + _state(INVALID), _set(nullptr), _socket(nullptr), _handler(nullptr), + _previousHandler(nullptr), _stream(nullptr), _buffer(new byte[CLIENT_BUFFER_SIZE]) { + open(set, socket); +} + +Client::~Client() { + close(); + delete[] _buffer; +} + +void Client::open(SDLNet_SocketSet set, TCPsocket socket) { + if (_state != INVALID) + close(); + _state = READING_HEADERS; + _socket = socket; + _set = set; + Reader cleanReader; + _reader = cleanReader; + if (_handler) + delete _handler; + _handler = nullptr; + if (_previousHandler) + delete _previousHandler; + _previousHandler = nullptr; + _stream = new Common::MemoryReadWriteStream(DisposeAfterUse::YES); + if (set) { + int numused = SDLNet_TCP_AddSocket(set, socket); + if (numused == -1) { + error("Client: SDLNet_AddSocket: %s\n", SDLNet_GetError()); + } + } +} + +bool Client::readMoreIfNeeded() { + if (_stream == nullptr) + return false; //nothing to read into + if (_stream->size() - _stream->pos() > 0) + return true; //not needed, some data left in the stream + if (!_socket) + return false; + if (!SDLNet_SocketReady(_socket)) + return false; + + int bytes = SDLNet_TCP_Recv(_socket, _buffer, CLIENT_BUFFER_SIZE); + if (bytes <= 0) { + warning("Client::readMoreIfNeeded: recv fail"); + close(); + return false; + } + + if (_stream->write(_buffer, bytes) != bytes) { + warning("Client::readMoreIfNeeded: failed to write() into MemoryReadWriteStream"); + close(); + return false; + } + + return true; +} + +void Client::readHeaders() { + if (!readMoreIfNeeded()) + return; + _reader.setContent(_stream); + if (_reader.readFirstHeaders()) + _state = (_reader.badRequest() ? BAD_REQUEST : READ_HEADERS); +} + +bool Client::readContent(Common::WriteStream *stream) { + if (!readMoreIfNeeded()) + return false; + _reader.setContent(_stream); + return _reader.readFirstContent(stream); +} + +bool Client::readBlockHeaders(Common::WriteStream *stream) { + if (!readMoreIfNeeded()) + return false; + _reader.setContent(_stream); + return _reader.readBlockHeaders(stream); +} + +bool Client::readBlockContent(Common::WriteStream *stream) { + if (!readMoreIfNeeded()) + return false; + _reader.setContent(_stream); + return _reader.readBlockContent(stream); +} + +void Client::setHandler(ClientHandler *handler) { + if (_handler) { + if (_previousHandler) + delete _previousHandler; + _previousHandler = _handler; //can't just delete it, as setHandler() could've been called by handler itself + } + _state = BEING_HANDLED; + _handler = handler; +} + +void Client::handle() { + if (_state != BEING_HANDLED) + warning("handle() called in a wrong Client's state"); + if (!_handler) + warning("Client doesn't have handler to be handled by"); + if (_handler) + _handler->handle(this); +} + +void Client::close() { + if (_set) { + if (_socket) { + int numused = SDLNet_TCP_DelSocket(_set, _socket); + if (numused == -1) + error("Client: SDLNet_DelSocket: %s\n", SDLNet_GetError()); + } + _set = nullptr; + } + + if (_socket) { + SDLNet_TCP_Close(_socket); + _socket = nullptr; + } + + if (_stream) { + delete _stream; + _stream = nullptr; + } + + _state = INVALID; +} + + +ClientState Client::state() const { return _state; } + +Common::String Client::headers() const { return _reader.headers(); } + +Common::String Client::method() const { return _reader.method(); } + +Common::String Client::path() const { return _reader.path(); } + +Common::String Client::query() const { return _reader.query(); } + +Common::String Client::queryParameter(Common::String name) const { return _reader.queryParameter(name); } + +Common::String Client::anchor() const { return _reader.anchor(); } + +bool Client::noMoreContent() const { return _reader.noMoreContent(); } + +bool Client::socketIsReady() { return SDLNet_SocketReady(_socket); } + +int Client::recv(void *data, int maxlen) { return SDLNet_TCP_Recv(_socket, data, maxlen); } + +int Client::send(void *data, int len) { return SDLNet_TCP_Send(_socket, data, len); } + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/client.h b/backends/networking/sdl_net/client.h new file mode 100644 index 0000000000..134c1be05d --- /dev/null +++ b/backends/networking/sdl_net/client.h @@ -0,0 +1,125 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_NETWORKING_SDL_NET_CLIENT_H +#define BACKENDS_NETWORKING_SDL_NET_CLIENT_H + +#include "backends/networking/sdl_net/reader.h" +#include "common/str.h" + +namespace Common { +class MemoryReadWriteStream; +} + +typedef struct _SDLNet_SocketSet *SDLNet_SocketSet; +typedef struct _TCPsocket *TCPsocket; + +namespace Networking { + +enum ClientState { + INVALID, + READING_HEADERS, + READ_HEADERS, + BAD_REQUEST, + BEING_HANDLED +}; + +class Client; + +#define CLIENT_BUFFER_SIZE 1 * 1024 * 1024 + +class ClientHandler { +public: + virtual ~ClientHandler() {}; + virtual void handle(Client *client) = 0; +}; + +/** + * Client class represents one client's HTTP request + * to the LocalWebserver. + * + * While in READING_HEADERS state, it's kept in LocalWebserver. + * Client must read the headers and decide whether it's + * READ_HEADERS (could be handled) or BAD_REQUEST (failed). + * + * If it's READ_HEADERS, LocalWebserver searches for a corresponding + * BaseHandler. These classes use the information from headers - + * like method, path, GET parameters - to build the response + * for this client's request. When they do, they call setHandler() + * and pass a special ClientHandler. Client becomes BEING_HANDLED. + * + * While in that state, LocalWebserver calls Client's handle() and + * it's passed to ClientHandler. The latter does the job: it commands + * Client to read or write bytes with its socket or calls + * readContent() methods, so Client reads the request through Reader. + */ + +class Client { + ClientState _state; + SDLNet_SocketSet _set; + TCPsocket _socket; + Reader _reader; + ClientHandler *_handler, *_previousHandler; + Common::MemoryReadWriteStream *_stream; + byte *_buffer; + + bool readMoreIfNeeded(); + +public: + Client(); + Client(SDLNet_SocketSet set, TCPsocket socket); + virtual ~Client(); + + void open(SDLNet_SocketSet set, TCPsocket socket); + void readHeaders(); + bool readContent(Common::WriteStream *stream); + bool readBlockHeaders(Common::WriteStream *stream); + bool readBlockContent(Common::WriteStream *stream); + void setHandler(ClientHandler *handler); + void handle(); + void close(); + + ClientState state() const; + Common::String headers() const; + Common::String method() const; + Common::String path() const; + Common::String query() const; + Common::String queryParameter(Common::String name) const; + Common::String anchor() const; + + bool noMoreContent() const; + + /** + * Return SDLNet_SocketReady(_socket). + * + * It's "ready" when it has something + * to read (recv()). You can send() + * when this is false. + */ + bool socketIsReady(); + int recv(void *data, int maxlen); + int send(void *data, int len); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/getclienthandler.cpp b/backends/networking/sdl_net/getclienthandler.cpp new file mode 100644 index 0000000000..1c4f5db8a8 --- /dev/null +++ b/backends/networking/sdl_net/getclienthandler.cpp @@ -0,0 +1,162 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/networking/sdl_net/getclienthandler.h" +#include "common/textconsole.h" + +namespace Networking { + +GetClientHandler::GetClientHandler(Common::SeekableReadStream *stream): + _responseCode(200), _headersPrepared(false), + _stream(stream), _buffer(new byte[CLIENT_HANDLER_BUFFER_SIZE]) {} + +GetClientHandler::~GetClientHandler() { + delete _stream; + delete[] _buffer; +} + +const char *GetClientHandler::responseMessage(long responseCode) { + switch (responseCode) { + case 100: return "Continue"; + case 101: return "Switching Protocols"; + case 102: return "Processing"; + + case 200: return "OK"; + case 201: return "Created"; + case 202: return "Accepted"; + case 203: return "Non-Authoritative Information"; + case 204: return "No Content"; + case 205: return "Reset Content"; + case 206: return "Partial Content"; + case 207: return "Multi-Status"; + case 226: return "IM Used"; + + case 300: return "Multiple Choices"; + case 301: return "Moved Permanently"; + case 302: return "Moved Temporarily"; //case 302: return "Found"; + case 303: return "See Other"; + case 304: return "Not Modified"; + case 305: return "Use Proxy"; + case 306: return "RESERVED"; + case 307: return "Temporary Redirect"; + + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 402: return "Payment Required"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 406: return "Not Acceptable"; + case 407: return "Proxy Authentication Required"; + case 408: return "Request Timeout"; + case 409: return "Conflict"; + case 410: return "Gone"; + case 411: return "Length Required"; + case 412: return "Precondition Failed"; + case 413: return "Request Entity Too Large"; + case 414: return "Request-URI Too Large"; + case 415: return "Unsupported Media Type"; + case 416: return "Requested Range Not Satisfiable"; + case 417: return "Expectation Failed"; + case 422: return "Unprocessable Entity"; + case 423: return "Locked"; + case 424: return "Failed Dependency"; + case 425: return "Unordered Collection"; + case 426: return "Upgrade Required"; + case 428: return "Precondition Required"; + case 429: return "Too Many Requests"; + case 431: return "Request Header Fields Too Large"; + case 434: return "Requested Host Unavailable"; + case 449: return "Retry With"; + case 451: return "Unavailable For Legal Reasons"; + + case 500: return "Internal Server Error"; + case 501: return "Not Implemented"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + case 504: return "Gateway Timeout"; + case 505: return "HTTP Version Not Supported"; + case 506: return "Variant Also Negotiates"; + case 507: return "Insufficient Storage"; + case 508: return "Loop Detected"; + case 509: return "Bandwidth Limit Exceeded"; + case 510: return "Not Extended"; + case 511: return "Network Authentication Required"; + } + return "Unknown"; +} + +void GetClientHandler::prepareHeaders() { + if (!_specialHeaders.contains("Content-Type")) + setHeader("Content-Type", "text/html"); + + if (!_specialHeaders.contains("Content-Length") && _stream) + setHeader("Content-Length", Common::String::format("%u", _stream->size())); + + _headers = Common::String::format("HTTP/1.1 %ld %s\r\n", _responseCode, responseMessage(_responseCode)); + for (Common::HashMap<Common::String, Common::String>::iterator i = _specialHeaders.begin(); i != _specialHeaders.end(); ++i) + _headers += i->_key + ": " + i->_value + "\r\n"; + _headers += "\r\n"; + + _headersPrepared = true; +} + +void GetClientHandler::handle(Client *client) { + if (!client) + return; + if (!_headersPrepared) + prepareHeaders(); + + uint32 readBytes; + + // send headers first + if (_headers.size() > 0) { + readBytes = _headers.size(); + if (readBytes > CLIENT_HANDLER_BUFFER_SIZE) + readBytes = CLIENT_HANDLER_BUFFER_SIZE; + memcpy(_buffer, _headers.c_str(), readBytes); + _headers.erase(0, readBytes); + } else { + if (!_stream) { + client->close(); + return; + } + + readBytes = _stream->read(_buffer, CLIENT_HANDLER_BUFFER_SIZE); + } + + if (readBytes != 0) + if (client->send(_buffer, readBytes) != readBytes) { + warning("GetClientHandler: unable to send all bytes to the client"); + client->close(); + return; + } + + // we're done here! + if (_stream->eos()) + client->close(); +} + +void GetClientHandler::setHeader(Common::String name, Common::String value) { _specialHeaders[name] = value; } +void GetClientHandler::setResponseCode(long code) { _responseCode = code; } + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/getclienthandler.h b/backends/networking/sdl_net/getclienthandler.h new file mode 100644 index 0000000000..3486ceef8a --- /dev/null +++ b/backends/networking/sdl_net/getclienthandler.h @@ -0,0 +1,57 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_NETWORKING_SDL_NET_GETCLIENTHANDLER_H +#define BACKENDS_NETWORKING_SDL_NET_GETCLIENTHANDLER_H + +#include "backends/networking/sdl_net/client.h" +#include "common/hashmap.h" +#include "common/stream.h" +#include "common/hash-str.h" + +namespace Networking { + +#define CLIENT_HANDLER_BUFFER_SIZE 1 * 1024 * 1024 + +class GetClientHandler: public ClientHandler { + Common::HashMap<Common::String, Common::String> _specialHeaders; + long _responseCode; + bool _headersPrepared; + Common::String _headers; + Common::SeekableReadStream *_stream; + byte *_buffer; + + static const char *responseMessage(long responseCode); + void prepareHeaders(); + +public: + GetClientHandler(Common::SeekableReadStream *stream); + virtual ~GetClientHandler(); + + virtual void handle(Client *client); + void setHeader(Common::String name, Common::String value); + void setResponseCode(long code); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/handlers/basehandler.h b/backends/networking/sdl_net/handlers/basehandler.h new file mode 100644 index 0000000000..dec5e955bd --- /dev/null +++ b/backends/networking/sdl_net/handlers/basehandler.h @@ -0,0 +1,41 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_NETWORKING_SDL_NET_BASEHANDLER_H +#define BACKENDS_NETWORKING_SDL_NET_BASEHANDLER_H + +#include "backends/networking/sdl_net/client.h" + +namespace Networking { + +class BaseHandler { +public: + BaseHandler() {} + virtual ~BaseHandler() {} + + virtual void handle(Client &) = 0; + virtual bool minimalModeSupported() { return false; } +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/handlers/createdirectoryhandler.cpp b/backends/networking/sdl_net/handlers/createdirectoryhandler.cpp new file mode 100644 index 0000000000..284bf16651 --- /dev/null +++ b/backends/networking/sdl_net/handlers/createdirectoryhandler.cpp @@ -0,0 +1,129 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/networking/sdl_net/handlers/createdirectoryhandler.h" +#include "backends/fs/fs-factory.h" +#include "backends/networking/sdl_net/handlerutils.h" +#include "backends/networking/sdl_net/localwebserver.h" +#include "common/translation.h" +#include <common/callback.h> + +namespace Networking { + +CreateDirectoryHandler::CreateDirectoryHandler() {} + +CreateDirectoryHandler::~CreateDirectoryHandler() {} + +void CreateDirectoryHandler::handleError(Client &client, Common::String message) const { + if (client.queryParameter("answer_json") == "true") + setJsonResponseHandler(client, "error", message); + else + HandlerUtils::setFilesManagerErrorMessageHandler(client, message); +} + +void CreateDirectoryHandler::setJsonResponseHandler(Client &client, Common::String type, Common::String message) const { + Common::JSONObject response; + response.setVal("type", new Common::JSONValue(type)); + response.setVal("message", new Common::JSONValue(message)); + + Common::JSONValue json = response; + LocalWebserver::setClientGetHandler(client, json.stringify(true)); +} + +/// public + +void CreateDirectoryHandler::handle(Client &client) { + Common::String path = client.queryParameter("path"); + Common::String name = client.queryParameter("directory_name"); + + // check that <path> is not an absolute root + if (path == "" || path == "/") { + handleError(client, _("Can't create directory here!")); + return; + } + + // check that <path> contains no '../' + if (HandlerUtils::hasForbiddenCombinations(path)) { + handleError(client, _("Invalid path!")); + return; + } + + // transform virtual path to actual file system one + Common::String prefixToRemove = "", prefixToAdd = ""; + if (!transformPath(path, prefixToRemove, prefixToAdd) || path.empty()) { + handleError(client, _("Invalid path!")); + return; + } + + // check that <path> exists, is directory and isn't forbidden + AbstractFSNode *node = g_system->getFilesystemFactory()->makeFileNodePath(path); + if (!HandlerUtils::permittedPath(node->getPath())) { + handleError(client, _("Invalid path!")); + return; + } + if (!node->exists()) { + handleError(client, _("Parent directory doesn't exists!")); + return; + } + if (!node->isDirectory()) { + handleError(client, _("Can't create a directory within a file!")); + return; + } + + // check that <directory_name> doesn't exist or is directory + if (path.lastChar() != '/' && path.lastChar() != '\\') + path += '/'; + node = g_system->getFilesystemFactory()->makeFileNodePath(path + name); + if (node->exists()) { + if (!node->isDirectory()) { + handleError(client, _("There is a file with that name in the parent directory!")); + return; + } + } else { + // create the <directory_name> in <path> + if (!node->create(true)) { + handleError(client, _("Failed to create the directory!")); + return; + } + } + + // if json requested, respond with it + if (client.queryParameter("answer_json") == "true") { + setJsonResponseHandler(client, "success", _("Directory created successfully!")); + return; + } + + // set redirect on success + HandlerUtils::setMessageHandler( + client, + Common::String::format( + "%s<br/><a href=\"files?path=%s\">%s</a>", + _("Directory created successfully!"), + client.queryParameter("path").c_str(), + _("Back to parent directory") + ), + (client.queryParameter("ajax") == "true" ? "/filesAJAX?path=" : "/files?path=") + + LocalWebserver::urlEncodeQueryParameterValue(client.queryParameter("path")) + ); +} + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/handlers/createdirectoryhandler.h b/backends/networking/sdl_net/handlers/createdirectoryhandler.h new file mode 100644 index 0000000000..2a18d5c4aa --- /dev/null +++ b/backends/networking/sdl_net/handlers/createdirectoryhandler.h @@ -0,0 +1,42 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_NETWORKING_SDL_NET_CREATEDIRECTORYHANDLER_H +#define BACKENDS_NETWORKING_SDL_NET_CREATEDIRECTORYHANDLER_H + +#include "backends/networking/sdl_net/handlers/filesbasehandler.h" + +namespace Networking { + +class CreateDirectoryHandler: public FilesBaseHandler { + void handleError(Client &client, Common::String message) const; + void setJsonResponseHandler(Client &client, Common::String type, Common::String message) const; +public: + CreateDirectoryHandler(); + virtual ~CreateDirectoryHandler(); + + virtual void handle(Client &client); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/handlers/downloadfilehandler.cpp b/backends/networking/sdl_net/handlers/downloadfilehandler.cpp new file mode 100644 index 0000000000..9e212b1a6c --- /dev/null +++ b/backends/networking/sdl_net/handlers/downloadfilehandler.cpp @@ -0,0 +1,88 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/networking/sdl_net/handlers/downloadfilehandler.h" +#include "backends/fs/fs-factory.h" +#include "backends/networking/sdl_net/getclienthandler.h" +#include "backends/networking/sdl_net/handlerutils.h" +#include "backends/networking/sdl_net/localwebserver.h" +#include "common/translation.h" + +namespace Networking { + +DownloadFileHandler::DownloadFileHandler() {} + +DownloadFileHandler::~DownloadFileHandler() {} + +/// public + +void DownloadFileHandler::handle(Client &client) { + Common::String path = client.queryParameter("path"); + + // check that <path> is not an absolute root + if (path == "" || path == "/") { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("Invalid path!")); + return; + } + + // check that <path> contains no '../' + if (HandlerUtils::hasForbiddenCombinations(path)) { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("Invalid path!")); + return; + } + + // transform virtual path to actual file system one + Common::String prefixToRemove = "", prefixToAdd = ""; + if (!transformPath(path, prefixToRemove, prefixToAdd, false) || path.empty()) { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("Invalid path!")); + return; + } + + // check that <path> exists, is directory and isn't forbidden + AbstractFSNode *node = g_system->getFilesystemFactory()->makeFileNodePath(path); + if (!HandlerUtils::permittedPath(node->getPath())) { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("Invalid path!")); + return; + } + if (!node->exists()) { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("The file doesn't exist!")); + return; + } + if (node->isDirectory()) { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("Can't download a directory!")); + return; + } + Common::SeekableReadStream *stream = node->createReadStream(); + if (stream == nullptr) { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("Failed to read the file!")); + return; + } + + GetClientHandler *handler = new GetClientHandler(stream); + handler->setResponseCode(200); + handler->setHeader("Content-Type", "application/force-download"); + handler->setHeader("Content-Disposition", "attachment; filename=\"" + node->getDisplayName() + "\""); + handler->setHeader("Content-Transfer-Encoding", "binary"); + client.setHandler(handler); +} + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/handlers/downloadfilehandler.h b/backends/networking/sdl_net/handlers/downloadfilehandler.h new file mode 100644 index 0000000000..5fa5e5d55a --- /dev/null +++ b/backends/networking/sdl_net/handlers/downloadfilehandler.h @@ -0,0 +1,40 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_NETWORKING_SDL_NET_DOWNLOADFILEHANDLER_H +#define BACKENDS_NETWORKING_SDL_NET_DOWNLOADFILEHANDLER_H + +#include "backends/networking/sdl_net/handlers/filesbasehandler.h" + +namespace Networking { + +class DownloadFileHandler: public FilesBaseHandler { +public: + DownloadFileHandler(); + virtual ~DownloadFileHandler(); + + virtual void handle(Client &client); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/handlers/filesajaxpagehandler.cpp b/backends/networking/sdl_net/handlers/filesajaxpagehandler.cpp new file mode 100644 index 0000000000..8c5ee29b70 --- /dev/null +++ b/backends/networking/sdl_net/handlers/filesajaxpagehandler.cpp @@ -0,0 +1,81 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/networking/sdl_net/handlers/filesajaxpagehandler.h" +#include "backends/networking/sdl_net/handlerutils.h" +#include "backends/networking/sdl_net/localwebserver.h" +#include "common/translation.h" + +namespace Networking { + +#define FILES_PAGE_NAME ".filesAJAX.html" + +FilesAjaxPageHandler::FilesAjaxPageHandler() {} + +FilesAjaxPageHandler::~FilesAjaxPageHandler() {} + +namespace { + +Common::String encodeDoubleQuotesAndSlashes(Common::String s) { + Common::String result = ""; + for (uint32 i = 0; i < s.size(); ++i) + if (s[i] == '"') { + result += "\\\""; + } else if (s[i] == '\\') { + result += "\\\\"; + } else { + result += s[i]; + } + return result; +} + +} + +/// public + +void FilesAjaxPageHandler::handle(Client &client) { + // load stylish response page from the archive + Common::SeekableReadStream *const stream = HandlerUtils::getArchiveFile(FILES_PAGE_NAME); + if (stream == nullptr) { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("The page is not available without the resources.")); + return; + } + + Common::String response = HandlerUtils::readEverythingFromStream(stream); + Common::String path = client.queryParameter("path"); + + //these occur twice: + replace(response, "{create_directory_button}", _("Create directory")); + replace(response, "{create_directory_button}", _("Create directory")); + replace(response, "{upload_files_button}", _("Upload files")); //tab + replace(response, "{upload_file_button}", _("Upload files")); //button in the tab + replace(response, "{create_directory_desc}", _("Type new directory name:")); + replace(response, "{upload_file_desc}", _("Select a file to upload:")); + replace(response, "{or_upload_directory_desc}", _("Or select a directory (works in Chrome only):")); + replace(response, "{index_of}", _("Index of ")); + replace(response, "{loading}", _("Loading...")); + replace(response, "{error}", _("Error occurred")); + replace(response, "{start_path}", encodeDoubleQuotesAndSlashes(path)); + LocalWebserver::setClientGetHandler(client, response); +} + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/handlers/filesajaxpagehandler.h b/backends/networking/sdl_net/handlers/filesajaxpagehandler.h new file mode 100644 index 0000000000..1d9b125c2e --- /dev/null +++ b/backends/networking/sdl_net/handlers/filesajaxpagehandler.h @@ -0,0 +1,40 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_NETWORKING_SDL_NET_FILESAJAXPAGEHANDLER_H +#define BACKENDS_NETWORKING_SDL_NET_FILESAJAXPAGEHANDLER_H + +#include "backends/networking/sdl_net/handlers/filesbasehandler.h" + +namespace Networking { + +class FilesAjaxPageHandler: public FilesBaseHandler { +public: + FilesAjaxPageHandler(); + virtual ~FilesAjaxPageHandler(); + + virtual void handle(Client &client); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/handlers/filesbasehandler.cpp b/backends/networking/sdl_net/handlers/filesbasehandler.cpp new file mode 100644 index 0000000000..135e0fb100 --- /dev/null +++ b/backends/networking/sdl_net/handlers/filesbasehandler.cpp @@ -0,0 +1,87 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/networking/sdl_net/handlers/filesbasehandler.h" +#include "backends/saves/default/default-saves.h" +#include "common/config-manager.h" +#include "common/system.h" + +namespace Networking { + +FilesBaseHandler::FilesBaseHandler() {} + +FilesBaseHandler::~FilesBaseHandler() {} + +Common::String FilesBaseHandler::parentPath(Common::String path) { + if (path.size() && (path.lastChar() == '/' || path.lastChar() == '\\')) path.deleteLastChar(); + if (!path.empty()) { + for (int i = path.size() - 1; i >= 0; --i) + if (i == 0 || path[i] == '/' || path[i] == '\\') { + path.erase(i); + break; + } + } + if (path.size() && path.lastChar() != '/' && path.lastChar() != '\\') + path += '/'; + return path; +} + +bool FilesBaseHandler::transformPath(Common::String &path, Common::String &prefixToRemove, Common::String &prefixToAdd, bool isDirectory) { + // <path> is not empty, but could lack the trailing slash + if (isDirectory && path.lastChar() != '/' && path.lastChar() != '\\') + path += '/'; + + if (path.hasPrefix("/root") && ConfMan.hasKey("rootpath", "cloud")) { + prefixToAdd = "/root/"; + prefixToRemove = ConfMan.get("rootpath", "cloud"); + if (prefixToRemove.size() && prefixToRemove.lastChar() != '/' && prefixToRemove.lastChar() != '\\') + prefixToRemove += '/'; + if (prefixToRemove == "/") prefixToRemove = ""; + path.erase(0, 5); + if (path.size() && (path[0] == '/' || path[0] == '\\')) + path.deleteChar(0); // if that was "/root/ab/c", it becomes "/ab/c", but we need "ab/c" + path = prefixToRemove + path; + if (path == "") + path = "/"; // absolute root is '/' + return true; + } + + if (path.hasPrefix("/saves")) { + prefixToAdd = "/saves/"; + + // determine savepath (prefix to remove) + DefaultSaveFileManager *manager = dynamic_cast<DefaultSaveFileManager *>(g_system->getSavefileManager()); + prefixToRemove = (manager ? manager->concatWithSavesPath("") : ConfMan.get("savepath")); + if (prefixToRemove.size() && prefixToRemove.lastChar() != '/' && prefixToRemove.lastChar() != '\\') + prefixToRemove += '/'; + + path.erase(0, 6); + if (path.size() && (path[0] == '/' || path[0] == '\\')) + path.deleteChar(0); + path = prefixToRemove + path; + return true; + } + + return false; +} + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/handlers/filesbasehandler.h b/backends/networking/sdl_net/handlers/filesbasehandler.h new file mode 100644 index 0000000000..1c7f4dd799 --- /dev/null +++ b/backends/networking/sdl_net/handlers/filesbasehandler.h @@ -0,0 +1,52 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_NETWORKING_SDL_NET_FILESBASEHANDLER_H +#define BACKENDS_NETWORKING_SDL_NET_FILESBASEHANDLER_H + +#include "backends/networking/sdl_net/handlers/basehandler.h" + +namespace Networking { + +class FilesBaseHandler: public BaseHandler { +protected: + Common::String parentPath(Common::String path); + + /** + * Transforms virtual <path> into actual file system path. + * + * Fills prefixes with actual file system prefix ("to remove") + * and virtual path prefix ("to add"). + * + * Returns true on success. + */ + bool transformPath(Common::String &path, Common::String &prefixToRemove, Common::String &prefixToAdd, bool isDirectory = true); +public: + FilesBaseHandler(); + virtual ~FilesBaseHandler(); + + virtual void handle(Client &client) = 0; +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/handlers/filespagehandler.cpp b/backends/networking/sdl_net/handlers/filespagehandler.cpp new file mode 100644 index 0000000000..e117b4922c --- /dev/null +++ b/backends/networking/sdl_net/handlers/filespagehandler.cpp @@ -0,0 +1,237 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/networking/sdl_net/handlers/filespagehandler.h" +#include "backends/networking/sdl_net/handlerutils.h" +#include "backends/networking/sdl_net/localwebserver.h" +#include "common/config-manager.h" +#include "common/translation.h" + +namespace Networking { + +#define INDEX_PAGE_NAME ".index.html" +#define FILES_PAGE_NAME ".files.html" + +FilesPageHandler::FilesPageHandler() {} + +FilesPageHandler::~FilesPageHandler() {} + +namespace { +Common::String encodeDoubleQuotes(Common::String s) { + Common::String result = ""; + for (uint32 i = 0; i < s.size(); ++i) + if (s[i] == '"') { + result += "\\\""; + } else { + result += s[i]; + } + return result; +} + +Common::String encodeHtmlEntities(Common::String s) { + Common::String result = ""; + for (uint32 i = 0; i < s.size(); ++i) + if (s[i] == '<') + result += "<"; + else if (s[i] == '>') + result += ">"; + else if (s[i] == '&') + result += "&"; + else if (s[i] > 0x7F) + result += Common::String::format("&#%d;", (int)s[i]); + else result += s[i]; + return result; +} + +Common::String getDisplayPath(Common::String s) { + Common::String result = ""; + for (uint32 i = 0; i < s.size(); ++i) + if (s[i] == '\\') + result += '/'; + else + result += s[i]; + if (result == "") + return "/"; + return result; +} +} + +bool FilesPageHandler::listDirectory(Common::String path, Common::String &content, const Common::String &itemTemplate) { + if (path == "" || path == "/") { + if (ConfMan.hasKey("rootpath", "cloud")) + addItem(content, itemTemplate, IT_DIRECTORY, "/root/", _("File system root")); + addItem(content, itemTemplate, IT_DIRECTORY, "/saves/", _("Saved games")); + return true; + } + + if (HandlerUtils::hasForbiddenCombinations(path)) + return false; + + Common::String prefixToRemove = "", prefixToAdd = ""; + if (!transformPath(path, prefixToRemove, prefixToAdd)) + return false; + + Common::FSNode node = Common::FSNode(path); + if (path == "/") + node = node.getParent(); // absolute root + + if (!HandlerUtils::permittedPath(node.getPath())) + return false; + + if (!node.isDirectory()) + return false; + + // list directory + Common::FSList _nodeContent; + if (!node.getChildren(_nodeContent, Common::FSNode::kListAll, false)) // do not show hidden files + _nodeContent.clear(); + else + Common::sort(_nodeContent.begin(), _nodeContent.end()); + + // add parent directory link + { + Common::String filePath = path; + if (filePath.hasPrefix(prefixToRemove)) + filePath.erase(0, prefixToRemove.size()); + if (filePath == "" || filePath == "/" || filePath == "\\") + filePath = "/"; + else + filePath = parentPath(prefixToAdd + filePath); + addItem(content, itemTemplate, IT_PARENT_DIRECTORY, filePath, _("Parent directory")); + } + + // fill the content + for (Common::FSList::iterator i = _nodeContent.begin(); i != _nodeContent.end(); ++i) { + Common::String name = i->getDisplayName(); + if (i->isDirectory()) + name += "/"; + + Common::String filePath = i->getPath(); + if (filePath.hasPrefix(prefixToRemove)) + filePath.erase(0, prefixToRemove.size()); + filePath = prefixToAdd + filePath; + + addItem(content, itemTemplate, detectType(i->isDirectory(), name), filePath, name); + } + + return true; +} + +FilesPageHandler::ItemType FilesPageHandler::detectType(bool isDirectory, const Common::String &name) { + if (isDirectory) + return IT_DIRECTORY; + if (name.hasSuffix(".txt")) + return IT_TXT; + if (name.hasSuffix(".zip")) + return IT_ZIP; + if (name.hasSuffix(".7z")) + return IT_7Z; + return IT_UNKNOWN; +} + +void FilesPageHandler::addItem(Common::String &content, const Common::String &itemTemplate, ItemType itemType, Common::String path, Common::String name, Common::String size) const { + Common::String item = itemTemplate, icon; + bool isDirectory = (itemType == IT_DIRECTORY || itemType == IT_PARENT_DIRECTORY); + switch (itemType) { + case IT_DIRECTORY: + icon = "dir.png"; + break; + case IT_PARENT_DIRECTORY: + icon = "up.png"; + break; + case IT_TXT: + icon = "txt.png"; + break; + case IT_ZIP: + icon = "zip.png"; + break; + case IT_7Z: + icon = "7z.png"; + break; + default: + icon = "unk.png"; + } + replace(item, "{icon}", icon); + replace(item, "{link}", (isDirectory ? "files?path=" : "download?path=") + LocalWebserver::urlEncodeQueryParameterValue(path)); + replace(item, "{name}", encodeHtmlEntities(name)); + replace(item, "{size}", size); + content += item; +} + +/// public + +void FilesPageHandler::handle(Client &client) { + Common::String response = + "<html>" \ + "<head><title>ScummVM</title></head>" \ + "<body>" \ + "<p>{create_directory_desc}</p>" \ + "<form action=\"create\">" \ + "<input type=\"hidden\" name=\"path\" value=\"{path}\"/>" \ + "<input type=\"text\" name=\"directory_name\" value=\"\"/>" \ + "<input type=\"submit\" value=\"{create_directory_button}\"/>" \ + "</form>" \ + "<hr/>" \ + "<p>{upload_file_desc}</p>" \ + "<form action=\"upload?path={path}\" method=\"post\" enctype=\"multipart/form-data\">" \ + "<input type=\"file\" name=\"upload_file-f\" allowdirs multiple/>" \ + "<span>{or_upload_directory_desc}</span>" \ + "<input type=\"file\" name=\"upload_file-d\" directory webkitdirectory multiple/>" \ + "<input type=\"submit\" value=\"{upload_file_button}\"/>" \ + "</form>" + "<hr/>" \ + "<h1>{index_of_directory}</h1>" \ + "<table>{content}</table>" \ + "</body>" \ + "</html>"; + Common::String itemTemplate = "<tr><td><img src=\"icons/{icon}\"/></td><td><a href=\"{link}\">{name}</a></td><td>{size}</td></tr>\n"; //TODO: load this template too? + + // load stylish response page from the archive + Common::SeekableReadStream *const stream = HandlerUtils::getArchiveFile(FILES_PAGE_NAME); + if (stream) + response = HandlerUtils::readEverythingFromStream(stream); + + Common::String path = client.queryParameter("path"); + Common::String content = ""; + + // show an error message if failed to list directory + if (!listDirectory(path, content, itemTemplate)) { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("ScummVM couldn't list the directory you specified.")); + return; + } + + //these occur twice: + replace(response, "{create_directory_button}", _("Create directory")); + replace(response, "{create_directory_button}", _("Create directory")); + replace(response, "{path}", encodeDoubleQuotes(client.queryParameter("path"))); + replace(response, "{path}", encodeDoubleQuotes(client.queryParameter("path"))); + replace(response, "{upload_files_button}", _("Upload files")); //tab + replace(response, "{upload_file_button}", _("Upload files")); //button in the tab + replace(response, "{create_directory_desc}", _("Type new directory name:")); + replace(response, "{upload_file_desc}", _("Select a file to upload:")); + replace(response, "{or_upload_directory_desc}", _("Or select a directory (works in Chrome only):")); + replace(response, "{index_of_directory}", Common::String::format(_("Index of %s"), encodeHtmlEntities(getDisplayPath(client.queryParameter("path"))).c_str())); + replace(response, "{content}", content); + LocalWebserver::setClientGetHandler(client, response); +} + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/handlers/filespagehandler.h b/backends/networking/sdl_net/handlers/filespagehandler.h new file mode 100644 index 0000000000..e404036cb6 --- /dev/null +++ b/backends/networking/sdl_net/handlers/filespagehandler.h @@ -0,0 +1,62 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_NETWORKING_SDL_NET_FILESPAGEHANDLER_H +#define BACKENDS_NETWORKING_SDL_NET_FILESPAGEHANDLER_H + +#include "backends/networking/sdl_net/handlers/filesbasehandler.h" + +namespace Networking { + +class FilesPageHandler: public FilesBaseHandler { + enum ItemType { + IT_DIRECTORY, + IT_PARENT_DIRECTORY, + IT_TXT, + IT_ZIP, + IT_7Z, + IT_UNKNOWN + }; + + /** + * Lists the directory <path>. + * + * Returns true on success. + */ + bool listDirectory(Common::String path, Common::String &content, const Common::String &itemTemplate); + + /** Helper method for detecting items' type. */ + static ItemType detectType(bool isDirectory, const Common::String &name); + + /** Helper method for adding items into the files list. */ + void addItem(Common::String &content, const Common::String &itemTemplate, ItemType itemType, Common::String path, Common::String name, Common::String size = "") const; + +public: + FilesPageHandler(); + virtual ~FilesPageHandler(); + + virtual void handle(Client &client); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/handlers/indexpagehandler.cpp b/backends/networking/sdl_net/handlers/indexpagehandler.cpp new file mode 100644 index 0000000000..985bd6635e --- /dev/null +++ b/backends/networking/sdl_net/handlers/indexpagehandler.cpp @@ -0,0 +1,65 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/networking/sdl_net/handlers/indexpagehandler.h" +#include "backends/networking/sdl_net/handlerutils.h" +#include "backends/networking/sdl_net/localwebserver.h" +#include "common/translation.h" +#include "gui/storagewizarddialog.h" + +namespace Networking { + +IndexPageHandler::IndexPageHandler(): CommandSender(nullptr) {} + +IndexPageHandler::~IndexPageHandler() {} + +/// public + +Common::String IndexPageHandler::code() const { return _code; } + +void IndexPageHandler::handle(Client &client) { + Common::String code = client.queryParameter("code"); + + if (code == "") { + // redirect to "/filesAJAX" + HandlerUtils::setMessageHandler( + client, + Common::String::format( + "%s<br/><a href=\"files\">%s</a>", + _("This is a local webserver index page."), + _("Open Files manager") + ), + "/filesAJAX" + ); + return; + } + + _code = code; + sendCommand(GUI::kStorageCodePassedCmd, 0); + HandlerUtils::setMessageHandler(client, _("ScummVM got the code and already connects to your cloud storage!")); +} + +bool IndexPageHandler::minimalModeSupported() { + return true; +} + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/handlers/indexpagehandler.h b/backends/networking/sdl_net/handlers/indexpagehandler.h new file mode 100644 index 0000000000..8065954b27 --- /dev/null +++ b/backends/networking/sdl_net/handlers/indexpagehandler.h @@ -0,0 +1,45 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_NETWORKING_SDL_NET_INDEXPAGEHANDLER_H +#define BACKENDS_NETWORKING_SDL_NET_INDEXPAGEHANDLER_H + +#include "backends/networking/sdl_net/handlers/basehandler.h" +#include "gui/object.h" + +namespace Networking { +class LocalWebserver; + +class IndexPageHandler: public BaseHandler, public GUI::CommandSender { + Common::String _code; +public: + IndexPageHandler(); + virtual ~IndexPageHandler(); + + Common::String code() const; + virtual void handle(Client &client); + virtual bool minimalModeSupported(); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/handlers/listajaxhandler.cpp b/backends/networking/sdl_net/handlers/listajaxhandler.cpp new file mode 100644 index 0000000000..f94b674a3c --- /dev/null +++ b/backends/networking/sdl_net/handlers/listajaxhandler.cpp @@ -0,0 +1,157 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/networking/sdl_net/handlers/listajaxhandler.h" +#include "backends/networking/sdl_net/handlerutils.h" +#include "backends/networking/sdl_net/localwebserver.h" +#include "common/config-manager.h" +#include "common/json.h" +#include "common/translation.h" + +namespace Networking { + +ListAjaxHandler::ListAjaxHandler() {} + +ListAjaxHandler::~ListAjaxHandler() {} + +Common::JSONObject ListAjaxHandler::listDirectory(Common::String path) { + Common::JSONArray itemsList; + Common::JSONObject errorResult; + Common::JSONObject successResult; + successResult.setVal("type", new Common::JSONValue("success")); + errorResult.setVal("type", new Common::JSONValue("error")); + + if (path == "" || path == "/") { + if (ConfMan.hasKey("rootpath", "cloud")) + addItem(itemsList, IT_DIRECTORY, "/root/", _("File system root")); + addItem(itemsList, IT_DIRECTORY, "/saves/", _("Saved games")); + successResult.setVal("items", new Common::JSONValue(itemsList)); + return successResult; + } + + if (HandlerUtils::hasForbiddenCombinations(path)) + return errorResult; + + Common::String prefixToRemove = "", prefixToAdd = ""; + if (!transformPath(path, prefixToRemove, prefixToAdd)) + return errorResult; + + Common::FSNode node = Common::FSNode(path); + if (path == "/") + node = node.getParent(); // absolute root + + if (!HandlerUtils::permittedPath(node.getPath())) + return errorResult; + + if (!node.isDirectory()) + return errorResult; + + // list directory + Common::FSList _nodeContent; + if (!node.getChildren(_nodeContent, Common::FSNode::kListAll, false)) // do not show hidden files + _nodeContent.clear(); + else + Common::sort(_nodeContent.begin(), _nodeContent.end()); + + // add parent directory link + { + Common::String filePath = path; + if (filePath.hasPrefix(prefixToRemove)) + filePath.erase(0, prefixToRemove.size()); + if (filePath == "" || filePath == "/" || filePath == "\\") + filePath = "/"; + else + filePath = parentPath(prefixToAdd + filePath); + addItem(itemsList, IT_PARENT_DIRECTORY, filePath, _("Parent directory")); + } + + // fill the content + for (Common::FSList::iterator i = _nodeContent.begin(); i != _nodeContent.end(); ++i) { + Common::String name = i->getDisplayName(); + if (i->isDirectory()) name += "/"; + + Common::String filePath = i->getPath(); + if (filePath.hasPrefix(prefixToRemove)) + filePath.erase(0, prefixToRemove.size()); + filePath = prefixToAdd + filePath; + + addItem(itemsList, detectType(i->isDirectory(), name), filePath, name); + } + + successResult.setVal("items", new Common::JSONValue(itemsList)); + return successResult; +} + +ListAjaxHandler::ItemType ListAjaxHandler::detectType(bool isDirectory, const Common::String &name) { + if (isDirectory) + return IT_DIRECTORY; + if (name.hasSuffix(".txt")) + return IT_TXT; + if (name.hasSuffix(".zip")) + return IT_ZIP; + if (name.hasSuffix(".7z")) + return IT_7Z; + return IT_UNKNOWN; +} + +void ListAjaxHandler::addItem(Common::JSONArray &responseItemsList, ItemType itemType, Common::String path, Common::String name, Common::String size) { + Common::String icon; + bool isDirectory = (itemType == IT_DIRECTORY || itemType == IT_PARENT_DIRECTORY); + switch (itemType) { + case IT_DIRECTORY: + icon = "dir.png"; + break; + case IT_PARENT_DIRECTORY: + icon = "up.png"; + break; + case IT_TXT: + icon = "txt.png"; + break; + case IT_ZIP: + icon = "zip.png"; + break; + case IT_7Z: + icon = "7z.png"; + break; + default: + icon = "unk.png"; + } + + Common::JSONObject item; + item.setVal("name", new Common::JSONValue(name)); + item.setVal("path", new Common::JSONValue(path)); + item.setVal("isDirectory", new Common::JSONValue(isDirectory)); + item.setVal("size", new Common::JSONValue(size)); + item.setVal("icon", new Common::JSONValue(icon)); + responseItemsList.push_back(new Common::JSONValue(item)); +} + +/// public + +void ListAjaxHandler::handle(Client &client) { + Common::String path = client.queryParameter("path"); + Common::JSONValue jsonResponse = listDirectory(path); + Common::String response = jsonResponse.stringify(true); + LocalWebserver::setClientGetHandler(client, response); +} + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/handlers/listajaxhandler.h b/backends/networking/sdl_net/handlers/listajaxhandler.h new file mode 100644 index 0000000000..40840ad6c3 --- /dev/null +++ b/backends/networking/sdl_net/handlers/listajaxhandler.h @@ -0,0 +1,63 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_NETWORKING_SDL_NET_LISTAJAXHANDLER_H +#define BACKENDS_NETWORKING_SDL_NET_LISTAJAXHANDLER_H + +#include "backends/networking/sdl_net/handlers/filesbasehandler.h" +#include "common/json.h" + +namespace Networking { + +class ListAjaxHandler: public FilesBaseHandler { + enum ItemType { + IT_DIRECTORY, + IT_PARENT_DIRECTORY, + IT_TXT, + IT_ZIP, + IT_7Z, + IT_UNKNOWN + }; + + /** + * Lists the directory <path>. + * + * Returns JSON with either listed directory or error response. + */ + Common::JSONObject listDirectory(Common::String path); + + /** Helper method for detecting items' type. */ + static ItemType detectType(bool isDirectory, const Common::String &name); + + /** Helper method for adding items into the files list. */ + static void addItem(Common::JSONArray &responseItemsList, ItemType itemType, Common::String path, Common::String name, Common::String size = ""); + +public: + ListAjaxHandler(); + virtual ~ListAjaxHandler(); + + virtual void handle(Client &client); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/handlers/resourcehandler.cpp b/backends/networking/sdl_net/handlers/resourcehandler.cpp new file mode 100644 index 0000000000..631eb63351 --- /dev/null +++ b/backends/networking/sdl_net/handlers/resourcehandler.cpp @@ -0,0 +1,75 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/networking/sdl_net/handlers/resourcehandler.h" +#include "backends/networking/sdl_net/handlerutils.h" +#include "backends/networking/sdl_net/localwebserver.h" + +namespace Networking { + +ResourceHandler::ResourceHandler() {} + +ResourceHandler::~ResourceHandler() {} + +const char *ResourceHandler::determineMimeType(Common::String &filename) { + // text + if (filename.hasSuffix(".html")) return "text/html"; + if (filename.hasSuffix(".css")) return "text/css"; + if (filename.hasSuffix(".txt")) return "text/plain"; + if (filename.hasSuffix(".js")) return "application/javascript"; + + // images + if (filename.hasSuffix(".jpeg") || filename.hasSuffix(".jpg") || filename.hasSuffix(".jpe")) return "image/jpeg"; + if (filename.hasSuffix(".gif")) return "image/gif"; + if (filename.hasSuffix(".png")) return "image/png"; + if (filename.hasSuffix(".svg")) return "image/svg+xml"; + if (filename.hasSuffix(".tiff")) return "image/tiff"; + if (filename.hasSuffix(".ico")) return "image/vnd.microsoft.icon"; + if (filename.hasSuffix(".wbmp")) return "image/vnd.wap.wbmp"; + + if (filename.hasSuffix(".zip")) return "application/zip"; + return "application/octet-stream"; +} + +/// public + +void ResourceHandler::handle(Client &client) { + Common::String filename = client.path(); + filename.deleteChar(0); + + // if archive hidden file is requested, ignore + if (filename.size() && filename[0] == '.') + return; + + // if file not found, don't set handler either + Common::SeekableReadStream *file = HandlerUtils::getArchiveFile(filename); + if (file == nullptr) + return; + + LocalWebserver::setClientGetHandler(client, file, 200, determineMimeType(filename)); +} + +bool ResourceHandler::minimalModeSupported() { + return true; +} + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/handlers/resourcehandler.h b/backends/networking/sdl_net/handlers/resourcehandler.h new file mode 100644 index 0000000000..2ec4c5bb19 --- /dev/null +++ b/backends/networking/sdl_net/handlers/resourcehandler.h @@ -0,0 +1,42 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_NETWORKING_SDL_NET_RESOURCEHANDLER_H +#define BACKENDS_NETWORKING_SDL_NET_RESOURCEHANDLER_H + +#include "backends/networking/sdl_net/handlers/basehandler.h" + +namespace Networking { + +class ResourceHandler: public BaseHandler { + static const char *determineMimeType(Common::String &filename); +public: + ResourceHandler(); + virtual ~ResourceHandler(); + + virtual void handle(Client &client); + virtual bool minimalModeSupported(); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/handlers/uploadfilehandler.cpp b/backends/networking/sdl_net/handlers/uploadfilehandler.cpp new file mode 100644 index 0000000000..a0e992c25e --- /dev/null +++ b/backends/networking/sdl_net/handlers/uploadfilehandler.cpp @@ -0,0 +1,79 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/networking/sdl_net/handlers/uploadfilehandler.h" +#include "backends/networking/sdl_net/handlerutils.h" +#include "backends/networking/sdl_net/uploadfileclienthandler.h" +#include "backends/fs/fs-factory.h" +#include "common/system.h" +#include "common/translation.h" + +namespace Networking { + +UploadFileHandler::UploadFileHandler() {} + +UploadFileHandler::~UploadFileHandler() {} + +/// public + +void UploadFileHandler::handle(Client &client) { + Common::String path = client.queryParameter("path"); + + // check that <path> is not an absolute root + if (path == "" || path == "/" || path == "\\") { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("Invalid path!")); + return; + } + + // check that <path> contains no '../' + if (HandlerUtils::hasForbiddenCombinations(path)) { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("Invalid path!")); + return; + } + + // transform virtual path to actual file system one + Common::String prefixToRemove = "", prefixToAdd = ""; + if (!transformPath(path, prefixToRemove, prefixToAdd, false) || path.empty()) { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("Invalid path!")); + return; + } + + // check that <path> exists, is directory and isn't forbidden + AbstractFSNode *node = g_system->getFilesystemFactory()->makeFileNodePath(path); + if (!HandlerUtils::permittedPath(node->getPath())) { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("Invalid path!")); + return; + } + if (!node->exists()) { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("The parent directory doesn't exist!")); + return; + } + if (!node->isDirectory()) { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("Can't upload into a file!")); + return; + } + + // if all OK, set special handler + client.setHandler(new UploadFileClientHandler(path)); +} + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/handlers/uploadfilehandler.h b/backends/networking/sdl_net/handlers/uploadfilehandler.h new file mode 100644 index 0000000000..cbff215156 --- /dev/null +++ b/backends/networking/sdl_net/handlers/uploadfilehandler.h @@ -0,0 +1,40 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_NETWORKING_SDL_NET_UPLOADFILEHANDLER_H +#define BACKENDS_NETWORKING_SDL_NET_UPLOADFILEHANDLER_H + +#include "backends/networking/sdl_net/handlers/filesbasehandler.h" + +namespace Networking { + +class UploadFileHandler: public FilesBaseHandler { +public: + UploadFileHandler(); + virtual ~UploadFileHandler(); + + virtual void handle(Client &client); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/handlerutils.cpp b/backends/networking/sdl_net/handlerutils.cpp new file mode 100644 index 0000000000..dc21ab5ce1 --- /dev/null +++ b/backends/networking/sdl_net/handlerutils.cpp @@ -0,0 +1,200 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/networking/sdl_net/handlerutils.h" +#include "backends/networking/sdl_net/localwebserver.h" +#include "backends/saves/default/default-saves.h" +#include "common/archive.h" +#include "common/config-manager.h" +#include "common/file.h" +#include "common/translation.h" +#include "common/unzip.h" + +namespace Networking { + +#define ARCHIVE_NAME "wwwroot.zip" + +#define INDEX_PAGE_NAME ".index.html" + +Common::Archive *HandlerUtils::getZipArchive() { + // first search in themepath + if (ConfMan.hasKey("themepath")) { + const Common::FSNode &node = Common::FSNode(ConfMan.get("themepath")); + if (!node.exists() || !node.isReadable() || !node.isDirectory()) + return nullptr; + + Common::FSNode fileNode = node.getChild(ARCHIVE_NAME); + if (fileNode.exists() && fileNode.isReadable() && !fileNode.isDirectory()) { + Common::SeekableReadStream *const stream = fileNode.createReadStream(); + Common::Archive *zipArchive = Common::makeZipArchive(stream); + if (zipArchive) + return zipArchive; + } + } + + // then use SearchMan to find it + Common::ArchiveMemberList fileList; + SearchMan.listMatchingMembers(fileList, ARCHIVE_NAME); + for (Common::ArchiveMemberList::iterator it = fileList.begin(); it != fileList.end(); ++it) { + Common::ArchiveMember const &m = **it; + Common::SeekableReadStream *const stream = m.createReadStream(); + Common::Archive *zipArchive = Common::makeZipArchive(stream); + if (zipArchive) + return zipArchive; + } + + return nullptr; +} + +Common::ArchiveMemberList HandlerUtils::listArchive() { + Common::ArchiveMemberList resultList; + Common::Archive *zipArchive = getZipArchive(); + if (zipArchive) { + zipArchive->listMembers(resultList); + delete zipArchive; + } + return resultList; +} + +Common::SeekableReadStream *HandlerUtils::getArchiveFile(Common::String name) { + Common::SeekableReadStream *result = nullptr; + Common::Archive *zipArchive = getZipArchive(); + if (zipArchive) { + const Common::ArchiveMemberPtr ptr = zipArchive->getMember(name); + if (ptr.get() == nullptr) + return nullptr; + result = ptr->createReadStream(); + delete zipArchive; + } + return result; +} + +Common::String HandlerUtils::readEverythingFromStream(Common::SeekableReadStream *const stream) { + Common::String result; + char buf[1024]; + uint32 readBytes; + while (!stream->eos()) { + readBytes = stream->read(buf, 1024); + result += Common::String(buf, readBytes); + } + return result; +} + +Common::String HandlerUtils::normalizePath(const Common::String &path) { + Common::String normalized; + bool slash = false; + for (uint32 i = 0; i < path.size(); ++i) { + char c = path[i]; + if (c == '\\' || c == '/') { + slash = true; + continue; + } + + if (slash) { + normalized += '/'; + slash = false; + } + + if ('A' <= c && c <= 'Z') { + normalized += c - 'A' + 'a'; + } else { + normalized += c; + } + } + if (slash) normalized += '/'; + return normalized; +} + +bool HandlerUtils::hasForbiddenCombinations(const Common::String &path) { + return (path.contains("/../") || path.contains("\\..\\") || path.contains("\\../") || path.contains("/..\\")); +} + +bool HandlerUtils::isBlacklisted(const Common::String &path) { + const char *blacklist[] = { + "/etc", + "/bin", + "c:/windows" // just saying: I know guys who install windows on another drives + }; + + // normalize path + Common::String normalized = normalizePath(path); + + uint32 size = sizeof(blacklist) / sizeof(const char *); + for (uint32 i = 0; i < size; ++i) + if (normalized.hasPrefix(blacklist[i])) + return true; + + return false; +} + +bool HandlerUtils::hasPermittedPrefix(const Common::String &path) { + // normalize path + Common::String normalized = normalizePath(path); + + // prefix for /root/ + Common::String prefix; + if (ConfMan.hasKey("rootpath", "cloud")) { + prefix = normalizePath(ConfMan.get("rootpath", "cloud")); + if (prefix == "/" || normalized.hasPrefix(prefix)) + return true; + } + + // prefix for /saves/ + DefaultSaveFileManager *manager = dynamic_cast<DefaultSaveFileManager *>(g_system->getSavefileManager()); + prefix = (manager ? manager->concatWithSavesPath("") : ConfMan.get("savepath")); + return (normalized.hasPrefix(normalizePath(prefix))); +} + +bool HandlerUtils::permittedPath(const Common::String path) { + return hasPermittedPrefix(path) && !isBlacklisted(path); +} + +void HandlerUtils::setMessageHandler(Client &client, Common::String message, Common::String redirectTo) { + Common::String response = "<html><head><title>ScummVM</title></head><body>{message}</body></html>"; + + // load stylish response page from the archive + Common::SeekableReadStream *const stream = getArchiveFile(INDEX_PAGE_NAME); + if (stream) + response = readEverythingFromStream(stream); + + replace(response, "{message}", message); + if (redirectTo.empty()) + LocalWebserver::setClientGetHandler(client, response); + else + LocalWebserver::setClientRedirectHandler(client, response, redirectTo); +} + +void HandlerUtils::setFilesManagerErrorMessageHandler(Client &client, Common::String message, Common::String redirectTo) { + setMessageHandler( + client, + Common::String::format( + "%s<br/><a href=\"files%s?path=%s\">%s</a>", + message.c_str(), + client.queryParameter("ajax") == "true" ? "AJAX" : "", + "%2F", //that's encoded "/" + _("Back to the files manager") + ), + redirectTo + ); +} + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/handlerutils.h b/backends/networking/sdl_net/handlerutils.h new file mode 100644 index 0000000000..4c2eff49b6 --- /dev/null +++ b/backends/networking/sdl_net/handlerutils.h @@ -0,0 +1,50 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_NETWORKING_SDL_NET_HANDLERUTILS_H +#define BACKENDS_NETWORKING_SDL_NET_HANDLERUTILS_H + +#include "backends/networking/sdl_net/client.h" +#include "common/archive.h" + +namespace Networking { + +class HandlerUtils { +public: + static Common::Archive *getZipArchive(); + static Common::ArchiveMemberList listArchive(); + static Common::SeekableReadStream *getArchiveFile(Common::String name); + static Common::String readEverythingFromStream(Common::SeekableReadStream *const stream); + + static Common::String normalizePath(const Common::String &path); + static bool hasForbiddenCombinations(const Common::String &path); + static bool isBlacklisted(const Common::String &path); + static bool hasPermittedPrefix(const Common::String &path); + static bool permittedPath(const Common::String path); + + static void setMessageHandler(Client &client, Common::String message, Common::String redirectTo = ""); + static void setFilesManagerErrorMessageHandler(Client &client, Common::String message, Common::String redirectTo = ""); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/localwebserver.cpp b/backends/networking/sdl_net/localwebserver.cpp new file mode 100644 index 0000000000..6557c7a88d --- /dev/null +++ b/backends/networking/sdl_net/localwebserver.cpp @@ -0,0 +1,446 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#define FORBIDDEN_SYMBOL_ALLOW_ALL + +#include "backends/networking/sdl_net/localwebserver.h" +#include "backends/networking/sdl_net/getclienthandler.h" +#include "common/memstream.h" +#include "common/str.h" +#include "common/system.h" +#include "common/timer.h" +#include "common/translation.h" +#include <SDL/SDL_net.h> +#include <common/config-manager.h> + +#ifdef POSIX +#include <sys/types.h> +#include <ifaddrs.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#endif + +namespace Common { +class MemoryReadWriteStream; + +DECLARE_SINGLETON(Networking::LocalWebserver); + +} + +namespace Networking { + +LocalWebserver::LocalWebserver(): _set(nullptr), _serverSocket(nullptr), _timerStarted(false), + _stopOnIdle(false), _minimalMode(false), _clients(0), _idlingFrames(0), _serverPort(DEFAULT_SERVER_PORT) { + addPathHandler("/", &_indexPageHandler); + addPathHandler("/files", &_filesPageHandler); + addPathHandler("/create", &_createDirectoryHandler); + addPathHandler("/download", &_downloadFileHandler); + addPathHandler("/upload", &_uploadFileHandler); + addPathHandler("/list", &_listAjaxHandler); + addPathHandler("/filesAJAX", &_filesAjaxPageHandler); + _defaultHandler = &_resourceHandler; +} + +LocalWebserver::~LocalWebserver() { + stop(); +} + +void localWebserverTimer(void *ignored) { + LocalServer.handle(); +} + +void LocalWebserver::startTimer(int interval) { + Common::TimerManager *manager = g_system->getTimerManager(); + if (manager->installTimerProc(localWebserverTimer, interval, 0, "Networking::LocalWebserver's Timer")) { + _timerStarted = true; + } else { + warning("Failed to install Networking::LocalWebserver's timer"); + } +} + +void LocalWebserver::stopTimer() { + Common::TimerManager *manager = g_system->getTimerManager(); + manager->removeTimerProc(localWebserverTimer); + _timerStarted = false; +} + +void LocalWebserver::start(bool useMinimalMode) { + _handleMutex.lock(); + _serverPort = getPort(); + _stopOnIdle = false; + if (_timerStarted) { + _handleMutex.unlock(); + return; + } + _minimalMode = useMinimalMode; + startTimer(); + + // Create a listening TCP socket + IPaddress ip; + if (SDLNet_ResolveHost(&ip, NULL, _serverPort) == -1) { + error("LocalWebserver: SDLNet_ResolveHost: %s\n", SDLNet_GetError()); + } + + resolveAddress(&ip); + + _serverSocket = SDLNet_TCP_Open(&ip); + if (!_serverSocket) { + warning("LocalWebserver: SDLNet_TCP_Open: %s", SDLNet_GetError()); + stopTimer(); + g_system->displayMessageOnOSD(_("Failed to start local webserver.\nCheck whether selected port is not used by another application and try again.")); + _handleMutex.unlock(); + return; + } + + // Create a socket set + _set = SDLNet_AllocSocketSet(MAX_CONNECTIONS + 1); //one more for our server socket + if (!_set) { + error("LocalWebserver: SDLNet_AllocSocketSet: %s\n", SDLNet_GetError()); + } + + int numused = SDLNet_TCP_AddSocket(_set, _serverSocket); + if (numused == -1) { + error("LocalWebserver: SDLNet_AddSocket: %s\n", SDLNet_GetError()); + } + _handleMutex.unlock(); +} + +void LocalWebserver::stop() { + _handleMutex.lock(); + if (_timerStarted) + stopTimer(); + + if (_serverSocket) { + SDLNet_TCP_Close(_serverSocket); + _serverSocket = nullptr; + } + + for (uint32 i = 0; i < MAX_CONNECTIONS; ++i) + _client[i].close(); + + _clients = 0; + + if (_set) { + SDLNet_FreeSocketSet(_set); + _set = nullptr; + } + _handleMutex.unlock(); +} + +void LocalWebserver::stopOnIdle() { _stopOnIdle = true; } + +void LocalWebserver::addPathHandler(Common::String path, BaseHandler *handler) { + if (_pathHandlers.contains(path)) + warning("LocalWebserver::addPathHandler: path already had a handler"); + _pathHandlers[path] = handler; +} + +Common::String LocalWebserver::getAddress() { return _address; } + +IndexPageHandler &LocalWebserver::indexPageHandler() { return _indexPageHandler; } + +bool LocalWebserver::isRunning() { + bool result = false; + _handleMutex.lock(); + result = _timerStarted; + _handleMutex.unlock(); + return result; +} + +uint32 LocalWebserver::getPort() { +#ifdef NETWORKING_LOCALWEBSERVER_ENABLE_PORT_OVERRIDE + if (ConfMan.hasKey("local_server_port")) + return ConfMan.getInt("local_server_port"); +#endif + return DEFAULT_SERVER_PORT; +} + +void LocalWebserver::handle() { + _handleMutex.lock(); + int numready = SDLNet_CheckSockets(_set, 0); + if (numready == -1) { + error("LocalWebserver: SDLNet_CheckSockets: %s\n", SDLNet_GetError()); + } else if (numready) { + acceptClient(); + } + + for (uint32 i = 0; i < MAX_CONNECTIONS; ++i) + handleClient(i); + + _clients = 0; + for (uint32 i = 0; i < MAX_CONNECTIONS; ++i) + if (_client[i].state() != INVALID) + ++_clients; + + if (_clients == 0) + ++_idlingFrames; + else + _idlingFrames = 0; + + if (_idlingFrames > FRAMES_PER_SECOND && _stopOnIdle) { + _handleMutex.unlock(); + stop(); + return; + } + + _handleMutex.unlock(); +} + +void LocalWebserver::handleClient(uint32 i) { + switch (_client[i].state()) { + case INVALID: + return; + case READING_HEADERS: + _client[i].readHeaders(); + break; + case READ_HEADERS: { + // decide what to do next with that client + // check whether we know a handler for such URL + BaseHandler *handler = nullptr; + if (_pathHandlers.contains(_client[i].path())) { + handler = _pathHandlers[_client[i].path()]; + } else { + // try default handler + handler = _defaultHandler; + } + + // if server's in "minimal mode", only handlers which support it are used + if (handler && (!_minimalMode || handler->minimalModeSupported())) + handler->handle(_client[i]); + + if (_client[i].state() == BEING_HANDLED || _client[i].state() == INVALID) + break; + + // if no handler, answer with default BAD REQUEST + // fallthrough + } + + case BAD_REQUEST: + setClientGetHandler(_client[i], "<html><head><title>ScummVM - Bad Request</title></head><body>BAD REQUEST</body></html>", 400); + break; + case BEING_HANDLED: + _client[i].handle(); + break; + } +} + +void LocalWebserver::acceptClient() { + if (!SDLNet_SocketReady(_serverSocket)) + return; + + TCPsocket client = SDLNet_TCP_Accept(_serverSocket); + if (!client) + return; + + if (_clients == MAX_CONNECTIONS) { //drop the connection + SDLNet_TCP_Close(client); + return; + } + + ++_clients; + for (uint32 i = 0; i < MAX_CONNECTIONS; ++i) + if (_client[i].state() == INVALID) { + _client[i].open(_set, client); + break; + } +} + +void LocalWebserver::resolveAddress(void *ipAddress) { + IPaddress *ip = (IPaddress *)ipAddress; + + // not resolved + _address = Common::String::format("http://127.0.0.1:%u/ (unresolved)", _serverPort); + + // default way (might work everywhere, surely works on Windows) + const char *name = SDLNet_ResolveIP(ip); + if (name == NULL) { + warning("LocalWebserver: SDLNet_ResolveHost: %s\n", SDLNet_GetError()); + } else { + IPaddress localIp; + if (SDLNet_ResolveHost(&localIp, name, _serverPort) == -1) { + warning("LocalWebserver: SDLNet_ResolveHost: %s\n", SDLNet_GetError()); + } else { + _address = Common::String::format( + "http://%u.%u.%u.%u:%u/", + localIp.host & 0xFF, (localIp.host >> 8) & 0xFF, (localIp.host >> 16) & 0xFF, (localIp.host >> 24) & 0xFF, + _serverPort + ); + } + } + + // check that our trick worked + if (_address.contains("/127.0.0.1:") || _address.contains("localhost") || _address.contains("/0.0.0.0:")) + warning("LocalWebserver: Failed to resolve IP with the default way"); + else + return; + + // if not - try platform-specific +#ifdef POSIX + struct ifaddrs *ifAddrStruct = NULL; + void *tmpAddrPtr = NULL; + + getifaddrs(&ifAddrStruct); + + for (struct ifaddrs *i = ifAddrStruct; i != NULL; i = i->ifa_next) { + if (!i->ifa_addr) { + continue; + } + + Common::String addr; + + // IPv4 + if (i->ifa_addr->sa_family == AF_INET) { + tmpAddrPtr = &((struct sockaddr_in *)i->ifa_addr)->sin_addr; + char addressBuffer[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, tmpAddrPtr, addressBuffer, INET_ADDRSTRLEN); + debug(9, "%s IP Address %s", i->ifa_name, addressBuffer); + addr = addressBuffer; + } + + // IPv6 + /* + if (i->ifa_addr->sa_family == AF_INET6) { + tmpAddrPtr = &((struct sockaddr_in6 *)i->ifa_addr)->sin6_addr; + char addressBuffer[INET6_ADDRSTRLEN]; + inet_ntop(AF_INET6, tmpAddrPtr, addressBuffer, INET6_ADDRSTRLEN); + debug(9, "%s IP Address %s", i->ifa_name, addressBuffer); + addr = addressBuffer; + } + */ + + if (addr.empty()) + continue; + + // ignored IPv4 addresses + if (addr.equals("127.0.0.1") || addr.equals("0.0.0.0") || addr.equals("localhost")) + continue; + + // ignored IPv6 addresses + /* + if (addr.equals("::1")) + continue; + */ + + // use the address found + _address = "http://" + addr + Common::String::format(":%u/", _serverPort); + } + + if (ifAddrStruct != NULL) freeifaddrs(ifAddrStruct); +#endif +} + +void LocalWebserver::setClientGetHandler(Client &client, Common::String response, long code, const char *mimeType) { + byte *data = new byte[response.size()]; + memcpy(data, response.c_str(), response.size()); + Common::MemoryReadStream *stream = new Common::MemoryReadStream(data, response.size(), DisposeAfterUse::YES); + setClientGetHandler(client, stream, code, mimeType); +} + +void LocalWebserver::setClientGetHandler(Client &client, Common::SeekableReadStream *responseStream, long code, const char *mimeType) { + GetClientHandler *handler = new GetClientHandler(responseStream); + handler->setResponseCode(code); + if (mimeType) + handler->setHeader("Content-Type", mimeType); + client.setHandler(handler); +} + +void LocalWebserver::setClientRedirectHandler(Client &client, Common::String response, Common::String location, const char *mimeType) { + byte *data = new byte[response.size()]; + memcpy(data, response.c_str(), response.size()); + Common::MemoryReadStream *stream = new Common::MemoryReadStream(data, response.size(), DisposeAfterUse::YES); + setClientRedirectHandler(client, stream, location, mimeType); +} + +void LocalWebserver::setClientRedirectHandler(Client &client, Common::SeekableReadStream *responseStream, Common::String location, const char *mimeType) { + GetClientHandler *handler = new GetClientHandler(responseStream); + handler->setResponseCode(302); //redirect + handler->setHeader("Location", location); + if (mimeType) + handler->setHeader("Content-Type", mimeType); + client.setHandler(handler); +} + +namespace { +int hexDigit(char c) { + if ('0' <= c && c <= '9') return c - '0'; + if ('A' <= c && c <= 'F') return c - 'A' + 10; + if ('a' <= c && c <= 'f') return c - 'a' + 10; + return -1; +} +} + +Common::String LocalWebserver::urlDecode(Common::String value) { + Common::String result = ""; + uint32 size = value.size(); + for (uint32 i = 0; i < size; ++i) { + if (value[i] == '+') { + result += ' '; + continue; + } + + if (value[i] == '%' && i + 2 < size) { + int d1 = hexDigit(value[i + 1]); + int d2 = hexDigit(value[i + 2]); + if (0 <= d1 && d1 < 16 && 0 <= d2 && d2 < 16) { + result += (char)(d1 * 16 + d2); + i = i + 2; + continue; + } + } + + result += value[i]; + } + return result; +} + +namespace { +bool isQueryUnreserved(char c) { + return ( + ('0' <= c && c <= '9') || + ('A' <= c && c <= 'Z') || + ('a' <= c && c <= 'z') || + c == '-' || c == '_' || c == '.' || c == '!' || + c == '~' || c == '*' || c == '\'' || c == '(' || c == ')' + ); +} +} + +Common::String LocalWebserver::urlEncodeQueryParameterValue(Common::String value) { + //OK chars = alphanum | "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")" + //reserved for query are ";", "/", "?", ":", "@", "&", "=", "+", "," + //that means these must be encoded too or otherwise they could malform the query + Common::String result = ""; + char hexChar[16] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + for (uint32 i = 0; i < value.size(); ++i) { + char c = value[i]; + if (isQueryUnreserved(c)) + result += c; + else { + result += '%'; + result += hexChar[(c >> 4) & 0xF]; + result += hexChar[c & 0xF]; + } + } + return result; +} + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/localwebserver.h b/backends/networking/sdl_net/localwebserver.h new file mode 100644 index 0000000000..c6cf8485c3 --- /dev/null +++ b/backends/networking/sdl_net/localwebserver.h @@ -0,0 +1,115 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_NETWORKING_SDL_NET_LOCALWEBSERVER_H +#define BACKENDS_NETWORKING_SDL_NET_LOCALWEBSERVER_H + +#include "backends/networking/sdl_net/client.h" +#include "backends/networking/sdl_net/handlers/basehandler.h" +#include "backends/networking/sdl_net/handlers/createdirectoryhandler.h" +#include "backends/networking/sdl_net/handlers/downloadfilehandler.h" +#include "backends/networking/sdl_net/handlers/filesajaxpagehandler.h" +#include "backends/networking/sdl_net/handlers/filespagehandler.h" +#include "backends/networking/sdl_net/handlers/indexpagehandler.h" +#include "backends/networking/sdl_net/handlers/listajaxhandler.h" +#include "backends/networking/sdl_net/handlers/resourcehandler.h" +#include "backends/networking/sdl_net/handlers/uploadfilehandler.h" +#include "common/hash-str.h" +#include "common/mutex.h" +#include "common/singleton.h" +#include "common/scummsys.h" + +namespace Common { +class SeekableReadStream; +} + +typedef struct _SDLNet_SocketSet *SDLNet_SocketSet; +typedef struct _TCPsocket *TCPsocket; + +namespace Networking { + +#define NETWORKING_LOCALWEBSERVER_ENABLE_PORT_OVERRIDE + +class LocalWebserver : public Common::Singleton<LocalWebserver> { + static const uint32 FRAMES_PER_SECOND = 20; + static const uint32 TIMER_INTERVAL = 1000000 / FRAMES_PER_SECOND; + static const uint32 MAX_CONNECTIONS = 10; + + friend void localWebserverTimer(void *); //calls handle() + + SDLNet_SocketSet _set; + TCPsocket _serverSocket; + Client _client[MAX_CONNECTIONS]; + int _clients; + bool _timerStarted, _stopOnIdle, _minimalMode; + Common::HashMap<Common::String, BaseHandler*> _pathHandlers; + BaseHandler *_defaultHandler; + IndexPageHandler _indexPageHandler; + FilesPageHandler _filesPageHandler; + CreateDirectoryHandler _createDirectoryHandler; + DownloadFileHandler _downloadFileHandler; + UploadFileHandler _uploadFileHandler; + ListAjaxHandler _listAjaxHandler; + FilesAjaxPageHandler _filesAjaxPageHandler; + ResourceHandler _resourceHandler; + uint32 _idlingFrames; + Common::Mutex _handleMutex; + Common::String _address; + uint32 _serverPort; + + void startTimer(int interval = TIMER_INTERVAL); + void stopTimer(); + void handle(); + void handleClient(uint32 i); + void acceptClient(); + void resolveAddress(void *ipAddress); + void addPathHandler(Common::String path, BaseHandler *handler); + +public: + static const uint32 DEFAULT_SERVER_PORT = 12345; + + LocalWebserver(); + virtual ~LocalWebserver(); + + void start(bool useMinimalMode = false); + void stop(); + void stopOnIdle(); + + Common::String getAddress(); + IndexPageHandler &indexPageHandler(); + bool isRunning(); + static uint32 getPort(); + + static void setClientGetHandler(Client &client, Common::String response, long code = 200, const char *mimeType = nullptr); + static void setClientGetHandler(Client &client, Common::SeekableReadStream *responseStream, long code = 200, const char *mimeType = nullptr); + static void setClientRedirectHandler(Client &client, Common::String response, Common::String location, const char *mimeType = nullptr); + static void setClientRedirectHandler(Client &client, Common::SeekableReadStream *responseStream, Common::String location, const char *mimeType = nullptr); + static Common::String urlDecode(Common::String value); + static Common::String urlEncodeQueryParameterValue(Common::String value); +}; + +/** Shortcut for accessing the local webserver. */ +#define LocalServer Networking::LocalWebserver::instance() + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/reader.cpp b/backends/networking/sdl_net/reader.cpp new file mode 100644 index 0000000000..8f3199f51c --- /dev/null +++ b/backends/networking/sdl_net/reader.cpp @@ -0,0 +1,462 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/networking/sdl_net/reader.h" +#include "backends/fs/fs-factory.h" +#include "backends/networking/sdl_net/localwebserver.h" +#include "common/memstream.h" +#include "common/stream.h" + +namespace Networking { + +Reader::Reader() { + _state = RS_NONE; + _content = nullptr; + _bytesLeft = 0; + + _window = nullptr; + _windowUsed = 0; + _windowSize = 0; + + _headersStream = nullptr; + _firstBlock = true; + + _contentLength = 0; + _availableBytes = 0; + _isBadRequest = false; + _allContentRead = false; +} + +Reader::~Reader() { + cleanup(); +} + +Reader &Reader::operator=(Reader &r) { + if (this == &r) + return *this; + cleanup(); + + _state = r._state; + _content = r._content; + _bytesLeft = r._bytesLeft; + r._state = RS_NONE; + + _window = r._window; + _windowUsed = r._windowUsed; + _windowSize = r._windowSize; + r._window = nullptr; + + _headersStream = r._headersStream; + r._headersStream = nullptr; + + _headers = r._headers; + _method = r._method; + _path = r._path; + _query = r._query; + _anchor = r._anchor; + _queryParameters = r._queryParameters; + _contentLength = r._contentLength; + _boundary = r._boundary; + _availableBytes = r._availableBytes; + _firstBlock = r._firstBlock; + _isBadRequest = r._isBadRequest; + _allContentRead = r._allContentRead; + + return *this; +} + +void Reader::cleanup() { + //_content is not to be freed, it's not owned by Reader + + if (_headersStream != nullptr) + delete _headersStream; + + if (_window != nullptr) + freeWindow(); +} + +bool Reader::readAndHandleFirstHeaders() { + Common::String boundary = "\r\n\r\n"; + if (_window == nullptr) { + makeWindow(boundary.size()); + } + if (_headersStream == nullptr) { + _headersStream = new Common::MemoryReadWriteStream(DisposeAfterUse::YES); + } + + while (readOneByteInStream(_headersStream, boundary)) { + if (_headersStream->size() > SUSPICIOUS_HEADERS_SIZE) { + _isBadRequest = true; + return true; + } + if (!bytesLeft()) + return false; + } + handleFirstHeaders(_headersStream); + + freeWindow(); + _state = RS_READING_CONTENT; + return true; +} + +bool Reader::readBlockHeadersIntoStream(Common::WriteStream *stream) { + Common::String boundary = "\r\n\r\n"; + if (_window == nullptr) makeWindow(boundary.size()); + + while (readOneByteInStream(stream, boundary)) { + if (!bytesLeft()) + return false; + } + if (stream) stream->flush(); + + freeWindow(); + _state = RS_READING_CONTENT; + return true; +} + +namespace { +void readFromThatUntilLineEnd(const char *cstr, Common::String needle, Common::String &result) { + const char *position = strstr(cstr, needle.c_str()); + + if (position) { + char c; + for (const char *i = position + needle.size(); c = *i, c != 0; ++i) { + if (c == '\n' || c == '\r') + break; + result += c; + } + } +} +} + +void Reader::handleFirstHeaders(Common::MemoryReadWriteStream *headersStream) { + if (!_boundary.empty()) { + warning("Reader: handleFirstHeaders() called when first headers were already handled"); + return; + } + + //parse method, path, query, fragment + _headers = readEverythingFromMemoryStream(headersStream); + parseFirstLine(_headers); + + //find boundary + _boundary = ""; + readFromThatUntilLineEnd(_headers.c_str(), "boundary=", _boundary); + + //find content length + Common::String contentLength = ""; + readFromThatUntilLineEnd(_headers.c_str(), "Content-Length: ", contentLength); + _contentLength = contentLength.asUint64(); + _availableBytes = _contentLength; +} + +void Reader::parseFirstLine(const Common::String &headers) { + uint32 headersSize = headers.size(); + bool bad = false; + + if (headersSize > 0) { + const char *cstr = headers.c_str(); + const char *position = strstr(cstr, "\r\n"); + if (position) { //we have at least one line - and we want the first one + //"<METHOD> <path> HTTP/<VERSION>\r\n" + Common::String method, path, http, buf; + uint32 length = position - cstr; + if (headersSize > length) + headersSize = length; + for (uint32 i = 0; i < headersSize; ++i) { + if (headers[i] != ' ') + buf += headers[i]; + if (headers[i] == ' ' || i == headersSize - 1) { + if (method == "") { + method = buf; + } else if (path == "") { + path = buf; + } else if (http == "") { + http = buf; + } else { + bad = true; + break; + } + buf = ""; + } + } + + //check that method is supported + if (method != "GET" && method != "PUT" && method != "POST") + bad = true; + + //check that HTTP/<VERSION> is OK + if (!http.hasPrefix("HTTP/")) + bad = true; + + _method = method; + parsePathQueryAndAnchor(path); + } + } + + if (bad) _isBadRequest = true; +} + +void Reader::parsePathQueryAndAnchor(Common::String path) { + //<path>[?query][#anchor] + bool readingPath = true; + bool readingQuery = false; + _path = ""; + _query = ""; + _anchor = ""; + for (uint32 i = 0; i < path.size(); ++i) { + if (readingPath) { + if (path[i] == '?') { + readingPath = false; + readingQuery = true; + } else { + _path += path[i]; + } + } else if (readingQuery) { + if (path[i] == '#') { + readingQuery = false; + } else { + _query += path[i]; + } + } else { + _anchor += path[i]; + } + } + + parseQueryParameters(); +} + +void Reader::parseQueryParameters() { + Common::String key = ""; + Common::String value = ""; + bool readingKey = true; + for (uint32 i = 0; i < _query.size(); ++i) { + if (readingKey) { + if (_query[i] == '=') { + readingKey = false; + value = ""; + } else { + key += _query[i]; + } + } else { + if (_query[i] == '&') { + if (_queryParameters.contains(key)) + warning("Reader: query parameter \"%s\" is already set!", key.c_str()); + else + _queryParameters[key] = LocalWebserver::urlDecode(value); + readingKey = true; + key = ""; + } else { + value += _query[i]; + } + } + } + + if (!key.empty()) { + if (_queryParameters.contains(key)) + warning("Reader: query parameter \"%s\" is already set!", key.c_str()); + else + _queryParameters[key] = LocalWebserver::urlDecode(value); + } +} + +bool Reader::readContentIntoStream(Common::WriteStream *stream) { + Common::String boundary = "--" + _boundary; + if (!_firstBlock) + boundary = "\r\n" + boundary; + if (_boundary.empty()) + boundary = "\r\n"; + if (_window == nullptr) + makeWindow(boundary.size()); + + while (readOneByteInStream(stream, boundary)) { + if (!bytesLeft()) + return false; + } + + _firstBlock = false; + if (stream) + stream->flush(); + + freeWindow(); + _state = RS_READING_HEADERS; + return true; +} + +void Reader::makeWindow(uint32 size) { + freeWindow(); + + _window = new byte[size]; + _windowUsed = 0; + _windowSize = size; +} + +void Reader::freeWindow() { + delete[] _window; + _window = nullptr; + _windowUsed = _windowSize = 0; +} + +namespace { +bool windowEqualsString(const byte *window, uint32 windowSize, const Common::String &boundary) { + if (boundary.size() != windowSize) + return false; + + for (uint32 i = 0; i < windowSize; ++i) { + if (window[i] != boundary[i]) + return false; + } + + return true; +} +} + +bool Reader::readOneByteInStream(Common::WriteStream *stream, const Common::String &boundary) { + byte b = readOne(); + _window[_windowUsed++] = b; + if (_windowUsed < _windowSize) + return true; + + //when window is filled, check whether that's the boundary + if (windowEqualsString(_window, _windowSize, boundary)) + return false; + + //if not, add the first byte of the window to the string + if (stream) + stream->writeByte(_window[0]); + for (uint32 i = 1; i < _windowSize; ++i) + _window[i - 1] = _window[i]; + --_windowUsed; + return true; +} + +byte Reader::readOne() { + byte b; + _content->read(&b, 1); + --_availableBytes; + --_bytesLeft; + return b; +} + +/// public + +bool Reader::readFirstHeaders() { + if (_state == RS_NONE) + _state = RS_READING_HEADERS; + + if (!bytesLeft()) + return false; + + if (_state == RS_READING_HEADERS) + return readAndHandleFirstHeaders(); + + warning("Reader::readFirstHeaders(): bad state"); + return false; +} + +bool Reader::readFirstContent(Common::WriteStream *stream) { + if (_state != RS_READING_CONTENT) { + warning("Reader::readFirstContent(): bad state"); + return false; + } + + // no difference, actually + return readBlockContent(stream); +} + +bool Reader::readBlockHeaders(Common::WriteStream *stream) { + if (_state != RS_READING_HEADERS) { + warning("Reader::readBlockHeaders(): bad state"); + return false; + } + + if (!bytesLeft()) + return false; + + return readBlockHeadersIntoStream(stream); +} + +bool Reader::readBlockContent(Common::WriteStream *stream) { + if (_state != RS_READING_CONTENT) { + warning("Reader::readBlockContent(): bad state"); + return false; + } + + if (!bytesLeft()) + return false; + + if (!readContentIntoStream(stream)) + return false; + + if (_availableBytes >= 2) { + Common::String bts; + bts += readOne(); + bts += readOne(); + if (bts == "--") + _allContentRead = true; + else if (bts != "\r\n") + warning("Reader: strange bytes: \"%s\"", bts.c_str()); + } else { + warning("Reader: strange ending"); + _allContentRead = true; + } + + return true; +} + +uint32 Reader::bytesLeft() const { return _bytesLeft; } + +void Reader::setContent(Common::MemoryReadWriteStream *stream) { + _content = stream; + _bytesLeft = stream->size() - stream->pos(); +} + +bool Reader::badRequest() const { return _isBadRequest; } + +bool Reader::noMoreContent() const { return _allContentRead; } + +Common::String Reader::headers() const { return _headers; } + +Common::String Reader::method() const { return _method; } + +Common::String Reader::path() const { return _path; } + +Common::String Reader::query() const { return _query; } + +Common::String Reader::queryParameter(Common::String name) const { return _queryParameters[name]; } + +Common::String Reader::anchor() const { return _anchor; } + +Common::String Reader::readEverythingFromMemoryStream(Common::MemoryReadWriteStream *stream) { + Common::String result; + char buf[1024]; + uint32 readBytes; + while (true) { + readBytes = stream->read(buf, 1024); + if (readBytes == 0) + break; + result += Common::String(buf, readBytes); + } + return result; +} + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/reader.h b/backends/networking/sdl_net/reader.h new file mode 100644 index 0000000000..16d62a27eb --- /dev/null +++ b/backends/networking/sdl_net/reader.h @@ -0,0 +1,143 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_NETWORKING_SDL_NET_READER_H +#define BACKENDS_NETWORKING_SDL_NET_READER_H + +#include "common/str.h" +#include "common/hashmap.h" +#include "common/hash-str.h" + +namespace Common { +class MemoryReadWriteStream; +class WriteStream; +} + +namespace Networking { + +enum ReaderState { + RS_NONE, + RS_READING_HEADERS, + RS_READING_CONTENT +}; + +/** + * This is a helper class for Client. + * + * It parses HTTP request and finds headers + * and content. It also supports POST form/multipart. + * + * One might pass the request even byte by byte, + * Reader will always be able to continue from the + * state it stopped on. + * + * Main headers/content must be read with + * readFirstHeaders() and readFirstContent() methods. + * Further headers/content blocks (POST form/multipart) + * must be read with readBlockHeaders() and readBlockContent(). + * + * Main headers and parsed URL components could be accessed + * with special methods after reading. + * + * To use the object, call setContent() and then one of those + * reading methods. It would return whether reading is over + * or not. If reading is over, content stream still could + * contain bytes to read with other methods. + * + * If reading is not over, Reader awaits you to call the + * same reading method when you'd get more content. + * + * If it's over, you should check whether Reader awaits + * more content with noMoreContent() and call the other + * reading method, if it is. When headers are read, one + * must read contents, and vice versa. + */ + +class Reader { + ReaderState _state; + Common::MemoryReadWriteStream *_content; + uint32 _bytesLeft; + + byte *_window; + uint32 _windowUsed, _windowSize; + + Common::MemoryReadWriteStream *_headersStream; + + Common::String _headers; + Common::String _method, _path, _query, _anchor; + Common::HashMap<Common::String, Common::String> _queryParameters; + uint32 _contentLength; + Common::String _boundary; + uint32 _availableBytes; + bool _firstBlock; + bool _isBadRequest; + bool _allContentRead; + + void cleanup(); + + bool readAndHandleFirstHeaders(); //true when ended reading + bool readBlockHeadersIntoStream(Common::WriteStream *stream); //true when ended reading + bool readContentIntoStream(Common::WriteStream *stream); //true when ended reading + + void handleFirstHeaders(Common::MemoryReadWriteStream *headers); + void parseFirstLine(const Common::String &headers); + void parsePathQueryAndAnchor(Common::String path); + void parseQueryParameters(); + + void makeWindow(uint32 size); + void freeWindow(); + bool readOneByteInStream(Common::WriteStream *stream, const Common::String &boundary); + + byte readOne(); + uint32 bytesLeft() const; + +public: + static const uint32 SUSPICIOUS_HEADERS_SIZE = 1024 * 1024; // 1 MB is really a lot + + Reader(); + ~Reader(); + + Reader &operator=(Reader &r); + + bool readFirstHeaders(); //true when ended reading + bool readFirstContent(Common::WriteStream *stream); //true when ended reading + bool readBlockHeaders(Common::WriteStream *stream); //true when ended reading + bool readBlockContent(Common::WriteStream *stream); //true when ended reading + + void setContent(Common::MemoryReadWriteStream *stream); + + bool badRequest() const; + bool noMoreContent() const; + + Common::String headers() const; + Common::String method() const; + Common::String path() const; + Common::String query() const; + Common::String queryParameter(Common::String name) const; + Common::String anchor() const; + + static Common::String readEverythingFromMemoryStream(Common::MemoryReadWriteStream *stream); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/uploadfileclienthandler.cpp b/backends/networking/sdl_net/uploadfileclienthandler.cpp new file mode 100644 index 0000000000..ebf341682c --- /dev/null +++ b/backends/networking/sdl_net/uploadfileclienthandler.cpp @@ -0,0 +1,212 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "backends/networking/sdl_net/uploadfileclienthandler.h" +#include "backends/fs/fs-factory.h" +#include "backends/networking/sdl_net/handlerutils.h" +#include "backends/networking/sdl_net/localwebserver.h" +#include "backends/networking/sdl_net/reader.h" +#include "common/file.h" +#include "common/memstream.h" +#include "common/translation.h" + +namespace Networking { + +UploadFileClientHandler::UploadFileClientHandler(Common::String parentDirectoryPath): + _state(UFH_READING_CONTENT), _headersStream(nullptr), _contentStream(nullptr), + _parentDirectoryPath(parentDirectoryPath), _uploadedFiles(0) {} + +UploadFileClientHandler::~UploadFileClientHandler() { + delete _headersStream; + delete _contentStream; +} + +void UploadFileClientHandler::handle(Client *client) { + if (client == nullptr) { + warning("UploadFileClientHandler::handle(): empty client pointer"); + return; + } + + while (true) { + switch (_state) { + case UFH_READING_CONTENT: + if (client->readContent(nullptr)) { + _state = UFH_READING_BLOCK_HEADERS; + continue; + } + break; + + case UFH_READING_BLOCK_HEADERS: + if (_headersStream == nullptr) + _headersStream = new Common::MemoryReadWriteStream(DisposeAfterUse::YES); + + if (client->readBlockHeaders(_headersStream)) { + handleBlockHeaders(client); + continue; + } + + // fail on suspicious headers + if (_headersStream->size() > Reader::SUSPICIOUS_HEADERS_SIZE) { + setErrorMessageHandler(*client, _("Invalid request: headers are too long!")); + } + break; + + case UFH_READING_BLOCK_CONTENT: + // _contentStream is created by handleBlockHeaders() if needed + + if (client->readBlockContent(_contentStream)) { + handleBlockContent(client); + continue; + } + break; + + case UFH_ERROR: + case UFH_STOP: + return; + } + + break; + } +} + +namespace { +void readFromThatUntilDoubleQuote(const char *cstr, Common::String needle, Common::String &result) { + const char *position = strstr(cstr, needle.c_str()); + + if (position) { + char c; + for (const char *i = position + needle.size(); c = *i, c != 0; ++i) { + if (c == '"') + break; + result += c; + } + } +} +} + +void UploadFileClientHandler::handleBlockHeaders(Client *client) { + _state = UFH_READING_BLOCK_CONTENT; + + // fail on suspicious headers + if (_headersStream->size() > Reader::SUSPICIOUS_HEADERS_SIZE) { + setErrorMessageHandler(*client, _("Invalid request: headers are too long!")); + } + + // search for "upload_file" field + Common::String headers = Reader::readEverythingFromMemoryStream(_headersStream); + Common::String fieldName = ""; + readFromThatUntilDoubleQuote(headers.c_str(), "name=\"", fieldName); + if (!fieldName.hasPrefix("upload_file")) + return; + + Common::String filename = ""; + readFromThatUntilDoubleQuote(headers.c_str(), "filename=\"", filename); + + // skip block if <filename> is empty + if (filename.empty()) + return; + + if (HandlerUtils::hasForbiddenCombinations(filename)) + return; + + // check that <path>/<filename> doesn't exist + Common::String path = _parentDirectoryPath; + if (path.lastChar() != '/' && path.lastChar() != '\\') + path += '/'; + AbstractFSNode *originalNode = g_system->getFilesystemFactory()->makeFileNodePath(path + filename); + if (!HandlerUtils::permittedPath(originalNode->getPath())) { + setErrorMessageHandler(*client, _("Invalid path!")); + return; + } + if (originalNode->exists()) { + setErrorMessageHandler(*client, _("There is a file with that name in the parent directory!")); + return; + } + + // remove previous stream (if there is one) + if (_contentStream) { + delete _contentStream; + _contentStream = nullptr; + } + + // create file stream (and necessary subdirectories) + Common::DumpFile *f = new Common::DumpFile(); + if (!f->open(originalNode->getPath(), true)) { + delete f; + setErrorMessageHandler(*client, _("Failed to upload the file!")); + return; + } + + _contentStream = f; +} + +void UploadFileClientHandler::handleBlockContent(Client *client) { + _state = UFH_READING_BLOCK_HEADERS; + + // if previous block headers were file-related and created a stream + if (_contentStream) { + _contentStream->flush(); + ++_uploadedFiles; + + delete _contentStream; + _contentStream = nullptr; + + if (client->noMoreContent()) { + // success - redirect back to directory listing + setSuccessHandler(*client); + return; + } + } + + // no more content avaiable + if (client->noMoreContent()) { + // if no file field was found - failure + if (_uploadedFiles == 0) { + setErrorMessageHandler(*client, _("No file was passed!")); + } else { + setSuccessHandler(*client); + } + } +} + +void UploadFileClientHandler::setErrorMessageHandler(Client &client, Common::String message) { + HandlerUtils::setFilesManagerErrorMessageHandler(client, message); + _state = UFH_ERROR; +} + +void UploadFileClientHandler::setSuccessHandler(Client &client) { + // success - redirect back to directory listing + HandlerUtils::setMessageHandler( + client, + Common::String::format( + "%s<br/><a href=\"files?path=%s\">%s</a>", + _("Uploaded successfully!"), + client.queryParameter("path").c_str(), + _("Back to parent directory") + ), + (client.queryParameter("ajax") == "true" ? "/filesAJAX?path=" : "/files?path=") + + LocalWebserver::urlEncodeQueryParameterValue(client.queryParameter("path")) + ); + _state = UFH_STOP; +} + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/uploadfileclienthandler.h b/backends/networking/sdl_net/uploadfileclienthandler.h new file mode 100644 index 0000000000..b6481cf18f --- /dev/null +++ b/backends/networking/sdl_net/uploadfileclienthandler.h @@ -0,0 +1,70 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#ifndef BACKENDS_NETWORKING_SDL_NET_UPLOADFILECLIENTHANDLER_H +#define BACKENDS_NETWORKING_SDL_NET_UPLOADFILECLIENTHANDLER_H + +#include "backends/networking/sdl_net/client.h" +#include "common/stream.h" + +namespace Networking { + +enum UploadFileHandlerState { + UFH_READING_CONTENT, + UFH_READING_BLOCK_HEADERS, + UFH_READING_BLOCK_CONTENT, + UFH_ERROR, + UFH_STOP +}; + +/** + * This class handles POST form/multipart upload. + * + * handleBlockHeaders() looks for filename and, if it's found, + * handleBlockContent() saves content into the file with such name. + * + * If no file found or other error occurs, it sets + * default error message handler. + */ + +class UploadFileClientHandler: public ClientHandler { + UploadFileHandlerState _state; + Common::MemoryReadWriteStream *_headersStream; + Common::WriteStream *_contentStream; + Common::String _parentDirectoryPath; + uint32 _uploadedFiles; + + void handleBlockHeaders(Client *client); + void handleBlockContent(Client *client); + void setErrorMessageHandler(Client &client, Common::String message); + void setSuccessHandler(Client &client); + +public: + UploadFileClientHandler(Common::String parentDirectoryPath); + virtual ~UploadFileClientHandler(); + + virtual void handle(Client *client); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/wwwroot.zip b/backends/networking/wwwroot.zip Binary files differnew file mode 100644 index 0000000000..b767d7c5d7 --- /dev/null +++ b/backends/networking/wwwroot.zip diff --git a/backends/networking/wwwroot/.files.html b/backends/networking/wwwroot/.files.html new file mode 100644 index 0000000000..f05c0113f8 --- /dev/null +++ b/backends/networking/wwwroot/.files.html @@ -0,0 +1,60 @@ +<!doctype html> +<html> + <head> + <title>ScummVM</title> + <meta charset="utf-8"/> + <link rel="stylesheet" type="text/css" href="style.css"/> + </head> + <body> + <div class="container"> + <div class='header'> + <center><img src="logo.png"/></center> + </div> + <div class="controls"> + <table class="buttons"><tr> + <td><a href="javascript:show('create_directory');">{create_directory_button}</a></td> + <td><a href="javascript:show('upload_file');">{upload_files_button}</a></td> + </tr></table> + <div id="create_directory" class="modal"> + <p>{create_directory_desc}</p> + <form action="create"> + <input type="hidden" name="path" value="{path}"/> + <input type="text" name="directory_name" value=""/> + <input type="submit" value="{create_directory_button}"/> + </form> + </div> + <div id="upload_file" class="modal"> + <p>{upload_file_desc}</p> + <form action="upload?path={path}" method="post" enctype="multipart/form-data"> + <!-- we don't need "[]" in the name, as our webserver is not using PHP --> + <!-- "allowdirs" is a proposal, not implemented yet --> + <input type="file" name="upload_file-f" allowdirs multiple/> + <br/><br/> + <p>{or_upload_directory_desc}</p> + <!-- "directory"/"webkitdirectory" works in Chrome only yet, "multiple" is just in case here --> + <input type="file" name="upload_file-d" directory webkitdirectory multiple/> + <input type="submit" value="{upload_file_button}"/> + </form> + </div> + </div> + <div class="content"> + <table class="files_list"> + <td></td><td><b class="directory_name">{index_of_directory}</b></td><td></td> + {content} + </table> + </div> + </div> + <script> + function show(id) { + var e = document.getElementById(id); + var visible = (e.style.display == "block"); + if (visible) id = ""; //hide + + e = document.getElementById("create_directory"); + e.style.display = (e.id == id ? "block" : "none"); + e = document.getElementById("upload_file"); + e.style.display = (e.id == id ? "block" : "none"); + } + </script> + </body> +</html>
\ No newline at end of file diff --git a/backends/networking/wwwroot/.filesAJAX.html b/backends/networking/wwwroot/.filesAJAX.html new file mode 100644 index 0000000000..d45c73069d --- /dev/null +++ b/backends/networking/wwwroot/.filesAJAX.html @@ -0,0 +1,240 @@ +<!doctype html> +<html> + <head> + <title>ScummVM</title> + <meta charset="utf-8"/> + <link rel="stylesheet" type="text/css" href="style.css"/> + </head> + <body> + <div class="container"> + <div class='header'> + <center><img src="logo.png"/></center> + </div> + <div class="controls"> + <table class="buttons"><tr> + <td><a href="javascript:show('create_directory');">{create_directory_button}</a></td> + <td><a href="javascript:show('upload_file');">{upload_files_button}</a></td> + </tr></table> + <div id="create_directory" class="modal"> + <p>{create_directory_desc}</p> + <form action="create" id="create_directory_form" onsubmit="return createDirectory();"> + <input type="hidden" name="path" value="{path}"/> + <input type="hidden" name="ajax" value="true"/> + <input type="text" name="directory_name" value=""/> + <input type="submit" value="{create_directory_button}"/> + </form> + </div> + <div id="upload_file" class="modal"> + <p>{upload_file_desc}</p> + <form action="upload?path={path}&ajax=true" method="post" enctype="multipart/form-data" id="files_upload_form"> + <!-- we don't need "[]" in the name, as our webserver is not using PHP --> + <!-- "allowdirs" is a proposal, not implemented yet --> + <input type="file" name="upload_file-f" allowdirs multiple/> + <br/><br/> + <p>{or_upload_directory_desc}</p> + <!-- "directory"/"webkitdirectory" works in Chrome only yet, "multiple" is just in case here --> + <input type="file" name="upload_file-d" directory webkitdirectory multiple/> + <input type="submit" value="{upload_file_button}"/> + </form> + </div> + </div> + <div class="content"> + <div id="loading_message">{loading}</div> + <div id="error_message">{error}</div> + <table class="files_list" id="files_list"> + </table> + </div> + </div> + <script> + function show(id) { + var e = document.getElementById(id); + var visible = (e.style.display == "block"); + if (visible) id = ""; //hide + + e = document.getElementById("create_directory"); + e.style.display = (e.id == id ? "block" : "none"); + e = document.getElementById("upload_file"); + e.style.display = (e.id == id ? "block" : "none"); + } + </script> + <script src="ajax.js"></script> + <script> + window.onload = function () { + showDirectory("{start_path}"); + } + + function showDirectory(path) { + if (isLoading) return; + showLoading(); + ajax.getAndParseJson("./list", {"path": path}, getCallback(path)); + } + + function getCallback(path) { + return function (jsonResponse) { + if (jsonResponse.type == "error") { + showError(); + return; + } + + openDirectory(path, jsonResponse.items); + hideLoading(); + }; + } + + function createDirectory() { + if (isLoading) return; + showLoading(); + + var data = {"answer_json": "true"}; + var elements = document.getElementById("create_directory_form").elements; + for (var el in elements) + data[elements[el].name] = elements[el].value; + + ajax.getAndParseJson("./create", data, getCreateDirectoryCallback(data["path"])); + show("create_directory"); + return false; // invalidate form, so it won't submit + } + + function getCreateDirectoryCallback(path) { + return function (jsonResponse) { + console.log(jsonResponse); + + if (jsonResponse.type == "error") { + showError(); + return; + } + + hideLoading(); + showDirectory(path); + }; + } + + var isLoading = false; + + function showLoading() { + isLoading = true; + var e = document.getElementById("loading_message"); + e.style.display = "block"; + e = document.getElementById("error_message"); + e.style.display = "none"; + } + + function showError() { + isLoading = false; + var e = document.getElementById("loading_message"); + e.style.display = "none"; + e = document.getElementById("error_message"); + e.style.display = "block"; + //TODO: pass the actual message there? + } + + function hideLoading() { + isLoading = false; + var e = document.getElementById("loading_message"); + e.style.display = "none"; + e = document.getElementById("error_message"); + e.style.display = "none"; + } + + function openDirectory(path, items) { + // update path + document.getElementById("create_directory_form").elements["path"].value = path; + document.getElementById("files_upload_form").action = "upload?path=" + path + "&ajax=true"; + + // update table contents + listDirectory(path, items); + } + + function makeBreadcrumb(name, path) { + var a = createElementWithContents("a", name); + a.onclick = function () { showDirectory(path); }; + a.href = "javascript:void(0);"; + return a; + } + + function makeBreadcrumbs(path) { + var b = document.createElement("b"); + b.className = "directory_name"; + + b.appendChild(createElementWithContents("span", "{index_of}")); + var slashes = true; + var crumb = ""; + var currentPath = ""; + path += ' '; //so the last slash is added + for (var i=0; i<path.length; ++i) { + if (path[i] == '/' || path[i] == '\\') { + if (!slashes) { + currentPath += crumb; + b.appendChild(makeBreadcrumb(crumb, currentPath+'/')); + slashes = true; + } + } else { + if (slashes) { + currentPath += "/"; + if (currentPath == "/") { //make special '/' crumb here + b.appendChild(makeBreadcrumb('/', '/')); + } else { + b.appendChild(createElementWithContents("span", "/")); + } + slashes = false; + crumb = ""; + } + crumb += path[i]; + } + } + return b; + } + + function listDirectory(path, items) { + // cleanup the list + var files_list = document.getElementById("files_list"); + while (files_list.hasChildNodes()) + files_list.removeChild(files_list.firstChild); + var tbody = document.createElement("tbody"); + + // add header item + var tr = document.createElement("tr"); + tr.appendChild(createElementWithContents("td", "")); + var td = document.createElement("td"); + td.appendChild(makeBreadcrumbs(path)); + tr.appendChild(td); + tr.appendChild(createElementWithContents("td", "")); + tbody.appendChild(tr); + + // add items + for (var i in items) + addItem(tbody, items[i]); + + files_list.appendChild(tbody); + } + + function addItem(tbody, item) { + var tr = document.createElement("tr"); + var td = document.createElement("td"); + var img = document.createElement("img"); + img.src = "./icons/" + item.icon; + td.appendChild(img); + tr.appendChild(td); + + td = document.createElement("td"); + var a = createElementWithContents("a", item.name); + if (item.isDirectory) { + a.onclick = function () { showDirectory(item.path); }; + a.href = "javascript:void(0);"; + } else + a.href = "./download?path=" + encodeURIComponent(item.path); + td.appendChild(a); + tr.appendChild(td); + + tr.appendChild(createElementWithContents("td", "")); + tbody.appendChild(tr); + } + + function createElementWithContents(type, innerHTML) { + var e = document.createElement(type); + e.innerHTML = innerHTML; + return e; + } + </script> + </body> +</html>
\ No newline at end of file diff --git a/backends/networking/wwwroot/.index.html b/backends/networking/wwwroot/.index.html new file mode 100644 index 0000000000..2a3d9d382d --- /dev/null +++ b/backends/networking/wwwroot/.index.html @@ -0,0 +1,18 @@ +<!doctype html> +<html> + <head> + <title>ScummVM</title> + <meta charset="utf-8"/> + <link rel="stylesheet" type="text/css" href="style.css"/> + </head> + <body> + <div class="container"> + <div class='header'> + <center><img src="logo.png"/></center> + </div> + <div class="content"> + <p>{message}</p> + </div> + </div> + </body> +</html>
\ No newline at end of file diff --git a/backends/networking/wwwroot/ajax.js b/backends/networking/wwwroot/ajax.js new file mode 100644 index 0000000000..c01d7e93fc --- /dev/null +++ b/backends/networking/wwwroot/ajax.js @@ -0,0 +1,48 @@ +// the following is snippet from http://stackoverflow.com/a/18078705 +// I changed a few things though + +var ajax = {}; +ajax.x = function () { return new XMLHttpRequest(); }; // "no one uses IE6" + +ajax.send = function (url, callback, errorCallback, method, data, async) { + if (async === undefined) async = true; + + var x = ajax.x(); + x.open(method, url, async); + x.onreadystatechange = function () { + if (x.readyState == XMLHttpRequest.DONE) { + if (x.status == 200) + callback(x.responseText); + else + errorCallback(x); + } + }; + if (method == 'POST') { + x.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + } + x.send(data) +}; + +ajax.get = function (url, data, callback, errorCallback, async) { + var query = []; + for (var key in data) { + query.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key])); + } + ajax.send(url + (query.length ? '?' + query.join('&') : ''), callback, errorCallback, 'GET', null, async) +}; + +ajax.post = function (url, data, callback, errorCallback, async) { + var query = []; + for (var key in data) { + query.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key])); + } + ajax.send(url, callback, errorCallback, 'POST', query.join('&'), async) +}; + +ajax.getAndParseJson = function (url, data, callback) { + ajax.get( + url, data, + function (responseText) { callback(JSON.parse(responseText)); }, + function (x) { console.log("error: " + x.status); } + ); +};
\ No newline at end of file diff --git a/backends/networking/wwwroot/favicon.ico b/backends/networking/wwwroot/favicon.ico Binary files differnew file mode 100644 index 0000000000..0283e8432e --- /dev/null +++ b/backends/networking/wwwroot/favicon.ico diff --git a/backends/networking/wwwroot/icons/7z.png b/backends/networking/wwwroot/icons/7z.png Binary files differnew file mode 100644 index 0000000000..656e7e7c62 --- /dev/null +++ b/backends/networking/wwwroot/icons/7z.png diff --git a/backends/networking/wwwroot/icons/dir.png b/backends/networking/wwwroot/icons/dir.png Binary files differnew file mode 100644 index 0000000000..bcdec04a57 --- /dev/null +++ b/backends/networking/wwwroot/icons/dir.png diff --git a/backends/networking/wwwroot/icons/txt.png b/backends/networking/wwwroot/icons/txt.png Binary files differnew file mode 100644 index 0000000000..023d2ee24a --- /dev/null +++ b/backends/networking/wwwroot/icons/txt.png diff --git a/backends/networking/wwwroot/icons/unk.png b/backends/networking/wwwroot/icons/unk.png Binary files differnew file mode 100644 index 0000000000..346eebecc3 --- /dev/null +++ b/backends/networking/wwwroot/icons/unk.png diff --git a/backends/networking/wwwroot/icons/up.png b/backends/networking/wwwroot/icons/up.png Binary files differnew file mode 100644 index 0000000000..2dc3df022b --- /dev/null +++ b/backends/networking/wwwroot/icons/up.png diff --git a/backends/networking/wwwroot/icons/zip.png b/backends/networking/wwwroot/icons/zip.png Binary files differnew file mode 100644 index 0000000000..cdfc5763dd --- /dev/null +++ b/backends/networking/wwwroot/icons/zip.png diff --git a/backends/networking/wwwroot/logo.png b/backends/networking/wwwroot/logo.png Binary files differnew file mode 100644 index 0000000000..9fdd2d0d1e --- /dev/null +++ b/backends/networking/wwwroot/logo.png diff --git a/backends/networking/wwwroot/style.css b/backends/networking/wwwroot/style.css new file mode 100644 index 0000000000..ba31587c4d --- /dev/null +++ b/backends/networking/wwwroot/style.css @@ -0,0 +1,113 @@ +html { + background: rgb(212, 117, 11); + background: linear-gradient(to bottom, rgb(212, 117, 11) 0%, rgb(212, 117, 11) 36%, rgb(239, 196, 24) 100%); + min-height: 100vh; +} + +.container { + width: 80%; + margin: 0 auto; +} + +.header { + padding: 10pt; + margin-bottom: 0; +} + +.content { + padding: 8pt; + background: rgb(251, 241, 206); + font-family: Tahoma; + font-size: 16pt; +} + +.content p { margin: 0 0 6pt 0; } + +.controls { + padding: 8pt; + background: #FFF; + font-family: Tahoma; + font-size: 16pt; +} + +.controls .buttons { + width: 100%; + max-width: 500pt; + margin: -8pt auto; + border: 0; + border-spacing: 0; +} + +.controls .buttons td { + width: 50%; + text-align: center; + margin: 0; + padding: 0; +} + +.controls .buttons a { + display: block; + height: 40pt; + line-height: 38pt; + vertical-align: middle; + color: #000; + text-decoration: none; +} + +.controls .buttons a:hover { + background: #F3F3F3; +} + +.modal { + margin-top: 10pt; + display: none; +} + +.modal p { margin: 0 0 6pt 0; } + +#create_directory input[type="text"], #upload_file input[type="file"] { + width: calc(100% - 2 * 5pt); +} + +.modal input { + border: 1px solid #EEE; + padding: 5pt; + font-size: 12pt; +} + +.modal input[type="submit"] { + display: block; + margin: 6pt auto; + background: #DDD; + border: 0; +} + +.modal input[type="submit"]:hover { + background: #F3F3F3; + cursor: pointer; +} + +td img { vertical-align: middle; height: 20px; } + +.directory_name { + display: block; + padding-bottom: 6px; +} + +.directory_name a { color: black; } +.directory_name a:hover { color: blue; } + +#loading_message, #error_message { + margin: -8pt; + margin-bottom: 5pt; + padding: 4pt; + text-align: center; +} + +#loading_message { + background: #99FF99; +} + +#error_message { + background: #FF9999; +} diff --git a/backends/platform/android/jni.cpp b/backends/platform/android/jni.cpp index 22e6a749c2..256ae09ef8 100644 --- a/backends/platform/android/jni.cpp +++ b/backends/platform/android/jni.cpp @@ -76,6 +76,8 @@ bool JNI::_ready_for_events = 0; jmethodID JNI::_MID_getDPI = 0; jmethodID JNI::_MID_displayMessageOnOSD = 0; +jmethodID JNI::_MID_openUrl = 0; +jmethodID JNI::_MID_isConnectionLimited = 0; jmethodID JNI::_MID_setWindowCaption = 0; jmethodID JNI::_MID_showVirtualKeyboard = 0; jmethodID JNI::_MID_getSysArchives = 0; @@ -232,6 +234,41 @@ void JNI::displayMessageOnOSD(const char *msg) { env->DeleteLocalRef(java_msg); } +bool JNI::openUrl(const char *url) { + bool success = true; + JNIEnv *env = JNI::getEnv(); + jstring javaUrl = env->NewStringUTF(url); + + env->CallVoidMethod(_jobj, _MID_openUrl, javaUrl); + + if (env->ExceptionCheck()) { + LOGE("Failed to open URL"); + + env->ExceptionDescribe(); + env->ExceptionClear(); + success = false; + } + + env->DeleteLocalRef(javaUrl); + return success; +} + +bool JNI::isConnectionLimited() { + bool limited = false; + JNIEnv *env = JNI::getEnv(); + limited = env->CallBooleanMethod(_jobj, _MID_isConnectionLimited); + + if (env->ExceptionCheck()) { + LOGE("Failed to check whether connection's limited"); + + env->ExceptionDescribe(); + env->ExceptionClear(); + limited = true; + } + + return limited; +} + void JNI::setWindowCaption(const char *caption) { JNIEnv *env = JNI::getEnv(); jstring java_caption = env->NewStringUTF(caption); @@ -411,6 +448,8 @@ void JNI::create(JNIEnv *env, jobject self, jobject asset_manager, FIND_METHOD(, setWindowCaption, "(Ljava/lang/String;)V"); FIND_METHOD(, getDPI, "([F)V"); FIND_METHOD(, displayMessageOnOSD, "(Ljava/lang/String;)V"); + FIND_METHOD(, openUrl, "(Ljava/lang/String;)V"); + FIND_METHOD(, isConnectionLimited, "()Z"); FIND_METHOD(, showVirtualKeyboard, "(Z)V"); FIND_METHOD(, getSysArchives, "()[Ljava/lang/String;"); FIND_METHOD(, initSurface, "()Ljavax/microedition/khronos/egl/EGLSurface;"); diff --git a/backends/platform/android/jni.h b/backends/platform/android/jni.h index 70feaaf72a..0798db448a 100644 --- a/backends/platform/android/jni.h +++ b/backends/platform/android/jni.h @@ -58,6 +58,8 @@ public: static void setWindowCaption(const char *caption); static void getDPI(float *values); static void displayMessageOnOSD(const char *msg); + static bool openUrl(const char *url); + static bool isConnectionLimited(); static void showVirtualKeyboard(bool enable); static void addSysArchivesToSearchSet(Common::SearchSet &s, int priority); @@ -89,6 +91,8 @@ private: static jmethodID _MID_getDPI; static jmethodID _MID_displayMessageOnOSD; + static jmethodID _MID_openUrl; + static jmethodID _MID_isConnectionLimited; static jmethodID _MID_setWindowCaption; static jmethodID _MID_showVirtualKeyboard; static jmethodID _MID_getSysArchives; diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVM.java b/backends/platform/android/org/scummvm/scummvm/ScummVM.java index 3b370a583d..47dcb32b22 100644 --- a/backends/platform/android/org/scummvm/scummvm/ScummVM.java +++ b/backends/platform/android/org/scummvm/scummvm/ScummVM.java @@ -53,6 +53,8 @@ public abstract class ScummVM implements SurfaceHolder.Callback, Runnable { // Callbacks from C++ peer instance abstract protected void getDPI(float[] values); abstract protected void displayMessageOnOSD(String msg); + abstract protected void openUrl(String url); + abstract protected boolean isConnectionLimited(); abstract protected void setWindowCaption(String caption); abstract protected void showVirtualKeyboard(boolean enable); abstract protected String[] getSysArchives(); diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java index 5b2dcae175..225496ca0d 100644 --- a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java +++ b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java @@ -2,9 +2,13 @@ package org.scummvm.scummvm; import android.app.Activity; import android.app.AlertDialog; +import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.media.AudioManager; +import android.net.Uri; +import android.net.wifi.WifiManager; +import android.net.wifi.WifiInfo; import android.os.Build; import android.os.Bundle; import android.os.Environment; @@ -75,6 +79,21 @@ public class ScummVMActivity extends Activity { } @Override + protected void openUrl(String url) { + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))); + } + + @Override + protected boolean isConnectionLimited() { + WifiManager wifiMgr = (WifiManager)getSystemService(Context.WIFI_SERVICE); + if (wifiMgr != null && wifiMgr.isWifiEnabled()) { + WifiInfo wifiInfo = wifiMgr.getConnectionInfo(); + return (wifiInfo == null || wifiInfo.getNetworkId() == -1); //WiFi is on, but it's not connected to any network + } + return true; + } + + @Override protected void setWindowCaption(final String caption) { runOnUiThread(new Runnable() { public void run() { diff --git a/backends/platform/ds/arm9/source/gbampsave.cpp b/backends/platform/ds/arm9/source/gbampsave.cpp index ef6091e2a2..236ec55801 100644 --- a/backends/platform/ds/arm9/source/gbampsave.cpp +++ b/backends/platform/ds/arm9/source/gbampsave.cpp @@ -56,7 +56,7 @@ Common::OutSaveFile *GBAMPSaveFileManager::openForSaving(const Common::String &f Common::WriteStream *stream = DS::DSFileStream::makeFromPath(fileSpec, true); // Use a write buffer stream = Common::wrapBufferedWriteStream(stream, SAVE_BUFFER_SIZE); - return stream; + return new OutSaveFile(stream); } Common::InSaveFile *GBAMPSaveFileManager::openForLoading(const Common::String &filename) { diff --git a/backends/platform/n64/framfs_save_manager.h b/backends/platform/n64/framfs_save_manager.h index 9bd4ee579e..aa26942bdc 100644 --- a/backends/platform/n64/framfs_save_manager.h +++ b/backends/platform/n64/framfs_save_manager.h @@ -106,7 +106,7 @@ public: virtual Common::OutSaveFile *openForSaving(const Common::String &filename, bool compress = true) { OutFRAMSave *s = new OutFRAMSave(filename.c_str()); if (!s->err()) { - return compress ? Common::wrapCompressedWriteStream(s) : s; + return new OutSaveFile(compress ? Common::wrapCompressedWriteStream(s) : s); } else { delete s; return 0; diff --git a/backends/platform/n64/pakfs_save_manager.h b/backends/platform/n64/pakfs_save_manager.h index 0c08f0c506..31aa01444c 100644 --- a/backends/platform/n64/pakfs_save_manager.h +++ b/backends/platform/n64/pakfs_save_manager.h @@ -108,7 +108,7 @@ public: virtual Common::OutSaveFile *openForSaving(const Common::String &filename, bool compress = true) { OutPAKSave *s = new OutPAKSave(filename.c_str()); if (!s->err()) { - return compress ? Common::wrapCompressedWriteStream(s) : s; + return new OutSaveFile(compress ? Common::wrapCompressedWriteStream(s) : s); } else { delete s; return NULL; diff --git a/backends/platform/ps2/savefilemgr.cpp b/backends/platform/ps2/savefilemgr.cpp index 4fd2b1c72b..569d0e13e3 100644 --- a/backends/platform/ps2/savefilemgr.cpp +++ b/backends/platform/ps2/savefilemgr.cpp @@ -192,7 +192,7 @@ Common::OutSaveFile *Ps2SaveFileManager::openForSaving(const Common::String &fil } _screen->wantAnim(false); - return compress ? Common::wrapCompressedWriteStream(sf) : sf; + return new OutSaveFile(compress ? Common::wrapCompressedWriteStream(sf) : sf); } bool Ps2SaveFileManager::removeSavefile(const Common::String &filename) { diff --git a/backends/platform/sdl/sdl.cpp b/backends/platform/sdl/sdl.cpp index dca6891fef..6862bb349f 100644 --- a/backends/platform/sdl/sdl.cpp +++ b/backends/platform/sdl/sdl.cpp @@ -60,6 +60,15 @@ #endif // !WIN32 #endif +#ifdef USE_SDL_NET +#include <SDL/SDL_net.h> +#endif + +#if SDL_VERSION_ATLEAST(2, 0, 0) +#include <SDL2/SDL.h> +#include <SDL2/SDL_clipboard.h> +#endif + OSystem_SDL::OSystem_SDL() : #ifdef USE_OPENGL @@ -73,6 +82,9 @@ OSystem_SDL::OSystem_SDL() #endif _inited(false), _initedSDL(false), +#ifdef USE_SDL_NET + _initedSDLnet(false), +#endif _logger(0), _mixerManager(0), _eventSource(0), @@ -120,6 +132,10 @@ OSystem_SDL::~OSystem_SDL() { delete _logger; _logger = 0; +#ifdef USE_SDL_NET + if (_initedSDLnet) SDLNet_Quit(); +#endif + SDL_Quit(); } @@ -160,6 +176,13 @@ void OSystem_SDL::init() { } +bool OSystem_SDL::hasFeature(Feature f) { +#if SDL_VERSION_ATLEAST(2, 0, 0) + if (f == kFeatureClipboardSupport) return true; +#endif + return ModularBackend::hasFeature(f); +} + void OSystem_SDL::initBackend() { // Check if backend has not been initialized assert(!_inited); @@ -294,6 +317,17 @@ void OSystem_SDL::initSDL() { _initedSDL = true; } + +#ifdef USE_SDL_NET + // Check if SDL_net has not been initialized + if (!_initedSDLnet) { + // Initialize SDL_net + if (SDLNet_Init() == -1) + error("Could not initialize SDL_net: %s", SDLNet_GetError()); + + _initedSDLnet = true; + } +#endif } void OSystem_SDL::addSysArchivesToSearchSet(Common::SearchSet &s, int priority) { @@ -431,6 +465,26 @@ Common::String OSystem_SDL::getSystemLanguage() const { #endif // USE_DETECTLANG } +bool OSystem_SDL::hasTextInClipboard() { +#if SDL_VERSION_ATLEAST(2, 0, 0) + return SDL_HasClipboardText() == SDL_TRUE; +#else + return false; +#endif +} + +Common::String OSystem_SDL::getTextFromClipboard() { + if (!hasTextInClipboard()) return ""; + +#if SDL_VERSION_ATLEAST(2, 0, 0) + char *text = SDL_GetClipboardText(); + if (text == nullptr) return ""; + return text; +#else + return ""; +#endif +} + uint32 OSystem_SDL::getMillis(bool skipRecord) { uint32 millis = SDL_GetTicks(); diff --git a/backends/platform/sdl/sdl.h b/backends/platform/sdl/sdl.h index 1fe670c5c3..17b4e9b001 100644 --- a/backends/platform/sdl/sdl.h +++ b/backends/platform/sdl/sdl.h @@ -55,6 +55,8 @@ public: */ virtual SdlMixerManager *getMixerManager(); + virtual bool hasFeature(Feature f); + // Override functions from ModularBackend and OSystem virtual void initBackend(); #if defined(USE_TASKBAR) @@ -69,6 +71,10 @@ public: virtual Common::String getSystemLanguage() const; + // Clipboard + virtual bool hasTextInClipboard(); + virtual Common::String getTextFromClipboard(); + virtual void setWindowCaption(const char *caption); virtual void addSysArchivesToSearchSet(Common::SearchSet &s, int priority = 0); virtual uint32 getMillis(bool skipRecord = false); @@ -81,6 +87,9 @@ public: protected: bool _inited; bool _initedSDL; +#ifdef USE_SDL_NET + bool _initedSDLnet; +#endif /** * Mixer manager that configures and setups SDL for diff --git a/backends/saves/default/default-saves.cpp b/backends/saves/default/default-saves.cpp index daec36ae72..8a7fba46f7 100644 --- a/backends/saves/default/default-saves.cpp +++ b/backends/saves/default/default-saves.cpp @@ -27,6 +27,11 @@ #include "common/scummsys.h" +#ifdef USE_LIBCURL +#include "backends/cloud/cloudmanager.h" +#include "common/file.h" +#endif + #if !defined(DISABLE_DEFAULT_SAVEFILEMANAGER) #include "backends/saves/default/default-saves.h" @@ -42,6 +47,10 @@ #include <errno.h> // for removeSavefile() #endif +#ifdef USE_LIBCURL +const char *DefaultSaveFileManager::TIMESTAMPS_FILENAME = "timestamps"; +#endif + DefaultSaveFileManager::DefaultSaveFileManager() { } @@ -59,28 +68,62 @@ void DefaultSaveFileManager::checkPath(const Common::FSNode &dir) { } } +void DefaultSaveFileManager::updateSavefilesList(Common::StringArray &lockedFiles) { + //make it refresh the cache next time it lists the saves + _cachedDirectory = ""; + + //remember the locked files list because some of these files don't exist yet + _lockedFiles = lockedFiles; +} + Common::StringArray DefaultSaveFileManager::listSavefiles(const Common::String &pattern) { // Assure the savefile name cache is up-to-date. assureCached(getSavePath()); if (getError().getCode() != Common::kNoError) return Common::StringArray(); + Common::HashMap<Common::String, bool> locked; + for (Common::StringArray::const_iterator i = _lockedFiles.begin(), end = _lockedFiles.end(); i != end; ++i) { + locked[*i] = true; + } + Common::StringArray results; for (SaveFileCache::const_iterator file = _saveFileCache.begin(), end = _saveFileCache.end(); file != end; ++file) { - if (file->_key.matchString(pattern, true)) { + if (!locked.contains(file->_key) && file->_key.matchString(pattern, true)) { results.push_back(file->_key); } } - return results; } +Common::InSaveFile *DefaultSaveFileManager::openRawFile(const Common::String &filename) { + // Assure the savefile name cache is up-to-date. + assureCached(getSavePath()); + if (getError().getCode() != Common::kNoError) + return nullptr; + + SaveFileCache::const_iterator file = _saveFileCache.find(filename); + if (file == _saveFileCache.end()) { + return nullptr; + } else { + // Open the file for loading. + Common::SeekableReadStream *sf = file->_value.createReadStream(); + return sf; + } +} + Common::InSaveFile *DefaultSaveFileManager::openForLoading(const Common::String &filename) { // Assure the savefile name cache is up-to-date. assureCached(getSavePath()); if (getError().getCode() != Common::kNoError) return nullptr; + for (Common::StringArray::const_iterator i = _lockedFiles.begin(), end = _lockedFiles.end(); i != end; ++i) { + if (filename == *i) { + return nullptr; //file is locked, no loading available + } + } + SaveFileCache::const_iterator file = _saveFileCache.find(filename); if (file == _saveFileCache.end()) { return nullptr; @@ -98,6 +141,19 @@ Common::OutSaveFile *DefaultSaveFileManager::openForSaving(const Common::String if (getError().getCode() != Common::kNoError) return nullptr; + for (Common::StringArray::const_iterator i = _lockedFiles.begin(), end = _lockedFiles.end(); i != end; ++i) { + if (filename == *i) { + return nullptr; //file is locked, no saving available + } + } + +#ifdef USE_LIBCURL + // Update file's timestamp + Common::HashMap<Common::String, uint32> timestamps = loadTimestamps(); + timestamps[filename] = INVALID_TIMESTAMP; + saveTimestamps(timestamps); +#endif + // Obtain node. SaveFileCache::const_iterator file = _saveFileCache.find(filename); Common::FSNode fileNode; @@ -112,7 +168,7 @@ Common::OutSaveFile *DefaultSaveFileManager::openForSaving(const Common::String // Open the file for saving. Common::WriteStream *const sf = fileNode.createWriteStream(); - Common::OutSaveFile *const result = compress ? Common::wrapCompressedWriteStream(sf) : sf; + Common::OutSaveFile *const result = new Common::OutSaveFile(compress ? Common::wrapCompressedWriteStream(sf) : sf); // Add file to cache now that it exists. _saveFileCache[filename] = Common::FSNode(fileNode.getPath()); @@ -181,6 +237,12 @@ void DefaultSaveFileManager::assureCached(const Common::String &savePathName) { // Check that path exists and is usable. checkPath(Common::FSNode(savePathName)); +#ifdef USE_LIBCURL + Common::Array<Common::String> files = CloudMan.getSyncingFiles(); //returns empty array if not syncing + if (!files.empty()) updateSavefilesList(files); //makes this cache invalid + else _lockedFiles = files; +#endif + if (_cachedDirectory == savePathName) { return; } @@ -216,4 +278,102 @@ void DefaultSaveFileManager::assureCached(const Common::String &savePathName) { _cachedDirectory = savePathName; } +#ifdef USE_LIBCURL + +Common::HashMap<Common::String, uint32> DefaultSaveFileManager::loadTimestamps() { + Common::HashMap<Common::String, uint32> timestamps; + + //refresh the files list + Common::Array<Common::String> files; + g_system->getSavefileManager()->updateSavefilesList(files); + + //start with listing all the files in saves/ directory and setting invalid timestamp to them + Common::StringArray localFiles = g_system->getSavefileManager()->listSavefiles("*"); + for (uint32 i = 0; i < localFiles.size(); ++i) + timestamps[localFiles[i]] = INVALID_TIMESTAMP; + + //now actually load timestamps from file + Common::InSaveFile *file = g_system->getSavefileManager()->openRawFile(TIMESTAMPS_FILENAME); + if (!file) { + warning("DefaultSaveFileManager: failed to open '%s' file to load timestamps", TIMESTAMPS_FILENAME); + return timestamps; + } + + while (!file->eos()) { + //read filename into buffer (reading until the first ' ') + Common::String buffer; + while (!file->eos()) { + byte b = file->readByte(); + if (b == ' ') break; + buffer += (char)b; + } + + //read timestamp info buffer (reading until ' ' or some line ending char) + Common::String filename = buffer; + while (true) { + bool lineEnded = false; + buffer = ""; + while (!file->eos()) { + byte b = file->readByte(); + if (b == ' ' || b == '\n' || b == '\r') { + lineEnded = (b == '\n'); + break; + } + buffer += (char)b; + } + + if (buffer == "" && file->eos()) break; + if (!lineEnded) filename += " " + buffer; + else break; + } + + //parse timestamp + uint32 timestamp = buffer.asUint64(); + if (buffer == "" || timestamp == 0) break; + timestamps[filename] = timestamp; + } + + delete file; + return timestamps; +} + +void DefaultSaveFileManager::saveTimestamps(Common::HashMap<Common::String, uint32> ×tamps) { + Common::DumpFile f; + Common::String filename = concatWithSavesPath(TIMESTAMPS_FILENAME); + if (!f.open(filename, true)) { + warning("DefaultSaveFileManager: failed to open '%s' file to save timestamps", filename.c_str()); + return; + } + + for (Common::HashMap<Common::String, uint32>::iterator i = timestamps.begin(); i != timestamps.end(); ++i) { + Common::String data = i->_key + Common::String::format(" %u\n", i->_value); + if (f.write(data.c_str(), data.size()) != data.size()) { + warning("DefaultSaveFileManager: failed to write timestamps data into '%s'", filename.c_str()); + return; + } + } + + f.flush(); + f.finalize(); + f.close(); +} + +Common::String DefaultSaveFileManager::concatWithSavesPath(Common::String name) { + DefaultSaveFileManager *manager = dynamic_cast<DefaultSaveFileManager *>(g_system->getSavefileManager()); + Common::String path = (manager ? manager->getSavePath() : ConfMan.get("savepath")); + if (path.size() > 0 && (path.lastChar() == '/' || path.lastChar() == '\\')) + return path + name; + + //simple heuristic to determine which path separator to use + int backslashes = 0; + for (uint32 i = 0; i < path.size(); ++i) + if (path[i] == '/') --backslashes; + else if (path[i] == '\\') ++backslashes; + + if (backslashes > 0) return path + '\\' + name; + return path + '/' + name; +} + +#endif // ifdef USE_LIBCURL + #endif // !defined(DISABLE_DEFAULT_SAVEFILEMANAGER) diff --git a/backends/saves/default/default-saves.h b/backends/saves/default/default-saves.h index bf4ca0229d..f2453810bf 100644 --- a/backends/saves/default/default-saves.h +++ b/backends/saves/default/default-saves.h @@ -27,7 +27,8 @@ #include "common/savefile.h" #include "common/str.h" #include "common/fs.h" -#include "common/hashmap.h" +#include "common/hash-str.h" +#include <limits.h> /** * Provides a default savefile manager implementation for common platforms. @@ -37,11 +38,24 @@ public: DefaultSaveFileManager(); DefaultSaveFileManager(const Common::String &defaultSavepath); + virtual void updateSavefilesList(Common::StringArray &lockedFiles); virtual Common::StringArray listSavefiles(const Common::String &pattern); + virtual Common::InSaveFile *openRawFile(const Common::String &filename); virtual Common::InSaveFile *openForLoading(const Common::String &filename); virtual Common::OutSaveFile *openForSaving(const Common::String &filename, bool compress = true); virtual bool removeSavefile(const Common::String &filename); +#ifdef USE_LIBCURL + + static const uint32 INVALID_TIMESTAMP = UINT_MAX; + static const char *TIMESTAMPS_FILENAME; + + static Common::HashMap<Common::String, uint32> loadTimestamps(); + static void saveTimestamps(Common::HashMap<Common::String, uint32> ×tamps); + static Common::String concatWithSavesPath(Common::String name); + +#endif + protected: /** * Get the path to the savegame directory. @@ -74,6 +88,12 @@ protected: */ SaveFileCache _saveFileCache; + /** + * List of "locked" files. These cannot be used for saving/loading + * because CloudManager is downloading those. + */ + Common::StringArray _lockedFiles; + private: /** * The currently cached directory. diff --git a/backends/saves/savefile.cpp b/backends/saves/savefile.cpp index b04c53d832..e21ea16ad8 100644 --- a/backends/saves/savefile.cpp +++ b/backends/saves/savefile.cpp @@ -23,9 +23,37 @@ #include "common/util.h" #include "common/savefile.h" #include "common/str.h" +#ifdef USE_LIBCURL +#include "backends/cloud/cloudmanager.h" +#endif namespace Common { +OutSaveFile::OutSaveFile(WriteStream *w): _wrapped(w) {} + +OutSaveFile::~OutSaveFile() {} + +bool OutSaveFile::err() const { return _wrapped->err(); } + +void OutSaveFile::clearErr() { _wrapped->clearErr(); } + +void OutSaveFile::finalize() { + _wrapped->finalize(); +#ifdef USE_LIBCURL + CloudMan.syncSaves(); +#endif +} + +bool OutSaveFile::flush() { return _wrapped->flush(); } + +uint32 OutSaveFile::write(const void *dataPtr, uint32 dataSize) { + return _wrapped->write(dataPtr, dataSize); +} + +int32 OutSaveFile::pos() const { + return _wrapped->pos(); +} + bool SaveFileManager::copySavefile(const String &oldFilename, const String &newFilename) { InSaveFile *inFile = 0; OutSaveFile *outFile = 0; diff --git a/base/main.cpp b/base/main.cpp index 1667106543..6b6868b78a 100644 --- a/base/main.cpp +++ b/base/main.cpp @@ -66,6 +66,13 @@ #endif #include "backends/keymapper/keymapper.h" +#ifdef USE_LIBCURL +#include "backends/cloud/cloudmanager.h" +#include "backends/networking/curl/connectionmanager.h" +#endif +#ifdef USE_SDL_NET +#include "backends/networking/sdl_net/localwebserver.h" +#endif #if defined(_WIN32_WCE) #include "backends/platform/wince/CELauncherDialog.h" @@ -475,6 +482,11 @@ extern "C" int scummvm_main(int argc, const char * const argv[]) { dlg.runModal(); } #endif + +#ifdef USE_LIBCURL + CloudMan.init(); + CloudMan.syncSaves(); +#endif // Unless a game was specified, show the launcher dialog if (0 == ConfMan.getActiveDomain()) @@ -584,6 +596,14 @@ extern "C" int scummvm_main(int argc, const char * const argv[]) { launcherDialog(); } } +#ifdef USE_SDL_NET + Networking::LocalWebserver::destroy(); +#endif +#ifdef USE_LIBCURL + Networking::ConnectionManager::destroy(); + //I think it's important to destroy it after ConnectionManager + Cloud::CloudManager::destroy(); +#endif PluginManager::instance().unloadAllPlugins(); PluginManager::destroy(); GUI::GuiManager::destroy(); diff --git a/base/version.cpp b/base/version.cpp index 299e4ce325..d40dd573f0 100644 --- a/base/version.cpp +++ b/base/version.cpp @@ -154,4 +154,25 @@ const char *gScummVMFeatures = "" #ifdef ENABLE_VKEYBD "virtual keyboard " #endif + +#ifdef USE_CLOUD + "cloud (" +#ifdef USE_LIBCURL + "servers" +#ifdef USE_SDL_NET + ", " +#endif +#endif +#ifdef USE_SDL_NET + "local" +#endif + ") " +#else +#ifdef USE_LIBCURL + "libcurl " +#endif +#ifdef USE_SDL_NET + "SDL_net " +#endif +#endif ; diff --git a/common/callback.h b/common/callback.h new file mode 100644 index 0000000000..c6c249a511 --- /dev/null +++ b/common/callback.h @@ -0,0 +1,138 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef COMMON_CALLBACK_H +#define COMMON_CALLBACK_H + +namespace Common { + +/** + * BaseCallback<S> is a simple base class for object-oriented callbacks. + * + * Object-oriented callbacks are such callbacks that know exact instance + * which method must be called. + * + * For backwards compatibility purposes, there is a GlobalFunctionCallback, + * which is BaseCallback<void *>, so it can be used with global C-like + * functions too. + * + * <S> is the type, which is passed to operator() of this callback. + * This allows you to specify that you accept a callback, which wants + * to receive an <S> object. + */ +template<typename S = void *> class BaseCallback { +public: + BaseCallback() {} + virtual ~BaseCallback() {} + virtual void operator()(S data) = 0; +}; + +/** + * GlobalFunctionCallback<T> is a simple wrapper for global C functions. + * + * If there is a method, which accepts BaseCallback<T>, you can + * easily pass your C function by passing + * new GlobalFunctionCallback<T>(yourFunction) + */ +template<typename T> class GlobalFunctionCallback: public BaseCallback<T> { + typedef void(*GlobalFunction)(T result); + GlobalFunction _callback; + +public: + GlobalFunctionCallback(GlobalFunction cb): _callback(cb) {} + virtual ~GlobalFunctionCallback() {} + virtual void operator()(T data) { + if (_callback) _callback(data); + } +}; + +/** + * Callback<T, S> implements an object-oriented callback. + * + * <T> stands for a class which method you want to call. + * <S>, again, is the type of an object passed to operator(). + * + * So, if you have void MyClass::myMethod(AnotherClass) method, + * the corresponding callback is Callback<MyClass, AnotherClass>. + * You create it similarly to this: + * new Callback<MyClass, AnotherClass>( + * pointerToMyClassObject, + * &MyClass::myMethod + * ) + */ +template<class T, typename S = void *> class Callback: public BaseCallback<S> { +protected: + typedef void(T::*TMethod)(S); + T *_object; + TMethod _method; +public: + Callback(T *object, TMethod method): _object(object), _method(method) {} + virtual ~Callback() {} + void operator()(S data) { (_object->*_method)(data); } +}; + +/** + * CallbackBridge<T, OS, S> helps you to chain callbacks. + * + * CallbackBridge keeps a pointer to BaseCallback<OS>. + * When its operator() is called, it passes this pointer + * along with the actual data (of type <S>) to the method + * of <T> class. + * + * This is needed when you have to call a callback only + * when your own callback is called. So, your callback + * is "inner" and the other one is "outer". + * + * CallbackBridge implements the "inner" one and calls + * the method you wanted. It passes the "outer", so you + * can call it from your method. You can ignore it, but + * probably there is no point in using CallbackBridge then. + * + * So, if you receive a BaseCallback<SomeClass> callback + * and you want to call it from your MyClass::myMethod method, + * you should create CallbackBridge<MyClass, SomeClass, S>, + * where <S> is data type you want to receive in MyClass::myMethod. + * + * You create it similarly to this: + * new Callback<MyClass, SomeClass, AnotherClass>( + * pointerToMyClassObject, + * &MyClass::myMethod, + * outerCallback + * ) + * where `outerCallback` is BaseCallback<SomeClass> and `myMethod` is: + * void MyClass::myMethod(BaseCallback<SomeClass> *, AnotherClass) + */ +template<class T, typename OS, typename S = void *> class CallbackBridge: public BaseCallback<S> { + typedef void(T::*TCallbackMethod)(BaseCallback<OS> *, S); + T *_object; + TCallbackMethod _method; + BaseCallback<OS> *_outerCallback; +public: + CallbackBridge(T *object, TCallbackMethod method, BaseCallback<OS> *outerCallback): + _object(object), _method(method), _outerCallback(outerCallback) {} + virtual ~CallbackBridge() {} + void operator()(S data) { (_object->*_method)(_outerCallback, data); } +}; + +} // End of namespace Common + +#endif diff --git a/common/config-manager.cpp b/common/config-manager.cpp index 24c877a4e9..fdd0c6f033 100644 --- a/common/config-manager.cpp +++ b/common/config-manager.cpp @@ -45,6 +45,10 @@ char const *const ConfigManager::kTransientDomain = "__TRANSIENT"; char const *const ConfigManager::kKeymapperDomain = "keymapper"; #endif +#ifdef USE_CLOUD +char const *const ConfigManager::kCloudDomain = "cloud"; +#endif + #pragma mark - @@ -67,6 +71,9 @@ void ConfigManager::copyFrom(ConfigManager &source) { #ifdef ENABLE_KEYMAPPER _keymapperDomain = source._keymapperDomain; #endif +#ifdef USE_CLOUD + _cloudDomain = source._cloudDomain; +#endif _domainSaveOrder = source._domainSaveOrder; _activeDomainName = source._activeDomainName; _activeDomain = &_gameDomains[_activeDomainName]; @@ -121,6 +128,10 @@ void ConfigManager::addDomain(const String &domainName, const ConfigManager::Dom } else if (domainName == kKeymapperDomain) { _keymapperDomain = domain; #endif +#ifdef USE_CLOUD + } else if (domainName == kCloudDomain) { + _cloudDomain = domain; +#endif } else if (domain.contains("gameid")) { // If the domain contains "gameid" we assume it's a game domain if (_gameDomains.contains(domainName)) @@ -160,6 +171,9 @@ void ConfigManager::loadFromStream(SeekableReadStream &stream) { #ifdef ENABLE_KEYMAPPER _keymapperDomain.clear(); #endif +#ifdef USE_CLOUD + _cloudDomain.clear(); +#endif // TODO: Detect if a domain occurs multiple times (or likewise, if // a key occurs multiple times inside one domain). @@ -272,6 +286,10 @@ void ConfigManager::flushToDisk() { // Write the keymapper domain writeDomain(*stream, kKeymapperDomain, _keymapperDomain); #endif +#ifdef USE_CLOUD + // Write the cloud domain + writeDomain(*stream, kCloudDomain, _cloudDomain); +#endif DomainMap::const_iterator d; @@ -359,6 +377,10 @@ const ConfigManager::Domain *ConfigManager::getDomain(const String &domName) con if (domName == kKeymapperDomain) return &_keymapperDomain; #endif +#ifdef USE_CLOUD + if (domName == kCloudDomain) + return &_cloudDomain; +#endif if (_gameDomains.contains(domName)) return &_gameDomains[domName]; if (_miscDomains.contains(domName)) @@ -379,6 +401,10 @@ ConfigManager::Domain *ConfigManager::getDomain(const String &domName) { if (domName == kKeymapperDomain) return &_keymapperDomain; #endif +#ifdef USE_CLOUD + if (domName == kCloudDomain) + return &_cloudDomain; +#endif if (_gameDomains.contains(domName)) return &_gameDomains[domName]; if (_miscDomains.contains(domName)) diff --git a/common/config-manager.h b/common/config-manager.h index 14f911f69d..669faaaf88 100644 --- a/common/config-manager.h +++ b/common/config-manager.h @@ -94,6 +94,11 @@ public: static char const *const kKeymapperDomain; #endif +#ifdef USE_CLOUD + /** The name of cloud domain used to store user's tokens */ + static char const *const kCloudDomain; +#endif + void loadDefaultConfigFile(); void loadConfigFile(const String &filename); @@ -188,6 +193,10 @@ private: Domain _keymapperDomain; #endif +#ifdef USE_CLOUD + Domain _cloudDomain; +#endif + Array<String> _domainSaveOrder; String _activeDomainName; diff --git a/common/file.cpp b/common/file.cpp index 4d9c630076..9797bcaa69 100644 --- a/common/file.cpp +++ b/common/file.cpp @@ -25,6 +25,8 @@ #include "common/file.h" #include "common/fs.h" #include "common/textconsole.h" +#include "common/system.h" +#include "backends/fs/fs-factory.h" namespace Common { @@ -149,10 +151,23 @@ DumpFile::~DumpFile() { close(); } -bool DumpFile::open(const String &filename) { +bool DumpFile::open(const String &filename, bool createPath) { assert(!filename.empty()); assert(!_handle); + if (createPath) { + for (uint32 i = 0; i < filename.size(); ++i) { + if (filename[i] == '/' || filename[i] == '\\') { + Common::String subpath = filename; + subpath.erase(i); + if (subpath.empty()) continue; + AbstractFSNode *node = g_system->getFilesystemFactory()->makeFileNodePath(subpath); + if (node->exists()) continue; + if (!node->create(true)) warning("DumpFile: unable to create directories from path prefix"); + } + } + } + FSNode node(filename); return open(node); } diff --git a/common/file.h b/common/file.h index 3d174834e9..8ad6249d6d 100644 --- a/common/file.h +++ b/common/file.h @@ -143,7 +143,7 @@ public: DumpFile(); virtual ~DumpFile(); - virtual bool open(const String &filename); + virtual bool open(const String &filename, bool createPath = false); virtual bool open(const FSNode &node); virtual void close(); diff --git a/common/json.cpp b/common/json.cpp new file mode 100644 index 0000000000..d0e585ac3d --- /dev/null +++ b/common/json.cpp @@ -0,0 +1,1099 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +/* +* Files JSON.cpp and JSONValue.cpp part of the SimpleJSON Library - http://mjpa.in/json +* +* Copyright (C) 2010 Mike Anchor +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +* THE SOFTWARE. +*/ + +#include "common/json.h" + +#ifdef __MINGW32__ +#define wcsncasecmp wcsnicmp +#endif + +// Macros to free an array/object +#define FREE_ARRAY(x) { JSONArray::iterator iter; for (iter = x.begin(); iter != x.end(); iter++) { delete *iter; } } +#define FREE_OBJECT(x) { JSONObject::iterator iter; for (iter = x.begin(); iter != x.end(); iter++) { delete (*iter)._value; } } + +namespace Common { + +/** +* Blocks off the public constructor +* +* @access private +* +*/ +JSON::JSON() {} + +/** +* Parses a complete JSON encoded string (UNICODE input version) +* +* @access public +* +* @param char* data The JSON text +* +* @return JSONValue* Returns a JSON Value representing the root, or NULL on error +*/ +JSONValue *JSON::parse(const char *data) { + // Skip any preceding whitespace, end of data = no JSON = fail + if (!skipWhitespace(&data)) + return NULL; + + // We need the start of a value here now... + JSONValue *value = JSONValue::parse(&data); + if (value == NULL) + return NULL; + + // Can be white space now and should be at the end of the string then... + if (skipWhitespace(&data)) { + delete value; + return NULL; + } + + // We're now at the end of the string + return value; +} + +/** +* Turns the passed in JSONValue into a JSON encode string +* +* @access public +* +* @param JSONValue* value The root value +* +* @return String Returns a JSON encoded string representation of the given value +*/ +String JSON::stringify(const JSONValue *value) { + if (value != NULL) + return value->stringify(); + else + return ""; +} + +/** +* Skips over any whitespace characters (space, tab, \r or \n) defined by the JSON spec +* +* @access protected +* +* @param char** data Pointer to a char* that contains the JSON text +* +* @return bool Returns true if there is more data, or false if the end of the text was reached +*/ +bool JSON::skipWhitespace(const char **data) { + while (**data != 0 && (**data == ' ' || **data == '\t' || **data == '\r' || **data == '\n')) + (*data)++; + + return **data != 0; +} + +/** +* Extracts a JSON String as defined by the spec - "<some chars>" +* Any escaped characters are swapped out for their unescaped values +* +* @access protected +* +* @param char** data Pointer to a char* that contains the JSON text +* @param String& str Reference to a String to receive the extracted string +* +* @return bool Returns true on success, false on failure +*/ +bool JSON::extractString(const char **data, String &str) { + str = ""; + + while (**data != 0) { + // Save the char so we can change it if need be + char next_char = **data; + + // Escaping something? + if (next_char == '\\') { + // Move over the escape char + (*data)++; + + // Deal with the escaped char + switch (**data) { + case '"': next_char = '"'; + break; + case '\\': next_char = '\\'; + break; + case '/': next_char = '/'; + break; + case 'b': next_char = '\b'; + break; + case 'f': next_char = '\f'; + break; + case 'n': next_char = '\n'; + break; + case 'r': next_char = '\r'; + break; + case 't': next_char = '\t'; + break; + case 'u': { + // We need 5 chars (4 hex + the 'u') or its not valid + if (!simplejson_wcsnlen(*data, 5)) + return false; + + // Deal with the chars + next_char = 0; + for (int i = 0; i < 4; i++) { + // Do it first to move off the 'u' and leave us on the + // final hex digit as we move on by one later on + (*data)++; + + next_char <<= 4; + + // Parse the hex digit + if (**data >= '0' && **data <= '9') + next_char |= (**data - '0'); + else if (**data >= 'A' && **data <= 'F') + next_char |= (10 + (**data - 'A')); + else if (**data >= 'a' && **data <= 'f') + next_char |= (10 + (**data - 'a')); + else { + // Invalid hex digit = invalid JSON + return false; + } + } + break; + } + + // By the spec, only the above cases are allowed + default: + return false; + } + } + + // End of the string? + else if (next_char == '"') { + (*data)++; + //str.reserve(); // Remove unused capacity //TODO + return true; + } + + // Disallowed char? + else if (next_char < ' ' && next_char != '\t') { + // SPEC Violation: Allow tabs due to real world cases + return false; + } + + // Add the next char + str += next_char; + + // Move on + (*data)++; + } + + // If we're here, the string ended incorrectly + return false; +} + +/** +* Parses some text as though it is an integer +* +* @access protected +* +* @param char** data Pointer to a char* that contains the JSON text +* +* @return double Returns the double value of the number found +*/ +double JSON::parseInt(const char **data) { + double integer = 0; + while (**data != 0 && **data >= '0' && **data <= '9') + integer = integer * 10 + (*(*data)++ - '0'); + + return integer; +} + +/** +* Parses some text as though it is a decimal +* +* @access protected +* +* @param char** data Pointer to a char* that contains the JSON text +* +* @return double Returns the double value of the decimal found +*/ +double JSON::parseDecimal(const char **data) { + double decimal = 0.0; + double factor = 0.1; + while (**data != 0 && **data >= '0' && **data <= '9') { + int digit = (*(*data)++ - '0'); + decimal = decimal + digit * factor; + factor *= 0.1; + } + return decimal; +} + +/** +* Parses a JSON encoded value to a JSONValue object +* +* @access protected +* +* @param char** data Pointer to a char* that contains the data +* +* @return JSONValue* Returns a pointer to a JSONValue object on success, NULL on error +*/ +JSONValue *JSONValue::parse(const char **data) { + // Is it a string? + if (**data == '"') { + String str; + if (!JSON::extractString(&(++(*data)), str)) + return NULL; + else + return new JSONValue(str); + } + + // Is it a boolean? + else if ((simplejson_wcsnlen(*data, 4) && scumm_strnicmp(*data, "true", 4) == 0) || (simplejson_wcsnlen(*data, 5) && scumm_strnicmp(*data, "false", 5) == 0)) { + bool value = scumm_strnicmp(*data, "true", 4) == 0; + (*data) += value ? 4 : 5; + return new JSONValue(value); + } + + // Is it a null? + else if (simplejson_wcsnlen(*data, 4) && scumm_strnicmp(*data, "null", 4) == 0) { + (*data) += 4; + return new JSONValue(); + } + + // Is it a number? + else if (**data == '-' || (**data >= '0' && **data <= '9')) { + // Negative? + bool neg = **data == '-'; + if (neg) (*data)++; + + long long int integer = 0; + double number = 0.0; + bool onlyInteger = true; + + // Parse the whole part of the number - only if it wasn't 0 + if (**data == '0') + (*data)++; + else if (**data >= '1' && **data <= '9') + number = integer = JSON::parseInt(data); + else + return NULL; + + // Could be a decimal now... + if (**data == '.') { + (*data)++; + + // Not get any digits? + if (!(**data >= '0' && **data <= '9')) + return NULL; + + // Find the decimal and sort the decimal place out + // Use ParseDecimal as ParseInt won't work with decimals less than 0.1 + // thanks to Javier Abadia for the report & fix + double decimal = JSON::parseDecimal(data); + + // Save the number + number += decimal; + onlyInteger = false; + } + + // Could be an exponent now... + if (**data == 'E' || **data == 'e') { + (*data)++; + + // Check signage of expo + bool neg_expo = false; + if (**data == '-' || **data == '+') { + neg_expo = **data == '-'; + (*data)++; + } + + // Not get any digits? + if (!(**data >= '0' && **data <= '9')) + return NULL; + + // Sort the expo out + double expo = JSON::parseInt(data); + for (double i = 0.0; i < expo; i++) + number = neg_expo ? (number / 10.0) : (number * 10.0); + onlyInteger = false; + } + + // Was it neg? + if (neg) number *= -1; + + if (onlyInteger) + return new JSONValue(neg ? -integer : integer); + + return new JSONValue(number); + } + + // An object? + else if (**data == '{') { + JSONObject object; + + (*data)++; + + while (**data != 0) { + // Whitespace at the start? + if (!JSON::skipWhitespace(data)) { + FREE_OBJECT(object); + return NULL; + } + + // Special case - empty object + if (object.size() == 0 && **data == '}') { + (*data)++; + return new JSONValue(object); + } + + // We want a string now... + String name; + if (!JSON::extractString(&(++(*data)), name)) { + FREE_OBJECT(object); + return NULL; + } + + // More whitespace? + if (!JSON::skipWhitespace(data)) { + FREE_OBJECT(object); + return NULL; + } + + // Need a : now + if (*((*data)++) != ':') { + FREE_OBJECT(object); + return NULL; + } + + // More whitespace? + if (!JSON::skipWhitespace(data)) { + FREE_OBJECT(object); + return NULL; + } + + // The value is here + JSONValue *value = parse(data); + if (value == NULL) { + FREE_OBJECT(object); + return NULL; + } + + // Add the name:value + if (object.find(name) != object.end()) + delete object[name]; + object[name] = value; + + // More whitespace? + if (!JSON::skipWhitespace(data)) { + FREE_OBJECT(object); + return NULL; + } + + // End of object? + if (**data == '}') { + (*data)++; + return new JSONValue(object); + } + + // Want a , now + if (**data != ',') { + FREE_OBJECT(object); + return NULL; + } + + (*data)++; + } + + // Only here if we ran out of data + FREE_OBJECT(object); + return NULL; + } + + // An array? + else if (**data == '[') { + JSONArray array; + + (*data)++; + + while (**data != 0) { + // Whitespace at the start? + if (!JSON::skipWhitespace(data)) { + FREE_ARRAY(array); + return NULL; + } + + // Special case - empty array + if (array.size() == 0 && **data == ']') { + (*data)++; + return new JSONValue(array); + } + + // Get the value + JSONValue *value = parse(data); + if (value == NULL) { + FREE_ARRAY(array); + return NULL; + } + + // Add the value + array.push_back(value); + + // More whitespace? + if (!JSON::skipWhitespace(data)) { + FREE_ARRAY(array); + return NULL; + } + + // End of array? + if (**data == ']') { + (*data)++; + return new JSONValue(array); + } + + // Want a , now + if (**data != ',') { + FREE_ARRAY(array); + return NULL; + } + + (*data)++; + } + + // Only here if we ran out of data + FREE_ARRAY(array); + return NULL; + } + + // Ran out of possibilites, it's bad! + else { + return NULL; + } +} + +/** +* Basic constructor for creating a JSON Value of type NULL +* +* @access public +*/ +JSONValue::JSONValue(/*NULL*/) { + _type = JSONType_Null; +} + +/** +* Basic constructor for creating a JSON Value of type String +* +* @access public +* +* @param char* m_char_value The string to use as the value +*/ +JSONValue::JSONValue(const char *charValue) { + _type = JSONType_String; + _stringValue = new String(String(charValue)); +} + +/** +* Basic constructor for creating a JSON Value of type String +* +* @access public +* +* @param String m_string_value The string to use as the value +*/ +JSONValue::JSONValue(const String &stringValue) { + _type = JSONType_String; + _stringValue = new String(stringValue); +} + +/** +* Basic constructor for creating a JSON Value of type Bool +* +* @access public +* +* @param bool m_bool_value The bool to use as the value +*/ +JSONValue::JSONValue(bool boolValue) { + _type = JSONType_Bool; + _boolValue = boolValue; +} + +/** +* Basic constructor for creating a JSON Value of type Number +* +* @access public +* +* @param double m_number_value The number to use as the value +*/ +JSONValue::JSONValue(double numberValue) { + _type = JSONType_Number; + _numberValue = numberValue; +} + +/** +* Basic constructor for creating a JSON Value of type Number (Integer) +* +* @access public +* +* @param int numberValue The number to use as the value +*/ +JSONValue::JSONValue(long long int numberValue) { + _type = JSONType_IntegerNumber; + _integerValue = numberValue; +} + +/** +* Basic constructor for creating a JSON Value of type Array +* +* @access public +* +* @param JSONArray m_array_value The JSONArray to use as the value +*/ +JSONValue::JSONValue(const JSONArray &arrayValue) { + _type = JSONType_Array; + _arrayValue = new JSONArray(arrayValue); +} + +/** +* Basic constructor for creating a JSON Value of type Object +* +* @access public +* +* @param JSONObject m_object_value The JSONObject to use as the value +*/ +JSONValue::JSONValue(const JSONObject &objectValue) { + _type = JSONType_Object; + _objectValue = new JSONObject(objectValue); +} + +/** +* Copy constructor to perform a deep copy of array / object values +* +* @access public +* +* @param JSONValue m_source The source JSONValue that is being copied +*/ +JSONValue::JSONValue(const JSONValue &source) { + _type = source._type; + + switch (_type) { + case JSONType_String: + _stringValue = new String(*source._stringValue); + break; + + case JSONType_Bool: + _boolValue = source._boolValue; + break; + + case JSONType_Number: + _numberValue = source._numberValue; + break; + + case JSONType_IntegerNumber: + _integerValue = source._integerValue; + break; + + case JSONType_Array: { + JSONArray source_array = *source._arrayValue; + JSONArray::iterator iter; + _arrayValue = new JSONArray(); + for (iter = source_array.begin(); iter != source_array.end(); iter++) + _arrayValue->push_back(new JSONValue(**iter)); + break; + } + + case JSONType_Object: { + JSONObject source_object = *source._objectValue; + _objectValue = new JSONObject(); + JSONObject::iterator iter; + for (iter = source_object.begin(); iter != source_object.end(); iter++) { + String name = (*iter)._key; + (*_objectValue)[name] = new JSONValue(*((*iter)._value)); + } + break; + } + + case JSONType_Null: + // Nothing to do. + break; + } +} + +/** +* The destructor for the JSON Value object +* Handles deleting the objects in the array or the object value +* +* @access public +*/ +JSONValue::~JSONValue() { + if (_type == JSONType_Array) { + JSONArray::iterator iter; + for (iter = _arrayValue->begin(); iter != _arrayValue->end(); iter++) + delete *iter; + delete _arrayValue; + } else if (_type == JSONType_Object) { + JSONObject::iterator iter; + for (iter = _objectValue->begin(); iter != _objectValue->end(); iter++) { + delete (*iter)._value; + } + delete _objectValue; + } else if (_type == JSONType_String) { + delete _stringValue; + } +} + +/** +* Checks if the value is a NULL +* +* @access public +* +* @return bool Returns true if it is a NULL value, false otherwise +*/ +bool JSONValue::isNull() const { + return _type == JSONType_Null; +} + +/** +* Checks if the value is a String +* +* @access public +* +* @return bool Returns true if it is a String value, false otherwise +*/ +bool JSONValue::isString() const { + return _type == JSONType_String; +} + +/** +* Checks if the value is a Bool +* +* @access public +* +* @return bool Returns true if it is a Bool value, false otherwise +*/ +bool JSONValue::isBool() const { + return _type == JSONType_Bool; +} + +/** +* Checks if the value is a Number +* +* @access public +* +* @return bool Returns true if it is a Number value, false otherwise +*/ +bool JSONValue::isNumber() const { + return _type == JSONType_Number; +} + +/** +* Checks if the value is an Integer +* +* @access public +* +* @return bool Returns true if it is an Integer value, false otherwise +*/ +bool JSONValue::isIntegerNumber() const { + return _type == JSONType_IntegerNumber; +} + +/** +* Checks if the value is an Array +* +* @access public +* +* @return bool Returns true if it is an Array value, false otherwise +*/ +bool JSONValue::isArray() const { + return _type == JSONType_Array; +} + +/** +* Checks if the value is an Object +* +* @access public +* +* @return bool Returns true if it is an Object value, false otherwise +*/ +bool JSONValue::isObject() const { + return _type == JSONType_Object; +} + +/** +* Retrieves the String value of this JSONValue +* Use isString() before using this method. +* +* @access public +* +* @return String Returns the string value +*/ +const String &JSONValue::asString() const { + return (*_stringValue); +} + +/** +* Retrieves the Bool value of this JSONValue +* Use isBool() before using this method. +* +* @access public +* +* @return bool Returns the bool value +*/ +bool JSONValue::asBool() const { + return _boolValue; +} + +/** +* Retrieves the Number value of this JSONValue +* Use isNumber() before using this method. +* +* @access public +* +* @return double Returns the number value +*/ +double JSONValue::asNumber() const { + return _numberValue; +} + +/** +* Retrieves the Integer value of this JSONValue +* Use isIntegerNumber() before using this method. +* +* @access public +* +* @return int Returns the number value +*/ +long long int JSONValue::asIntegerNumber() const { + return _integerValue; +} + +/** +* Retrieves the Array value of this JSONValue +* Use isArray() before using this method. +* +* @access public +* +* @return JSONArray Returns the array value +*/ +const JSONArray &JSONValue::asArray() const { + return (*_arrayValue); +} + +/** +* Retrieves the Object value of this JSONValue +* Use isObject() before using this method. +* +* @access public +* +* @return JSONObject Returns the object value +*/ +const JSONObject &JSONValue::asObject() const { + return (*_objectValue); +} + +/** +* Retrieves the number of children of this JSONValue. +* This number will be 0 or the actual number of children +* if isArray() or isObject(). +* +* @access public +* +* @return The number of children. +*/ +std::size_t JSONValue::countChildren() const { + switch (_type) { + case JSONType_Array: + return _arrayValue->size(); + case JSONType_Object: + return _objectValue->size(); + default: + return 0; + } +} + +/** +* Checks if this JSONValue has a child at the given index. +* Use isArray() before using this method. +* +* @access public +* +* @return bool Returns true if the array has a value at the given index. +*/ +bool JSONValue::hasChild(std::size_t index) const { + if (_type == JSONType_Array) { + return index < _arrayValue->size(); + } else { + return false; + } +} + +/** +* Retrieves the child of this JSONValue at the given index. +* Use isArray() before using this method. +* +* @access public +* +* @return JSONValue* Returns JSONValue at the given index or NULL +* if it doesn't exist. +*/ +JSONValue *JSONValue::child(std::size_t index) { + if (index < _arrayValue->size()) { + return (*_arrayValue)[index]; + } else { + return NULL; + } +} + +/** +* Checks if this JSONValue has a child at the given key. +* Use isObject() before using this method. +* +* @access public +* +* @return bool Returns true if the object has a value at the given key. +*/ +bool JSONValue::hasChild(const char *name) const { + if (_type == JSONType_Object) { + return _objectValue->find(name) != _objectValue->end(); + } else { + return false; + } +} + +/** +* Retrieves the child of this JSONValue at the given key. +* Use isObject() before using this method. +* +* @access public +* +* @return JSONValue* Returns JSONValue for the given key in the object +* or NULL if it doesn't exist. +*/ +JSONValue *JSONValue::child(const char *name) { + JSONObject::const_iterator it = _objectValue->find(name); + if (it != _objectValue->end()) { + return it->_value; + } else { + return NULL; + } +} + +/** +* Retrieves the keys of the JSON Object or an empty vector +* if this value is not an object. +* +* @access public +* +* @return std::vector<String> A vector containing the keys. +*/ +Array<String> JSONValue::objectKeys() const { + Array<String> keys; + + if (_type == JSONType_Object) { + JSONObject::const_iterator iter = _objectValue->begin(); + while (iter != _objectValue->end()) { + keys.push_back(iter->_key); + + iter++; + } + } + + return keys; +} + +/** +* Creates a JSON encoded string for the value with all necessary characters escaped +* +* @access public +* +* @param bool prettyprint Enable prettyprint +* +* @return String Returns the JSON string +*/ +String JSONValue::stringify(bool const prettyprint) const { + size_t const indentDepth = prettyprint ? 1 : 0; + return stringifyImpl(indentDepth); +} + + +/** +* Creates a JSON encoded string for the value with all necessary characters escaped +* +* @access private +* +* @param size_t indentDepth The prettyprint indentation depth (0 : no prettyprint) +* +* @return String Returns the JSON string +*/ +String JSONValue::stringifyImpl(size_t const indentDepth) const { + String ret_string; + size_t const indentDepth1 = indentDepth ? indentDepth + 1 : 0; + String const indentStr = indent(indentDepth); + String const indentStr1 = indent(indentDepth1); + + switch (_type) { + case JSONType_Null: + ret_string = "null"; + break; + + case JSONType_String: + ret_string = stringifyString(*_stringValue); + break; + + case JSONType_Bool: + ret_string = _boolValue ? "true" : "false"; + break; + + case JSONType_Number: { + if (isinf(_numberValue) || isnan(_numberValue)) + ret_string = "null"; + else { + char str[80]; + sprintf(str, "%lg", _numberValue); + ret_string = str; + } + break; + } + + case JSONType_IntegerNumber: { + char str[80]; + sprintf(str, "%lld", _integerValue); + ret_string = str; + break; + } + + case JSONType_Array: { + ret_string = indentDepth ? "[\n" + indentStr1 : "["; + JSONArray::const_iterator iter = _arrayValue->begin(); + while (iter != _arrayValue->end()) { + ret_string += (*iter)->stringifyImpl(indentDepth1); + + // Not at the end - add a separator + if (++iter != _arrayValue->end()) + ret_string += ","; + } + ret_string += indentDepth ? "\n" + indentStr + "]" : "]"; + break; + } + + case JSONType_Object: { + ret_string = indentDepth ? "{\n" + indentStr1 : "{"; + JSONObject::const_iterator iter = _objectValue->begin(); + while (iter != _objectValue->end()) { + ret_string += stringifyString((*iter)._key); + ret_string += ":"; + ret_string += (*iter)._value->stringifyImpl(indentDepth1); + + // Not at the end - add a separator + if (++iter != _objectValue->end()) + ret_string += ","; + } + ret_string += indentDepth ? "\n" + indentStr + "}" : "}"; + break; + } + } + + return ret_string; +} + +/** +* Creates a JSON encoded string with all required fields escaped +* Works from http://www.ecma-internationl.org/publications/files/ECMA-ST/ECMA-262.pdf +* Section 15.12.3. +* +* @access private +* +* @param String str The string that needs to have the characters escaped +* +* @return String Returns the JSON string +*/ +String JSONValue::stringifyString(const String &str) { + String str_out = "\""; + + String::const_iterator iter = str.begin(); + while (iter != str.end()) { + char chr = *iter; + + if (chr == '"' || chr == '\\' || chr == '/') { + str_out += '\\'; + str_out += chr; + } else if (chr == '\b') { + str_out += "\\b"; + } else if (chr == '\f') { + str_out += "\\f"; + } else if (chr == '\n') { + str_out += "\\n"; + } else if (chr == '\r') { + str_out += "\\r"; + } else if (chr == '\t') { + str_out += "\\t"; + } else if (chr < ' ' || chr > 126) { + str_out += "\\u"; + for (int i = 0; i < 4; i++) { + int value = (chr >> 12) & 0xf; + if (value >= 0 && value <= 9) + str_out += (char)('0' + value); + else if (value >= 10 && value <= 15) + str_out += (char)('A' + (value - 10)); + chr <<= 4; + } + } else { + str_out += chr; + } + + iter++; + } + + str_out += "\""; + return str_out; +} + +/** +* Creates the indentation string for the depth given +* +* @access private +* +* @param size_t indent The prettyprint indentation depth (0 : no indentation) +* +* @return String Returns the string +*/ +String JSONValue::indent(size_t depth) { + const size_t indent_step = 2; + depth ? --depth : 0; + String indentStr; + for (int i = 0; i < depth * indent_step; ++i) indentStr += ' '; + return indentStr; +} + +} // End of namespace Common diff --git a/common/json.h b/common/json.h new file mode 100644 index 0000000000..d9bbbdb77c --- /dev/null +++ b/common/json.h @@ -0,0 +1,166 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +/* +* Files JSON.h and JSONValue.h part of the SimpleJSON Library - http://mjpa.in/json +* +* Copyright (C) 2010 Mike Anchor +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +* THE SOFTWARE. +*/ + +#ifndef COMMON_JSON_H +#define COMMON_JSON_H + +#include "common/array.h" +#include "common/hashmap.h" +#include "common/hash-str.h" +#include "common/str.h" + +// Win32 incompatibilities +#if defined(WIN32) && !defined(__GNUC__) +static inline bool isnan(double x) { + return x != x; +} + +static inline bool isinf(double x) { + return !isnan(x) && isnan(x - x); +} +#endif + +// Simple function to check a string 's' has at least 'n' characters +static inline bool simplejson_wcsnlen(const char *s, size_t n) { + if (s == 0) + return false; + + const char *save = s; + while (n-- > 0) { + if (*(save++) == 0) return false; + } + + return true; +} + +namespace Common { + +// Custom types +class JSONValue; +typedef Array<JSONValue*> JSONArray; +typedef HashMap<String, JSONValue*> JSONObject; + +class JSON; + +enum JSONType { JSONType_Null, JSONType_String, JSONType_Bool, JSONType_Number, JSONType_IntegerNumber, JSONType_Array, JSONType_Object }; + +class JSONValue { + friend class JSON; + +public: + JSONValue(/*NULL*/); + JSONValue(const char *charValue); + JSONValue(const String &stringValue); + JSONValue(bool boolValue); + JSONValue(double numberValue); + JSONValue(long long int numberValue); + JSONValue(const JSONArray &arrayValue); + JSONValue(const JSONObject &objectValue); + JSONValue(const JSONValue &source); + ~JSONValue(); + + bool isNull() const; + bool isString() const; + bool isBool() const; + bool isNumber() const; + bool isIntegerNumber() const; + bool isArray() const; + bool isObject() const; + + const String &asString() const; + bool asBool() const; + double asNumber() const; + long long int asIntegerNumber() const; + const JSONArray &asArray() const; + const JSONObject &asObject() const; + + size_t countChildren() const; + bool hasChild(size_t index) const; + JSONValue *child(size_t index); + bool hasChild(const char *name) const; + JSONValue *child(const char *name); + Array<String> objectKeys() const; + + String stringify(bool const prettyprint = false) const; +protected: + static JSONValue *parse(const char **data); + +private: + static String stringifyString(const String &str); + String stringifyImpl(size_t const indentDepth) const; + static String indent(size_t depth); + + JSONType _type; + + union { + bool _boolValue; + double _numberValue; + long long int _integerValue; + String *_stringValue; + JSONArray *_arrayValue; + JSONObject *_objectValue; + }; + +}; + +class JSON { + friend class JSONValue; + +public: + static JSONValue *parse(const char *data); + static String stringify(const JSONValue *value); +protected: + static bool skipWhitespace(const char **data); + static bool extractString(const char **data, String &str); + static double parseInt(const char **data); + static double parseDecimal(const char **data); +private: + JSON(); +}; + +} // End of namespace Common + +#endif diff --git a/common/memstream.h b/common/memstream.h index 59d5f15b1a..25fdde91c7 100644 --- a/common/memstream.h +++ b/common/memstream.h @@ -209,6 +209,89 @@ public: bool seek(int32 offset, int whence = SEEK_SET); }; +/** +* MemoryStream based on RingBuffer. Grows if has insufficient buffer size. +*/ +class MemoryReadWriteStream : public WriteStream { +private: + uint32 _capacity; + uint32 _size; + byte *_data; + uint32 _writePos, _readPos, _pos, _length; + DisposeAfterUse::Flag _disposeMemory; + + void ensureCapacity(uint32 new_len) { + if (new_len <= _capacity) + return; + + byte *old_data = _data; + uint32 oldCapacity = _capacity; + + _capacity = MAX(new_len + 32, _capacity * 2); + _data = (byte *)malloc(_capacity); + + if (old_data) { + // Copy old data + if (_readPos < _writePos) { + memcpy(_data, old_data + _readPos, _writePos - _readPos); + _writePos = _length; + _readPos = 0; + } else { + memcpy(_data, old_data + _readPos, oldCapacity - _readPos); + memcpy(_data + oldCapacity - _readPos, old_data, _writePos); + _writePos = _length; + _readPos = 0; + } + free(old_data); + } + } +public: + MemoryReadWriteStream(DisposeAfterUse::Flag disposeMemory = DisposeAfterUse::NO) : _capacity(0), _size(0), _data(0), _writePos(0), _readPos(0), _pos(0), _length(0), _disposeMemory(disposeMemory) {} + + ~MemoryReadWriteStream() { + if (_disposeMemory) + free(_data); + } + + uint32 write(const void *dataPtr, uint32 dataSize) { + ensureCapacity(_length + dataSize); + if (_writePos + dataSize < _capacity) { + memcpy(_data + _writePos, dataPtr, dataSize); + } else { + memcpy(_data + _writePos, dataPtr, _capacity - _writePos); + const byte *shiftedPtr = (const byte *)dataPtr + _capacity - _writePos; + memcpy(_data, shiftedPtr, dataSize - (_capacity - _writePos)); + } + _writePos = (_writePos + dataSize) % _capacity; + _pos += dataSize; + _length += dataSize; + if (_pos > _size) + _size = _pos; + return dataSize; + } + + virtual uint32 read(void *dataPtr, uint32 dataSize) { + uint32 length = _length; + if (length < dataSize) dataSize = length; + if (dataSize == 0 || _capacity == 0) return 0; + if (_readPos + dataSize < _capacity) { + memcpy(dataPtr, _data + _readPos, dataSize); + } else { + memcpy(dataPtr, _data + _readPos, _capacity - _readPos); + byte *shiftedPtr = (byte *)dataPtr + _capacity - _readPos; + memcpy(shiftedPtr, _data, dataSize - (_capacity - _readPos)); + } + _readPos = (_readPos + dataSize) % _capacity; + _length -= dataSize; + return dataSize; + } + + int32 pos() const { return _pos - _length; } //'read' position in the stream + uint32 size() const { return _size; } //that's also 'write' position in the stream, as it's append-only + + byte *getData() { return _data; } +}; + } // End of namespace Common #endif diff --git a/common/module.mk b/common/module.mk index 570040c8e1..54aa16f557 100644 --- a/common/module.mk +++ b/common/module.mk @@ -16,6 +16,7 @@ MODULE_OBJS := \ iff_container.o \ ini-file.o \ installshield_cab.o \ + json.o \ language.o \ localization.o \ macresman.o \ diff --git a/common/savefile.h b/common/savefile.h index 9fca07f9d5..eb7e6f946e 100644 --- a/common/savefile.h +++ b/common/savefile.h @@ -28,6 +28,7 @@ #include "common/stream.h" #include "common/str-array.h" #include "common/error.h" +#include "common/ptr.h" namespace Common { @@ -44,8 +45,21 @@ typedef SeekableReadStream InSaveFile; * That typically means "save games", but also includes things like the * IQ points in Indy3. */ -typedef WriteStream OutSaveFile; +class OutSaveFile: public WriteStream { +protected: + ScopedPtr<WriteStream> _wrapped; +public: + OutSaveFile(WriteStream *w); + virtual ~OutSaveFile(); + + virtual bool err() const; + virtual void clearErr(); + virtual void finalize(); + virtual bool flush(); + virtual uint32 write(const void *dataPtr, uint32 dataSize); + virtual int32 pos() const; +}; /** * The SaveFileManager is serving as a factory for InSaveFile @@ -137,6 +151,15 @@ public: virtual InSaveFile *openForLoading(const String &name) = 0; /** + * Open the file with the specified name in the given directory for loading. + * In contrast to openForLoading(), it returns raw file instead of unpacked. + * + * @param name The name of the savefile. + * @return Pointer to an InSaveFile, or NULL if an error occurred. + */ + virtual InSaveFile *openRawFile(const String &name) = 0; + + /** * Removes the given savefile from the system. * * @param name The name of the savefile to be removed. @@ -174,6 +197,13 @@ public: * @see Common::matchString() */ virtual StringArray listSavefiles(const String &pattern) = 0; + + /** + * Refreshes the save files list (because some new files could've been added) + * and remembers the "locked" files list. These files could not be used + * for saving or loading because they are being synced by CloudManager. + */ + virtual void updateSavefilesList(StringArray &lockedFiles) = 0; }; } // End of namespace Common diff --git a/common/str.cpp b/common/str.cpp index 3ff49a90c6..90bd539790 100644 --- a/common/str.cpp +++ b/common/str.cpp @@ -335,6 +335,15 @@ bool String::contains(char x) const { return strchr(c_str(), x) != NULL; } +uint64 String::asUint64() const { + uint64 result = 0; + for (uint32 i = 0; i < _size; ++i) { + if (_str[i] < '0' || _str[i] > '9') break; + result = result * 10L + (_str[i] - '0'); + } + return result; +} + bool String::matchString(const char *pat, bool ignoreCase, bool pathMode) const { return Common::matchString(c_str(), pat, ignoreCase, pathMode); } @@ -829,6 +838,15 @@ bool matchString(const char *str, const char *pat, bool ignoreCase, bool pathMod } } +void replace(Common::String &source, const Common::String &what, const Common::String &with) { + const char *cstr = source.c_str(); + const char *position = strstr(cstr, what.c_str()); + if (position) { + uint32 index = position - cstr; + source.replace(index, what.size(), with); + } +} + String tag2string(uint32 tag) { char str[5]; str[0] = (char)(tag >> 24); diff --git a/common/str.h b/common/str.h index 9ada8aaaa0..d55ba072a9 100644 --- a/common/str.h +++ b/common/str.h @@ -162,6 +162,9 @@ public: bool contains(const char *x) const; bool contains(char x) const; + /** Return uint64 corrensponding to String's contents. */ + uint64 asUint64() const; + /** * Simple DOS-style pattern matching function (understands * and ? like used in DOS). * Taken from exult/files/listfiles.cc @@ -233,12 +236,12 @@ public: void trim(); uint hash() const; - + /**@{ * Functions to replace some amount of chars with chars from some other string. * * @note The implementation follows that of the STL's std::string: - * http://www.cplusplus.com/reference/string/string/replace/ + * http://www.cplusplus.com/reference/string/string/replace/ * * @param pos Starting position for the replace in the original string. * @param count Number of chars to replace from the original string. @@ -247,7 +250,7 @@ public: * @param countOri Same as count * @param posDest Initial position to read str from. * @param countDest Number of chars to read from str. npos by default. - */ + */ // Replace 'count' bytes, starting from 'pos' with str. void replace(uint32 pos, uint32 count, const String &str); // The same as above, but accepts a C-like array of characters. @@ -264,7 +267,7 @@ public: // str[posDest, posDest + countDest) void replace(uint32 posOri, uint32 countOri, const char *str, uint32 posDest, uint32 countDest); - /**@}*/ + /**@}*/ /** * Print formatted data into a String object. Similar to sprintf, @@ -387,6 +390,15 @@ String normalizePath(const String &path, const char sep); */ bool matchString(const char *str, const char *pat, bool ignoreCase = false, bool pathMode = false); +/** + * Function which replaces substring with the other. It happens in place. + * If there is no substring found, original string is not changed. + * + * @param source String to search and replace substring in. + * @param what Substring to replace. + * @param with String to replace with. + */ +void replace(Common::String &source, const Common::String &what, const Common::String &with); /** * Take a 32 bit value and turn it into a four character string, where each of diff --git a/common/system.h b/common/system.h index 6d185d3075..805eba68ed 100644 --- a/common/system.h +++ b/common/system.h @@ -314,7 +314,15 @@ public: * * This feature has no associated state. */ - kFeatureDisplayLogFile + kFeatureDisplayLogFile, + + /** + * The presence of this feature indicates whether the hasTextInClipboard() + * and getTextFromClipboard() calls are supported. + * + * This feature has no associated state. + */ + kFeatureClipboardSupport }; /** @@ -1086,6 +1094,45 @@ public: virtual void displayMessageOnOSD(const char *msg) = 0; /** + * Blit a bitmap to the 'on screen display'. + * + * If the current pixel format has one byte per pixel, the graphics data + * uses 8 bits per pixel, using the palette specified via setPalette. + * If more than one byte per pixel is in use, the graphics data uses the + * pixel format returned by getScreenFormat. + * + * @param buf the buffer containing the graphics data source + * @param pitch the pitch of the buffer (number of bytes in a scanline) + * @param x the x coordinate of the destination rectangle + * @param y the y coordinate of the destination rectangle + * @param w the width of the destination rectangle + * @param h the height of the destination rectangle + * + * @note The specified destination rectangle must be completly contained + * in the visible screen space, and must be non-empty. If not, a + * backend may or may not perform clipping, trigger an assert or + * silently corrupt memory. + * + * @see updateScreen + * @see getScreenFormat + * @see copyRectToScreen + */ + + virtual void copyRectToOSD(const void *buf, int pitch, int x, int y, int w, int h) = 0; + + /** + * Clears 'on screen display' from everything drawn on it. + */ + + virtual void clearOSD() = 0; + + /** + * Returns 'on screen display' pixel format. + */ + + virtual Graphics::PixelFormat getOSDFormat() = 0; + + /** * Return the SaveFileManager, used to store and load savestates * and other modifiable persistent game data. For more information, * refer to the SaveFileManager documentation. @@ -1200,6 +1247,28 @@ public: virtual bool displayLogFile() { return false; } /** + * Returns whether there is text available in the clipboard. + * + * The kFeatureClipboardSupport feature flag can be used to + * test whether this call has been implemented by the active + * backend. + * + * @return true if there is text in the clipboard, false otherwise + */ + virtual bool hasTextInClipboard() { return false; } + + /** + * Returns clipboard contents as a String. + * + * The kFeatureClipboardSupport feature flag can be used to + * test whether this call has been implemented by the active + * backend. + * + * @return clipboard contents ("" if hasTextInClipboard() == false) + */ + virtual Common::String getTextFromClipboard() { return ""; } + + /** * Returns the locale of the system. * * This returns the currently set up locale of the system, on which diff --git a/common/xmlparser.cpp b/common/xmlparser.cpp index da4f577e3c..4470180710 100644 --- a/common/xmlparser.cpp +++ b/common/xmlparser.cpp @@ -128,7 +128,7 @@ bool XMLParser::parserError(const String &errStr) { while (currentPosition--) errorMessage += (char)_stream->readByte(); } - + errorMessage += "\n\nParser error: "; errorMessage += errStr; errorMessage += "\n\n"; @@ -117,6 +117,8 @@ done # # Default lib behavior yes/no/auto _vorbis=auto +_sdlnet=auto +_libcurl=auto _tremor=auto _tremolo=no _flac=auto @@ -153,6 +155,7 @@ _build_hq_scalers=yes _enable_prof=no _global_constructors=no _bink=yes +_cloud=auto # Default vkeybd/keymapper/eventrec options _vkeybd=no _keymapper=no @@ -185,9 +188,11 @@ _staticlibpath= _xcodetoolspath= _sparklepath= _sdlconfig=sdl2-config +_libcurlconfig=curl-config _freetypeconfig=freetype-config _sdlpath="$PATH" _freetypepath="$PATH" +_libcurlpath="$PATH" _nasmpath="$PATH" NASMFLAGS="" NASM="" @@ -201,6 +206,7 @@ _have_x86=no # Add (virtual) features add_feature 16bit "16bit color" "_16bit" +add_feature cloud "cloud" "_cloud" add_feature faad "libfaad" "_faad" add_feature flac "FLAC" "_flac" add_feature freetype2 "FreeType2" "_freetype2" @@ -433,6 +439,41 @@ find_freetypeconfig() { } # +# Determine curl-config +# +find_libcurlconfig() { + echo_n "Looking for curl-config... " + libcurlconfigs="$_libcurlconfig" + _libcurlconfig= + + IFS="${IFS= }"; ac_save_ifs="$IFS"; IFS="$SEPARATOR" + for path_dir in $_libcurlpath; do + #reset separator to parse sdlconfigs + IFS=":" + for libcurlconfig in $libcurlconfigs; do + if test -f "$path_dir/$libcurlconfig" ; then + _libcurlconfig="$path_dir/$libcurlconfig" + echo $_libcurlconfig + # Save the prefix + _libcurlpath=$path_dir + if test `basename $path_dir` = bin ; then + _sdlpath=`dirname $path_dir` + fi + # break at first sdl-config found in path + break 2 + fi + done + done + + IFS="$ac_save_ifs" + + if test -z "$_libcurlconfig"; then + echo "none found!" + exit 1 + fi +} + +# # Determine extension used for executables # get_system_exe_extension() { @@ -922,6 +963,7 @@ Optional Features: --disable-hq-scalers exclude HQ2x and HQ3x scalers --disable-translation don't build support for translated messages --disable-taskbar don't build support for taskbar and launcher integration + --disable-cloud don't build cloud support --enable-vkeybd build virtual keyboard support --enable-keymapper build key mapper support --enable-eventrecorder enable event recording functionality @@ -1004,6 +1046,11 @@ Optional Libraries: --with-sndio-prefix=DIR Prefix where sndio is installed (optional) --disable-sndio disable sndio MIDI driver [autodetect] + --with-sdlnet-prefix=DIR Prefix where SDL_Net is + installed (optional) + --disable-sdlnet disable SDL_Net networking library [autodetect] + --disable-libcurl disable libcurl networking library [autodetect] + Some influential environment variables: LDFLAGS linker flags, e.g. -L<lib dir> if you have libraries in a nonstandard directory <lib dir> @@ -1082,6 +1129,12 @@ for ac_option in $@; do --disable-freetype2) _freetype2=no ;; --enable-taskbar) _taskbar=yes ;; --disable-taskbar) _taskbar=no ;; + --enable-sdlnet) _sdlnet=yes ;; + --disable-sdlnet) _sdlnet=no ;; + --enable-libcurl) _libcurl=yes ;; + --disable-libcurl) _libcurl=no ;; + --enable-cloud) _cloud=yes ;; + --disable-cloud) _cloud=no ;; --enable-updates) _updates=yes ;; --disable-updates) _updates=no ;; --enable-libunity) _libunity=yes ;; @@ -1190,6 +1243,11 @@ for ac_option in $@; do LIBUNITY_CFLAGS="-I$arg/include" LIBUNITY_LIBS="-L$arg/lib" ;; + --with-sdlnet-prefix=*) + arg=`echo $ac_option | cut -d '=' -f 2` + SDL_NET_CFLAGS="-I$arg/include" + SDL_NET_LIBS="-L$arg/lib" + ;; --backend=*) _backend=`echo $ac_option | cut -d '=' -f 2` ;; @@ -4072,6 +4130,110 @@ EOF esac # +# Check for SDL_Net +# +echocheck "SDL_Net" +if test "$_sdlnet" = auto ; then + _sdlnet=no + cat > $TMPC << EOF +#include "SDL/SDL_net.h" +int main(int argc, char *argv[]) { SDLNet_Init(); return 0; } +EOF + cc_check $LIBS $INCLUDES $SDL_NET_CFLAGS $SDL_NET_LIBS -lSDL_net && _sdlnet=yes +fi +if test "$_sdlnet" = yes ; then + append_var LIBS "$SDL_NET_LIBS -lSDL_net" + append_var INCLUDES "$SDL_NET_CFLAGS" +fi +define_in_config_if_yes "$_sdlnet" 'USE_SDL_NET' +echo "$_sdlnet" + +# +# Check for libcurl to be present +# +if test "$_libcurl" != "no"; then + + # Look for the curl-config script + find_libcurlconfig + + if test -z "$_libcurlconfig"; then + _libcurl=no + else + LIBCURL_LIBS=`$_libcurlconfig --libs` + LIBCURL_CFLAGS=`$_libcurlconfig --cflags` + + if test "$_libcurl" = "auto"; then + _libcurl=no + + cat > $TMPC << EOF + #include <curl/curl.h> + int main(int argc, char *argv[]) { + int x; + curl_easy_setopt(NULL,CURLOPT_URL,NULL); + x=CURL_ERROR_SIZE; + x=CURLOPT_WRITEFUNCTION; + x=CURLOPT_WRITEDATA; + x=CURLOPT_ERRORBUFFER; + x=CURLOPT_STDERR; + x=CURLOPT_VERBOSE; + + curl_version_info_data *data = curl_version_info(CURLVERSION_NOW); + if (data->features & CURL_VERSION_SSL) + return 0; + return 1; + } +EOF + + cc_check_no_clean $LIBCURL_CFLAGS $LIBCURL_LIBS + if test "$?" -eq 0; then + $TMPO$HOSTEXEEXT + if test "$?" -eq 0; then + _libcurl=yes + else + _libcurl="no SSL support" + fi + fi + cc_check_clean + fi + + if test "$_libcurl" = "yes"; then + append_var LIBS "$LIBCURL_LIBS" + append_var INCLUDES "$LIBCURL_CFLAGS" + fi + fi + +fi + +echocheck "libcurl" +echo "$_libcurl" + +define_in_config_if_yes "$_libcurl" "USE_LIBCURL" + +# +# Check whether to build cloud integration support +# +echo_n "Cloud integration..." +if test "$_cloud" = "no"; then + echo "no" +else + _cloud=no + if test "$_sdlnet" = "yes"; then + _cloud=yes + echo_n "local" + fi + if test "$_libcurl" = "yes"; then + if test "$_cloud" = "yes"; then echo_n ", "; fi + _cloud=yes + echo_n "servers" + fi + if test "$_cloud" = "no"; then + echo_n "no" + fi + echo # newline +fi +define_in_config_if_yes $_cloud 'USE_CLOUD' + +# # Check is NSDockTilePlugIn protocol is supported # case $_host_os in @@ -4652,7 +4814,11 @@ if test "$_keymapper" = yes ; then fi if test "$_eventrec" = yes ; then - echo ", event recorder" + echo_n ", event recorder" +fi + +if test "$_cloud" = yes ; then + echo ", cloud" else echo fi diff --git a/devtools/create_project/create_project.cpp b/devtools/create_project/create_project.cpp index 70781ffb55..91690c2128 100644 --- a/devtools/create_project/create_project.cpp +++ b/devtools/create_project/create_project.cpp @@ -1000,10 +1000,12 @@ const Feature s_features[] = { { "png", "USE_PNG", "libpng16", true, "libpng support" }, { "faad", "USE_FAAD", "libfaad", false, "AAC support" }, { "mpeg2", "USE_MPEG2", "libmpeg2", false, "MPEG-2 support" }, - { "theora", "USE_THEORADEC", "libtheora_static", true, "Theora decoding support" }, - { "freetype", "USE_FREETYPE2", "freetype", true, "FreeType support" }, - { "jpeg", "USE_JPEG", "jpeg-static", true, "libjpeg support" }, - {"fluidsynth", "USE_FLUIDSYNTH", "libfluidsynth", true, "FluidSynth support" }, + { "theora", "USE_THEORADEC", "libtheora_static", true, "Theora decoding support" }, + { "freetype", "USE_FREETYPE2", "freetype", true, "FreeType support" }, + { "jpeg", "USE_JPEG", "jpeg-static", true, "libjpeg support" }, + {"fluidsynth", "USE_FLUIDSYNTH", "libfluidsynth", true, "FluidSynth support" }, + { "libcurl", "USE_LIBCURL", "libcurl", true, "libcurl support" }, + { "sdlnet", "USE_SDL_NET", "SDL_net", true, "SDL_net support" }, // Feature flags { "bink", "USE_BINK", "", true, "Bink video support" }, @@ -1015,6 +1017,7 @@ const Feature s_features[] = { { "opengl", "USE_OPENGL", "", true, "OpenGL support" }, { "opengles", "USE_GLES", "", true, "forced OpenGL ES mode" }, { "taskbar", "USE_TASKBAR", "", true, "Taskbar integration support" }, + { "cloud", "USE_CLOUD", "", true, "Cloud integration support" }, { "translation", "USE_TRANSLATION", "", true, "Translation support" }, { "vkeybd", "ENABLE_VKEYBD", "", false, "Virtual keyboard support"}, { "keymapper", "ENABLE_KEYMAPPER", "", false, "Keymapper support"}, diff --git a/dists/android/AndroidManifest.xml b/dists/android/AndroidManifest.xml index c091039266..64870a459b 100644 --- a/dists/android/AndroidManifest.xml +++ b/dists/android/AndroidManifest.xml @@ -40,6 +40,8 @@ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> + <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" android:required="true"/> + <uses-feature android:name="android.hardware.screen.landscape" android:required="false" /> diff --git a/dists/android/AndroidManifest.xml.in b/dists/android/AndroidManifest.xml.in index 7eaece9d1f..9601425c74 100644 --- a/dists/android/AndroidManifest.xml.in +++ b/dists/android/AndroidManifest.xml.in @@ -40,6 +40,8 @@ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> + <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" android:required="true"/> + <uses-feature android:name="android.hardware.screen.landscape" android:required="false" /> diff --git a/dists/cloudicon.png b/dists/cloudicon.png Binary files differnew file mode 100644 index 0000000000..21373aea1a --- /dev/null +++ b/dists/cloudicon.png diff --git a/dists/cloudicon_disabled.png b/dists/cloudicon_disabled.png Binary files differnew file mode 100644 index 0000000000..27c9600b0a --- /dev/null +++ b/dists/cloudicon_disabled.png diff --git a/dists/scummvm.rc b/dists/scummvm.rc index 873feaa419..00d71ef717 100644 --- a/dists/scummvm.rc +++ b/dists/scummvm.rc @@ -19,6 +19,9 @@ scummmodern.zip FILE "gui/themes/scummmodern.zip" #ifdef USE_TRANSLATION translations.dat FILE "gui/themes/translations.dat" #endif +#ifdef USE_SDL_NET +wwwroot.zip FILE "backends/networking/wwwroot.zip" +#endif #if ENABLE_ACCESS == STATIC_PLUGIN access.dat FILE "dists/engine-data/access.dat" diff --git a/engines/access/detection.cpp b/engines/access/detection.cpp index 368753f117..3e70de3635 100644 --- a/engines/access/detection.cpp +++ b/engines/access/detection.cpp @@ -111,7 +111,8 @@ bool AccessMetaEngine::hasFeature(MetaEngineFeature f) const { (f == kSupportsLoadingDuringStartup) || (f == kSupportsDeleteSave) || (f == kSavesSupportMetaInfo) || - (f == kSavesSupportThumbnail); + (f == kSavesSupportThumbnail) || + (f == kSimpleSavesNames); } bool Access::AccessEngine::hasFeature(EngineFeature f) const { diff --git a/engines/adl/detection.cpp b/engines/adl/detection.cpp index be9165b127..10812d79ea 100644 --- a/engines/adl/detection.cpp +++ b/engines/adl/detection.cpp @@ -205,6 +205,7 @@ bool AdlMetaEngine::hasFeature(MetaEngineFeature f) const { case kSavesSupportThumbnail: case kSavesSupportCreationDate: case kSavesSupportPlayTime: + case kSimpleSavesNames: return true; default: return false; diff --git a/engines/agi/detection.cpp b/engines/agi/detection.cpp index 9f66d78d80..5cb239f8d8 100644 --- a/engines/agi/detection.cpp +++ b/engines/agi/detection.cpp @@ -231,7 +231,8 @@ bool AgiMetaEngine::hasFeature(MetaEngineFeature f) const { (f == kSavesSupportMetaInfo) || (f == kSavesSupportThumbnail) || (f == kSavesSupportCreationDate) || - (f == kSavesSupportPlayTime); + (f == kSavesSupportPlayTime) || + (f == kSimpleSavesNames); } bool AgiBase::hasFeature(EngineFeature f) const { diff --git a/engines/agos/detection.cpp b/engines/agos/detection.cpp index 2c89522089..dbc4ee9145 100644 --- a/engines/agos/detection.cpp +++ b/engines/agos/detection.cpp @@ -125,7 +125,8 @@ public: bool AgosMetaEngine::hasFeature(MetaEngineFeature f) const { return - (f == kSupportsListSaves); + (f == kSupportsListSaves) || + (f == kSimpleSavesNames); } bool AGOS::AGOSEngine::hasFeature(EngineFeature f) const { diff --git a/engines/avalanche/detection.cpp b/engines/avalanche/detection.cpp index e35c5d2cac..def395b77f 100644 --- a/engines/avalanche/detection.cpp +++ b/engines/avalanche/detection.cpp @@ -99,7 +99,8 @@ bool AvalancheMetaEngine::hasFeature(MetaEngineFeature f) const { (f == kSupportsDeleteSave) || (f == kSupportsLoadingDuringStartup) || (f == kSavesSupportMetaInfo) || - (f == kSavesSupportThumbnail); + (f == kSavesSupportThumbnail) || + (f == kSimpleSavesNames); } SaveStateList AvalancheMetaEngine::listSaves(const char *target) const { diff --git a/engines/bbvs/detection.cpp b/engines/bbvs/detection.cpp index 7c0045ee73..fa735c9ec3 100644 --- a/engines/bbvs/detection.cpp +++ b/engines/bbvs/detection.cpp @@ -97,7 +97,8 @@ bool BbvsMetaEngine::hasFeature(MetaEngineFeature f) const { (f == kSupportsLoadingDuringStartup) || (f == kSavesSupportMetaInfo) || (f == kSavesSupportThumbnail) || - (f == kSavesSupportCreationDate); + (f == kSavesSupportCreationDate) || + (f == kSimpleSavesNames); } void BbvsMetaEngine::removeSaveState(const char *target, int slot) const { diff --git a/engines/cge/detection.cpp b/engines/cge/detection.cpp index 82d27f8d54..0df1e8711e 100644 --- a/engines/cge/detection.cpp +++ b/engines/cge/detection.cpp @@ -180,7 +180,8 @@ bool CGEMetaEngine::hasFeature(MetaEngineFeature f) const { (f == kSupportsDeleteSave) || (f == kSavesSupportMetaInfo) || (f == kSavesSupportThumbnail) || - (f == kSavesSupportCreationDate); + (f == kSavesSupportCreationDate) || + (f == kSimpleSavesNames); } void CGEMetaEngine::removeSaveState(const char *target, int slot) const { diff --git a/engines/cge2/detection.cpp b/engines/cge2/detection.cpp index 2b84d167c7..3701baa40f 100644 --- a/engines/cge2/detection.cpp +++ b/engines/cge2/detection.cpp @@ -185,7 +185,8 @@ bool CGE2MetaEngine::hasFeature(MetaEngineFeature f) const { (f == kSavesSupportThumbnail) || (f == kSavesSupportCreationDate) || (f == kSupportsListSaves) || - (f == kSupportsLoadingDuringStartup); + (f == kSupportsLoadingDuringStartup) || + (f == kSimpleSavesNames); } int CGE2MetaEngine::getMaximumSaveSlot() const { diff --git a/engines/drascula/detection.cpp b/engines/drascula/detection.cpp index ffec393a0a..3bc8069b76 100644 --- a/engines/drascula/detection.cpp +++ b/engines/drascula/detection.cpp @@ -339,7 +339,8 @@ bool DrasculaMetaEngine::hasFeature(MetaEngineFeature f) const { (f == kSavesSupportMetaInfo) || (f == kSavesSupportThumbnail) || (f == kSavesSupportCreationDate) || - (f == kSavesSupportPlayTime); + (f == kSavesSupportPlayTime) || + (f == kSimpleSavesNames); } const ExtraGuiOptions DrasculaMetaEngine::getExtraGuiOptions(const Common::String &target) const { diff --git a/engines/gnap/detection.cpp b/engines/gnap/detection.cpp index 7e4ab56d1f..d92a037232 100644 --- a/engines/gnap/detection.cpp +++ b/engines/gnap/detection.cpp @@ -89,7 +89,8 @@ bool GnapMetaEngine::hasFeature(MetaEngineFeature f) const { (f == kSupportsDeleteSave) || (f == kSavesSupportMetaInfo) || (f == kSavesSupportThumbnail) || - (f == kSavesSupportCreationDate); + (f == kSavesSupportCreationDate) || + (f == kSimpleSavesNames); } bool Gnap::GnapEngine::hasFeature(EngineFeature f) const { diff --git a/engines/hopkins/detection.cpp b/engines/hopkins/detection.cpp index cfdbf8030c..041afecaa8 100644 --- a/engines/hopkins/detection.cpp +++ b/engines/hopkins/detection.cpp @@ -128,7 +128,8 @@ bool HopkinsMetaEngine::hasFeature(MetaEngineFeature f) const { (f == kSupportsLoadingDuringStartup) || (f == kSupportsDeleteSave) || (f == kSavesSupportMetaInfo) || - (f == kSavesSupportThumbnail); + (f == kSavesSupportThumbnail) || + (f == kSimpleSavesNames); } bool Hopkins::HopkinsEngine::hasFeature(EngineFeature f) const { diff --git a/engines/kyra/detection.cpp b/engines/kyra/detection.cpp index 989a45b420..70c7e7c93c 100644 --- a/engines/kyra/detection.cpp +++ b/engines/kyra/detection.cpp @@ -173,7 +173,8 @@ bool KyraMetaEngine::hasFeature(MetaEngineFeature f) const { (f == kSupportsLoadingDuringStartup) || (f == kSupportsDeleteSave) || (f == kSavesSupportMetaInfo) || - (f == kSavesSupportThumbnail); + (f == kSavesSupportThumbnail) || + (f == kSimpleSavesNames); } bool Kyra::KyraEngine_v1::hasFeature(EngineFeature f) const { diff --git a/engines/kyra/kyra_v1.h b/engines/kyra/kyra_v1.h index c801829855..4de7494510 100644 --- a/engines/kyra/kyra_v1.h +++ b/engines/kyra/kyra_v1.h @@ -38,6 +38,7 @@ #include "kyra/item.h" namespace Common { +class OutSaveFile; class SeekableReadStream; class WriteStream; } // End of namespace Common @@ -423,7 +424,7 @@ protected: virtual Common::Error saveGameStateIntern(int slot, const char *saveName, const Graphics::Surface *thumbnail) = 0; Common::SeekableReadStream *openSaveForReading(const char *filename, SaveHeader &header, bool checkID = true); - Common::WriteStream *openSaveForWriting(const char *filename, const char *saveName, const Graphics::Surface *thumbnail) const; + Common::OutSaveFile *openSaveForWriting(const char *filename, const char *saveName, const Graphics::Surface *thumbnail) const; // TODO: Consider moving this to Screen virtual Graphics::Surface *generateSaveThumbnail() const { return 0; } diff --git a/engines/kyra/saveload.cpp b/engines/kyra/saveload.cpp index 2ae6420bd7..81ea796fe9 100644 --- a/engines/kyra/saveload.cpp +++ b/engines/kyra/saveload.cpp @@ -184,7 +184,7 @@ Common::SeekableReadStream *KyraEngine_v1::openSaveForReading(const char *filena return in; } -Common::WriteStream *KyraEngine_v1::openSaveForWriting(const char *filename, const char *saveName, const Graphics::Surface *thumbnail) const { +Common::OutSaveFile *KyraEngine_v1::openSaveForWriting(const char *filename, const char *saveName, const Graphics::Surface *thumbnail) const { if (shouldQuit()) return 0; @@ -226,7 +226,7 @@ Common::WriteStream *KyraEngine_v1::openSaveForWriting(const char *filename, con delete genThumbnail; } - return out; + return new Common::OutSaveFile(out); } const char *KyraEngine_v1::getSavegameFilename(int num) { diff --git a/engines/kyra/saveload_eob.cpp b/engines/kyra/saveload_eob.cpp index cca8f3a0a4..9329d14255 100644 --- a/engines/kyra/saveload_eob.cpp +++ b/engines/kyra/saveload_eob.cpp @@ -995,7 +995,7 @@ bool EoBCoreEngine::saveAsOriginalSaveFile(int slot) { return false; Common::FSNode nf = nd.getChild(_flags.gameID == GI_EOB1 ? "EOBDATA.SAV" : Common::String::format("EOBDATA%d.SAV", slot)); - Common::WriteStream *out = nf.createWriteStream(); + Common::OutSaveFile *out = new Common::OutSaveFile(nf.createWriteStream()); if (_flags.gameID == GI_EOB2) { static const char tempStr[20] = "SCUMMVM EXPORT "; diff --git a/engines/lab/detection.cpp b/engines/lab/detection.cpp index 30890b5acf..bf6d4563b5 100644 --- a/engines/lab/detection.cpp +++ b/engines/lab/detection.cpp @@ -151,7 +151,8 @@ bool LabMetaEngine::hasFeature(MetaEngineFeature f) const { (f == kSavesSupportMetaInfo) || (f == kSavesSupportThumbnail) || (f == kSavesSupportCreationDate) || - (f == kSavesSupportPlayTime); + (f == kSavesSupportPlayTime) || + (f == kSimpleSavesNames); } bool Lab::LabEngine::hasFeature(EngineFeature f) const { diff --git a/engines/mads/detection.cpp b/engines/mads/detection.cpp index 4736503a38..4fb8b82eb3 100644 --- a/engines/mads/detection.cpp +++ b/engines/mads/detection.cpp @@ -166,7 +166,8 @@ bool MADSMetaEngine::hasFeature(MetaEngineFeature f) const { (f == kSupportsLoadingDuringStartup) || (f == kSupportsDeleteSave) || (f == kSavesSupportMetaInfo) || - (f == kSavesSupportThumbnail); + (f == kSavesSupportThumbnail) || + (f == kSimpleSavesNames); } bool MADS::MADSEngine::hasFeature(EngineFeature f) const { diff --git a/engines/metaengine.h b/engines/metaengine.h index e7bfebab71..568b66ac51 100644 --- a/engines/metaengine.h +++ b/engines/metaengine.h @@ -236,7 +236,19 @@ public: * the game till the save. * This flag may only be set when 'kSavesSupportMetaInfo' is set. */ - kSavesSupportPlayTime + kSavesSupportPlayTime, + + /** + * Feature is available if engine's saves could be detected + * with "<target>.###" pattern and "###" corresponds to slot + * number. + * + * If that's not true or engine is using some unusual way + * of detecting saves and slot numbers, this should be + * unavailable. In that case Save/Load dialog for engine's + * games is locked during cloud saves sync. + */ + kSimpleSavesNames }; /** diff --git a/engines/mortevielle/detection.cpp b/engines/mortevielle/detection.cpp index 6791707dd5..7bc5339179 100644 --- a/engines/mortevielle/detection.cpp +++ b/engines/mortevielle/detection.cpp @@ -90,6 +90,7 @@ bool MortevielleMetaEngine::hasFeature(MetaEngineFeature f) const { case kSavesSupportMetaInfo: case kSavesSupportThumbnail: case kSavesSupportCreationDate: + case kSimpleSavesNames: return true; default: return false; diff --git a/engines/neverhood/detection.cpp b/engines/neverhood/detection.cpp index 0f409a6435..0c0347ef13 100644 --- a/engines/neverhood/detection.cpp +++ b/engines/neverhood/detection.cpp @@ -228,7 +228,8 @@ bool NeverhoodMetaEngine::hasFeature(MetaEngineFeature f) const { (f == kSavesSupportMetaInfo) || (f == kSavesSupportThumbnail) || (f == kSavesSupportCreationDate) || - (f == kSavesSupportPlayTime); + (f == kSavesSupportPlayTime) || + (f == kSimpleSavesNames); } bool Neverhood::NeverhoodEngine::hasFeature(EngineFeature f) const { diff --git a/engines/prince/detection.cpp b/engines/prince/detection.cpp index 1c6f63aff3..ad759823d8 100644 --- a/engines/prince/detection.cpp +++ b/engines/prince/detection.cpp @@ -56,7 +56,8 @@ bool PrinceMetaEngine::hasFeature(MetaEngineFeature f) const { (f == kSavesSupportThumbnail) || (f == kSavesSupportCreationDate) || (f == kSupportsListSaves) || - (f == kSupportsLoadingDuringStartup); + (f == kSupportsLoadingDuringStartup) || + (f == kSimpleSavesNames); } bool Prince::PrinceEngine::hasFeature(EngineFeature f) const { diff --git a/engines/saga/detection.cpp b/engines/saga/detection.cpp index 0677e84d67..6fe4277c27 100644 --- a/engines/saga/detection.cpp +++ b/engines/saga/detection.cpp @@ -157,7 +157,8 @@ bool SagaMetaEngine::hasFeature(MetaEngineFeature f) const { (f == kSavesSupportMetaInfo) || (f == kSavesSupportThumbnail) || (f == kSavesSupportCreationDate) || - (f == kSavesSupportPlayTime); + (f == kSavesSupportPlayTime) || + (f == kSimpleSavesNames); } bool Saga::SagaEngine::hasFeature(EngineFeature f) const { diff --git a/engines/savestate.cpp b/engines/savestate.cpp index 186d7bc5f2..7366aa6a61 100644 --- a/engines/savestate.cpp +++ b/engines/savestate.cpp @@ -27,12 +27,12 @@ SaveStateDescriptor::SaveStateDescriptor() // FIXME: default to 0 (first slot) or to -1 (invalid slot) ? : _slot(-1), _description(), _isDeletable(true), _isWriteProtected(false), - _saveDate(), _saveTime(), _playTime(), _thumbnail() { + _isLocked(false), _saveDate(), _saveTime(), _playTime(), _thumbnail() { } SaveStateDescriptor::SaveStateDescriptor(int s, const Common::String &d) : _slot(s), _description(d), _isDeletable(true), _isWriteProtected(false), - _saveDate(), _saveTime(), _playTime(), _thumbnail() { + _isLocked(false), _saveDate(), _saveTime(), _playTime(), _thumbnail() { } void SaveStateDescriptor::setThumbnail(Graphics::Surface *t) { diff --git a/engines/savestate.h b/engines/savestate.h index 21ade602fa..3244d61fdb 100644 --- a/engines/savestate.h +++ b/engines/savestate.h @@ -90,6 +90,24 @@ public: bool getWriteProtectedFlag() const { return _isWriteProtected; } /** + * Defines whether the save state is "locked" because is being synced. + */ + void setLocked(bool state) { + _isLocked = state; + + //just in case: + if (state) { + setDeletableFlag(false); + setWriteProtectedFlag(true); + } + } + + /** + * Queries whether the save state is "locked" because is being synced. + */ + bool getLocked() const { return _isLocked; } + + /** * Return a thumbnail graphics surface representing the savestate visually. * This is usually a scaled down version of the game graphics. The size * should be either 160x100 or 160x120 pixels, depending on the aspect @@ -180,6 +198,11 @@ private: bool _isWriteProtected; /** + * Whether the save state is "locked" because is being synced. + */ + bool _isLocked; + + /** * Human readable description of the date the save state was created. */ Common::String _saveDate; diff --git a/engines/scumm/detection.cpp b/engines/scumm/detection.cpp index 4c9d1221aa..e6740df482 100644 --- a/engines/scumm/detection.cpp +++ b/engines/scumm/detection.cpp @@ -974,7 +974,8 @@ bool ScummMetaEngine::hasFeature(MetaEngineFeature f) const { (f == kSavesSupportMetaInfo) || (f == kSavesSupportThumbnail) || (f == kSavesSupportCreationDate) || - (f == kSavesSupportPlayTime); + (f == kSavesSupportPlayTime) || + (f == kSimpleSavesNames); } bool ScummEngine::hasFeature(EngineFeature f) const { diff --git a/engines/sherlock/detection.cpp b/engines/sherlock/detection.cpp index 5a94b3485f..c6e632f999 100644 --- a/engines/sherlock/detection.cpp +++ b/engines/sherlock/detection.cpp @@ -199,7 +199,8 @@ bool SherlockMetaEngine::hasFeature(MetaEngineFeature f) const { (f == kSupportsLoadingDuringStartup) || (f == kSupportsDeleteSave) || (f == kSavesSupportMetaInfo) || - (f == kSavesSupportThumbnail); + (f == kSavesSupportThumbnail) || + (f == kSimpleSavesNames); } bool Sherlock::SherlockEngine::hasFeature(EngineFeature f) const { diff --git a/engines/sky/detection.cpp b/engines/sky/detection.cpp index 4b91f50a61..d86689e5d7 100644 --- a/engines/sky/detection.cpp +++ b/engines/sky/detection.cpp @@ -78,7 +78,7 @@ public: virtual bool hasFeature(MetaEngineFeature f) const; virtual GameList getSupportedGames() const; virtual const ExtraGuiOptions getExtraGuiOptions(const Common::String &target) const; - virtual GameDescriptor findGame(const char *gameid) const; + virtual GameDescriptor findGame(const char *gameid) const; virtual GameList detectGames(const Common::FSList &fslist) const; virtual Common::Error createInstance(OSystem *syst, Engine **engine) const; diff --git a/engines/sword2/sword2.cpp b/engines/sword2/sword2.cpp index 44371bf6cf..4f3caa437e 100644 --- a/engines/sword2/sword2.cpp +++ b/engines/sword2/sword2.cpp @@ -107,7 +107,8 @@ bool Sword2MetaEngine::hasFeature(MetaEngineFeature f) const { return (f == kSupportsListSaves) || (f == kSupportsLoadingDuringStartup) || - (f == kSupportsDeleteSave); + (f == kSupportsDeleteSave) || + (f == kSimpleSavesNames); } bool Sword2::Sword2Engine::hasFeature(EngineFeature f) const { diff --git a/engines/testbed/cloud.cpp b/engines/testbed/cloud.cpp new file mode 100644 index 0000000000..a2d62733be --- /dev/null +++ b/engines/testbed/cloud.cpp @@ -0,0 +1,565 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "common/config-manager.h" +#include "common/stream.h" +#include "common/util.h" +#include "testbed/fs.h" +#include "testbed/cloud.h" +#include <backends/cloud/cloudmanager.h> + +namespace Testbed { + +CloudTestSuite::CloudTestSuite() { + // Cloud tests depend on CloudMan. + // If there is no Storage connected to it, disable this test suite. + if (CloudMan.getCurrentStorage() == nullptr) { + logPrintf("WARNING! : No Storage connected to CloudMan found. Skipping Cloud tests\n"); + Testsuite::enable(false); + } + + addTest("UserInfo", &CloudTests::testInfo, true); + addTest("ListDirectory", &CloudTests::testDirectoryListing, true); + addTest("CreateDirectory", &CloudTests::testDirectoryCreating, true); + addTest("FileUpload", &CloudTests::testUploading, true); + addTest("FileDownload", &CloudTests::testDownloading, true); + addTest("FolderDownload", &CloudTests::testFolderDownloading, true); + addTest("SyncSaves", &CloudTests::testSavesSync, true); +} + +/* +void CloudTestSuite::enable(bool flag) { + Testsuite::enable(ConfParams.isGameDataFound() ? flag : false); +} +*/ + +///// TESTS GO HERE ///// + +bool CloudTests::waitForCallback() { + const int TIMEOUT = 30; + + Common::Point pt; + pt.x = 10; pt.y = 10; + Testsuite::writeOnScreen("Waiting for callback...", pt); + + int left = TIMEOUT; + while (--left) { + if (ConfParams.isCloudTestCallbackCalled()) return true; + if (ConfParams.isCloudTestErrorCallbackCalled()) return true; + g_system->delayMillis(1000); + } + return false; +} + +bool CloudTests::waitForCallbackMore() { + while (!waitForCallback()) { + Common::String info = "It takes more time than expected. Do you want to skip the test or wait more?"; + if (Testsuite::handleInteractiveInput(info, "Wait", "Skip", kOptionRight)) { + Testsuite::logPrintf("Info! Skipping test : info()\n"); + return false; + } + } + return true; +} + +const char *CloudTests::getRemoteTestPath() { + if (CloudMan.getStorageIndex() == Cloud::kStorageDropboxId) + return "/testbed"; + return "testbed"; +} + +void CloudTests::infoCallback(Cloud::Storage::StorageInfoResponse response) { + ConfParams.setCloudTestCallbackCalled(true); + Testsuite::logPrintf("Info! User's ID: %s\n", response.value.uid().c_str()); + Testsuite::logPrintf("Info! User's email: %s\n", response.value.email().c_str()); + Testsuite::logPrintf("Info! User's name: %s\n", response.value.name().c_str()); + Testsuite::logPrintf("Info! User's quota: %lu bytes used / %lu bytes available\n", response.value.used(), response.value.available()); +} + +void CloudTests::directoryListedCallback(Cloud::Storage::FileArrayResponse response) { + ConfParams.setCloudTestCallbackCalled(true); + if (response.value.size() == 0) { + Testsuite::logPrintf("Warning! Directory is empty!\n"); + return; + } + + Common::String directory, file; + uint32 directories = 0, files = 0; + for (uint32 i = 0; i < response.value.size(); ++i) { + if (response.value[i].isDirectory()) { + if (++directories == 1) directory = response.value[i].path(); + } else { + if (++files == 1) file = response.value[i].name(); + } + } + + if (directories == 0) { + Testsuite::logPrintf("Info! %u files listed, first one is '%s'\n", files, file.c_str()); + } else if (files == 0) { + Testsuite::logPrintf("Info! %u directories listed, first one is '%s'\n", directories, directory.c_str()); + } else { + Testsuite::logPrintf("Info! %u directories and %u files listed\n", directories, files); + Testsuite::logPrintf("Info! First directory is '%s' and first file is '%s'\n", directory.c_str(), file.c_str()); + } +} + +void CloudTests::directoryCreatedCallback(Cloud::Storage::BoolResponse response) { + ConfParams.setCloudTestCallbackCalled(true); + if (response.value) { + Testsuite::logPrintf("Info! Directory created!\n"); + } else { + Testsuite::logPrintf("Info! Such directory already exists!\n"); + } +} + +void CloudTests::fileUploadedCallback(Cloud::Storage::UploadResponse response) { + ConfParams.setCloudTestCallbackCalled(true); + Testsuite::logPrintf("Info! Uploaded file into '%s'\n", response.value.path().c_str()); + Testsuite::logPrintf("Info! It's id = '%s' and size = '%u'\n", response.value.id().c_str(), response.value.size()); +} + +void CloudTests::fileDownloadedCallback(Cloud::Storage::BoolResponse response) { + ConfParams.setCloudTestCallbackCalled(true); + if (response.value) { + Testsuite::logPrintf("Info! File downloaded!\n"); + } else { + Testsuite::logPrintf("Info! Failed to download the file!\n"); + } +} + +void CloudTests::directoryDownloadedCallback(Cloud::Storage::FileArrayResponse response) { + ConfParams.setCloudTestCallbackCalled(true); + if (response.value.size() == 0) { + Testsuite::logPrintf("Info! Directory is downloaded successfully!\n"); + } else { + Testsuite::logPrintf("Warning! %u files were not downloaded during folder downloading!\n", response.value.size()); + } +} + +void CloudTests::savesSyncedCallback(Cloud::Storage::BoolResponse response) { + ConfParams.setCloudTestCallbackCalled(true); + if (response.value) { + Testsuite::logPrintf("Info! Saves are synced successfully!\n"); + } else { + Testsuite::logPrintf("Warning! Saves were not synced!\n"); + } +} + +void CloudTests::errorCallback(Networking::ErrorResponse response) { + ConfParams.setCloudTestErrorCallbackCalled(true); + Testsuite::logPrintf("Info! Error Callback was called\n"); + Testsuite::logPrintf("Info! code = %ld, message = %s\n", response.httpResponseCode, response.response.c_str()); +} + +/** This test calls Storage::info(). */ + +TestExitStatus CloudTests::testInfo() { + ConfParams.setCloudTestCallbackCalled(false); + ConfParams.setCloudTestErrorCallbackCalled(false); + + if (CloudMan.getCurrentStorage() == nullptr) { + Testsuite::logPrintf("Couldn't find connected Storage\n"); + return kTestFailed; + } + + Common::String info = Common::String::format( + "Welcome to the Cloud test suite!\n" + "We're going to use the %s cloud storage which is connected right now.\n\n" + "Testing Cloud Storage API info() method.\n" + "In this test we'll try to list user infomation.", + CloudMan.getCurrentStorage()->name().c_str() + ); + + if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) { + Testsuite::logPrintf("Info! Skipping test : info()\n"); + return kTestSkipped; + } + + if (CloudMan.info( + new Common::GlobalFunctionCallback<Cloud::Storage::StorageInfoResponse>(&infoCallback), + new Common::GlobalFunctionCallback<Networking::ErrorResponse>(&errorCallback) + ) == nullptr) { + Testsuite::logPrintf("Warning! No Request is returned!\n"); + } + + if (!waitForCallbackMore()) return kTestSkipped; + Testsuite::clearScreen(); + + if (ConfParams.isCloudTestErrorCallbackCalled()) { + Testsuite::logPrintf("Error callback was called\n"); + return kTestFailed; + } + + Testsuite::logDetailedPrintf("Info was displayed\n"); + return kTestPassed; +} + +TestExitStatus CloudTests::testDirectoryListing() { + ConfParams.setCloudTestCallbackCalled(false); + ConfParams.setCloudTestErrorCallbackCalled(false); + + if (CloudMan.getCurrentStorage() == nullptr) { + Testsuite::logPrintf("Couldn't find connected Storage\n"); + return kTestFailed; + } + + Common::String info = "Testing Cloud Storage API listDirectory() method.\n" + "In this test we'll try to list root directory."; + + if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) { + Testsuite::logPrintf("Info! Skipping test : listDirectory()\n"); + return kTestSkipped; + } + + if (CloudMan.listDirectory( + "", + new Common::GlobalFunctionCallback<Cloud::Storage::FileArrayResponse>(&directoryListedCallback), + new Common::GlobalFunctionCallback<Networking::ErrorResponse>(&errorCallback) + ) == nullptr) { + Testsuite::logPrintf("Warning! No Request is returned!\n"); + } + + if (!waitForCallbackMore()) return kTestSkipped; + Testsuite::clearScreen(); + + if (ConfParams.isCloudTestErrorCallbackCalled()) { + Testsuite::logPrintf("Error callback was called\n"); + return kTestFailed; + } + + Testsuite::logDetailedPrintf("Directory was listed\n"); + return kTestPassed; +} + +TestExitStatus CloudTests::testDirectoryCreating() { + ConfParams.setCloudTestCallbackCalled(false); + ConfParams.setCloudTestErrorCallbackCalled(false); + + if (CloudMan.getCurrentStorage() == nullptr) { + Testsuite::logPrintf("Couldn't find connected Storage\n"); + return kTestFailed; + } + + Common::String info = "Testing Cloud Storage API createDirectory() method.\n" + "In this test we'll try to create a 'testbed' directory."; + + if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) { + Testsuite::logPrintf("Info! Skipping test : createDirectory()\n"); + return kTestSkipped; + } + + Common::String info2 = "We'd list the root directory, create the directory and the list it again.\n" + "If all goes smoothly, you'd notice that there are more directories now."; + Testsuite::displayMessage(info2); + + // list root directory + if (CloudMan.listDirectory( + "", + new Common::GlobalFunctionCallback<Cloud::Storage::FileArrayResponse>(&directoryListedCallback), + new Common::GlobalFunctionCallback<Networking::ErrorResponse>(&errorCallback) + ) == nullptr) { + Testsuite::logPrintf("Warning! No Request is returned!\n"); + } + + if (!waitForCallbackMore()) return kTestSkipped; + Testsuite::clearScreen(); + + if (ConfParams.isCloudTestErrorCallbackCalled()) { + Testsuite::logPrintf("Error callback was called\n"); + return kTestFailed; + } + + ConfParams.setCloudTestCallbackCalled(false); + + // create 'testbed' + if (CloudMan.getCurrentStorage()->createDirectory( + getRemoteTestPath(), + new Common::GlobalFunctionCallback<Cloud::Storage::BoolResponse>(&directoryCreatedCallback), + new Common::GlobalFunctionCallback<Networking::ErrorResponse>(&errorCallback) + ) == nullptr) { + Testsuite::logPrintf("Warning! No Request is returned!\n"); + } + + if (!waitForCallbackMore()) return kTestSkipped; + Testsuite::clearScreen(); + + if (ConfParams.isCloudTestErrorCallbackCalled()) { + Testsuite::logPrintf("Error callback was called\n"); + return kTestFailed; + } + + ConfParams.setCloudTestCallbackCalled(false); + + // list it again + if (CloudMan.listDirectory( + "", + new Common::GlobalFunctionCallback<Cloud::Storage::FileArrayResponse>(&directoryListedCallback), + new Common::GlobalFunctionCallback<Networking::ErrorResponse>(&errorCallback) + ) == nullptr) { + Testsuite::logPrintf("Warning! No Request is returned!\n"); + } + + if (!waitForCallbackMore()) return kTestSkipped; + Testsuite::clearScreen(); + + if (ConfParams.isCloudTestErrorCallbackCalled()) { + Testsuite::logPrintf("Error callback was called\n"); + return kTestFailed; + } + + if (Testsuite::handleInteractiveInput("Was the CloudMan able to create a 'testbed' directory?", "Yes", "No", kOptionRight)) { + Testsuite::logDetailedPrintf("Error! Directory was not created!\n"); + return kTestFailed; + } + + Testsuite::logDetailedPrintf("Directory was created\n"); + return kTestPassed; +} + +TestExitStatus CloudTests::testUploading() { + ConfParams.setCloudTestCallbackCalled(false); + ConfParams.setCloudTestErrorCallbackCalled(false); + + if (CloudMan.getCurrentStorage() == nullptr) { + Testsuite::logPrintf("Couldn't find connected Storage\n"); + return kTestFailed; + } + + Common::String info = "Testing Cloud Storage API upload() method.\n" + "In this test we'll try to upload a 'test1/file.txt' file."; + + if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) { + Testsuite::logPrintf("Info! Skipping test : upload()\n"); + return kTestSkipped; + } + + if (!ConfParams.isGameDataFound()) { + Testsuite::logPrintf("Info! Couldn't find the game data, so skipping test : upload()\n"); + return kTestSkipped; + } + + const Common::String &path = ConfMan.get("path"); + Common::FSDirectory gameRoot(path); + Common::FSDirectory *directory = gameRoot.getSubDirectory("test1"); + Common::FSNode node = directory->getFSNode().getChild("file.txt"); + delete directory; + + if (CloudMan.getCurrentStorage()->uploadStreamSupported()) { + if (CloudMan.getCurrentStorage()->upload( + Common::String(getRemoteTestPath()) + "/testfile.txt", + node.createReadStream(), + new Common::GlobalFunctionCallback<Cloud::Storage::UploadResponse>(&fileUploadedCallback), + new Common::GlobalFunctionCallback<Networking::ErrorResponse>(&errorCallback) + ) == nullptr) { + Testsuite::logPrintf("Warning! No Request is returned!\n"); + } + } else { + Common::String filepath = node.getPath(); + if (CloudMan.getCurrentStorage()->upload( + Common::String(getRemoteTestPath()) + "/testfile.txt", + filepath.c_str(), + new Common::GlobalFunctionCallback<Cloud::Storage::UploadResponse>(&fileUploadedCallback), + new Common::GlobalFunctionCallback<Networking::ErrorResponse>(&errorCallback) + ) == nullptr) { + Testsuite::logPrintf("Warning! No Request is returned!\n"); + } + } + + if (!waitForCallbackMore()) return kTestSkipped; + Testsuite::clearScreen(); + + if (ConfParams.isCloudTestErrorCallbackCalled()) { + Testsuite::logPrintf("Error callback was called\n"); + return kTestFailed; + } + + Common::String info2 = "upload() is finished. Do you want to list '/testbed' directory?"; + + if (!Testsuite::handleInteractiveInput(info2, "Yes", "No", kOptionRight)) { + ConfParams.setCloudTestCallbackCalled(false); + + if (CloudMan.listDirectory( + getRemoteTestPath(), + new Common::GlobalFunctionCallback<Cloud::Storage::FileArrayResponse>(&directoryListedCallback), + new Common::GlobalFunctionCallback<Networking::ErrorResponse>(&errorCallback) + ) == nullptr) { + Testsuite::logPrintf("Warning! No Request is returned!\n"); + } + + if (!waitForCallbackMore()) return kTestSkipped; + Testsuite::clearScreen(); + + if (ConfParams.isCloudTestErrorCallbackCalled()) { + Testsuite::logPrintf("Error callback was called\n"); + return kTestFailed; + } + } + + if (Testsuite::handleInteractiveInput("Was the CloudMan able to upload into 'testbed/testfile.txt' file?", "Yes", "No", kOptionRight)) { + Testsuite::logDetailedPrintf("Error! File was not uploaded!\n"); + return kTestFailed; + } + + Testsuite::logDetailedPrintf("File was uploaded\n"); + return kTestPassed; +} + +TestExitStatus CloudTests::testDownloading() { + ConfParams.setCloudTestCallbackCalled(false); + ConfParams.setCloudTestErrorCallbackCalled(false); + + if (CloudMan.getCurrentStorage() == nullptr) { + Testsuite::logPrintf("Couldn't find connected Storage\n"); + return kTestFailed; + } + + Common::String info = "Testing Cloud Storage API download() method.\n" + "In this test we'll try to download that 'testbed/testfile.txt' file."; + + if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) { + Testsuite::logPrintf("Info! Skipping test : download()\n"); + return kTestSkipped; + } + + const Common::String &path = ConfMan.get("path"); + Common::FSDirectory gameRoot(path); + Common::FSNode node = gameRoot.getFSNode().getChild("downloaded_file.txt"); + Common::String filepath = node.getPath(); + if (CloudMan.getCurrentStorage()->download( + Common::String(getRemoteTestPath()) + "/testfile.txt", + filepath.c_str(), + new Common::GlobalFunctionCallback<Cloud::Storage::BoolResponse>(&fileDownloadedCallback), + new Common::GlobalFunctionCallback<Networking::ErrorResponse>(&errorCallback) + ) == nullptr) { + Testsuite::logPrintf("Warning! No Request is returned!\n"); + } + + if (!waitForCallbackMore()) return kTestSkipped; + Testsuite::clearScreen(); + + if (ConfParams.isCloudTestErrorCallbackCalled()) { + Testsuite::logPrintf("Error callback was called\n"); + return kTestFailed; + } + + if (Testsuite::handleInteractiveInput("Was the CloudMan able to download into 'testbed/downloaded_file.txt' file?", "Yes", "No", kOptionRight)) { + Testsuite::logDetailedPrintf("Error! File was not downloaded!\n"); + return kTestFailed; + } + + Testsuite::logDetailedPrintf("File was downloaded\n"); + return kTestPassed; +} + +TestExitStatus CloudTests::testFolderDownloading() { + ConfParams.setCloudTestCallbackCalled(false); + ConfParams.setCloudTestErrorCallbackCalled(false); + + if (CloudMan.getCurrentStorage() == nullptr) { + Testsuite::logPrintf("Couldn't find connected Storage\n"); + return kTestFailed; + } + + Common::String info = "Testing Cloud Storage API downloadFolder() method.\n" + "In this test we'll try to download remote 'testbed/' directory."; + + if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) { + Testsuite::logPrintf("Info! Skipping test : downloadFolder()\n"); + return kTestSkipped; + } + + const Common::String &path = ConfMan.get("path"); + Common::FSDirectory gameRoot(path); + Common::FSNode node = gameRoot.getFSNode().getChild("downloaded_directory"); + Common::String filepath = node.getPath(); + if (CloudMan.downloadFolder( + getRemoteTestPath(), + filepath.c_str(), + new Common::GlobalFunctionCallback<Cloud::Storage::FileArrayResponse>(&directoryDownloadedCallback), + new Common::GlobalFunctionCallback<Networking::ErrorResponse>(&errorCallback) + ) == nullptr) { + Testsuite::logPrintf("Warning! No Request is returned!\n"); + } + + if (!waitForCallbackMore()) return kTestSkipped; + Testsuite::clearScreen(); + + if (ConfParams.isCloudTestErrorCallbackCalled()) { + Testsuite::logPrintf("Error callback was called\n"); + return kTestFailed; + } + + if (Testsuite::handleInteractiveInput("Was the CloudMan able to download into 'testbed/downloaded_directory'?", "Yes", "No", kOptionRight)) { + Testsuite::logDetailedPrintf("Error! Directory was not downloaded!\n"); + return kTestFailed; + } + + Testsuite::logDetailedPrintf("Directory was downloaded\n"); + return kTestPassed; +} + +TestExitStatus CloudTests::testSavesSync() { + ConfParams.setCloudTestCallbackCalled(false); + ConfParams.setCloudTestErrorCallbackCalled(false); + + if (CloudMan.getCurrentStorage() == nullptr) { + Testsuite::logPrintf("Couldn't find connected Storage\n"); + return kTestFailed; + } + + Common::String info = "Testing Cloud Storage API syncSaves() method.\n" + "In this test we'll try to sync your saves."; + + if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) { + Testsuite::logPrintf("Info! Skipping test : syncSaves()\n"); + return kTestSkipped; + } + + const Common::String &path = ConfMan.get("path"); + Common::FSDirectory gameRoot(path); + Common::FSNode node = gameRoot.getFSNode().getChild("downloaded_directory"); + Common::String filepath = node.getPath(); + if (CloudMan.syncSaves( + new Common::GlobalFunctionCallback<Cloud::Storage::BoolResponse>(&savesSyncedCallback), + new Common::GlobalFunctionCallback<Networking::ErrorResponse>(&errorCallback) + ) == nullptr) { + Testsuite::logPrintf("Warning! No Request is returned!\n"); + } + + if (!waitForCallbackMore()) return kTestSkipped; + Testsuite::clearScreen(); + + if (ConfParams.isCloudTestErrorCallbackCalled()) { + Testsuite::logPrintf("Error callback was called\n"); + return kTestFailed; + } + + if (Testsuite::handleInteractiveInput("Was the CloudMan able to sync saves?", "Yes", "No", kOptionRight)) { + Testsuite::logDetailedPrintf("Error! Saves were not synced!\n"); + return kTestFailed; + } + + Testsuite::logDetailedPrintf("Saves were synced successfully\n"); + return kTestPassed; +} + +} // End of namespace Testbed diff --git a/engines/testbed/cloud.h b/engines/testbed/cloud.h new file mode 100644 index 0000000000..ed27d7da82 --- /dev/null +++ b/engines/testbed/cloud.h @@ -0,0 +1,83 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef TESTBED_CLOUD_H +#define TESTBED_CLOUD_H + +#include "testbed/testsuite.h" +#include <backends/cloud/storage.h> + +// This file can be used as template for header files of other newer testsuites. + +namespace Testbed { + +namespace CloudTests { + +// Helper functions for Cloud tests + +bool waitForCallback(); +bool waitForCallbackMore(); +const char *getRemoteTestPath(); +void infoCallback(Cloud::Storage::StorageInfoResponse response); +void directoryListedCallback(Cloud::Storage::FileArrayResponse response); +void directoryCreatedCallback(Cloud::Storage::BoolResponse response); +void fileUploadedCallback(Cloud::Storage::UploadResponse response); +void fileDownloadedCallback(Cloud::Storage::BoolResponse response); +void directoryDownloadedCallback(Cloud::Storage::FileArrayResponse response); +void savesSyncedCallback(Cloud::Storage::BoolResponse response); +void errorCallback(Networking::ErrorResponse response); + +TestExitStatus testInfo(); +TestExitStatus testDirectoryListing(); +TestExitStatus testDirectoryCreating(); +TestExitStatus testUploading(); +TestExitStatus testDownloading(); +TestExitStatus testFolderDownloading(); +TestExitStatus testSavesSync(); + +} // End of namespace CloudTests + +class CloudTestSuite : public Testsuite { +public: + /** + * The constructor for the CloudTestSuite + * For every test to be executed one must: + * 1) Create a function that would invoke the test + * 2) Add that test to list by executing addTest() + * + * @see addTest() + */ + CloudTestSuite(); + ~CloudTestSuite() {} + const char *getName() const { + return "Cloud"; + } + + const char *getDescription() const { + return "CloudMan, Storage API tests"; + } + +}; + +} // End of namespace Testbed + +#endif // TESTBED_TEMPLATE_H diff --git a/engines/testbed/config-params.h b/engines/testbed/config-params.h index 89aae199b6..57fd099bf4 100644 --- a/engines/testbed/config-params.h +++ b/engines/testbed/config-params.h @@ -54,6 +54,10 @@ private: */ bool _isInteractive; bool _isGameDataFound; +#ifdef USE_LIBCURL + bool _isCloudTestCallbackCalled; + bool _isCloudTestErrorCallbackCalled; +#endif bool _rerunTests; TestbedConfigManager *_testbedConfMan; @@ -68,6 +72,14 @@ public: bool isGameDataFound() { return _isGameDataFound; } void setGameDataFound(bool status) { _isGameDataFound = status; } +#ifdef USE_LIBCURL + bool isCloudTestCallbackCalled() const { return _isCloudTestCallbackCalled; } + void setCloudTestCallbackCalled(bool status) { _isCloudTestCallbackCalled = status; } + + bool isCloudTestErrorCallbackCalled() const { return _isCloudTestErrorCallbackCalled; } + void setCloudTestErrorCallbackCalled(bool status) { _isCloudTestErrorCallbackCalled = status; } +#endif + TestbedConfigManager *getTestbedConfigManager() { return _testbedConfMan; } void setTestbedConfigManager(TestbedConfigManager* confMan) { _testbedConfMan = confMan; } diff --git a/engines/testbed/misc.cpp b/engines/testbed/misc.cpp index 5847a8d2e4..20651e76e6 100644 --- a/engines/testbed/misc.cpp +++ b/engines/testbed/misc.cpp @@ -22,6 +22,7 @@ #include "testbed/misc.h" #include "common/timer.h" +#include <backends/networking/browser/openurl.h> namespace Testbed { @@ -160,10 +161,34 @@ TestExitStatus MiscTests::testMutexes() { return kTestFailed; } +TestExitStatus MiscTests::testOpenUrl() { + Common::String info = "Testing openUrl() method.\n" + "In this test we'll try to open scummvm.org in your default browser."; + + if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) { + Testsuite::logPrintf("Info! Skipping test : openUrl()\n"); + return kTestSkipped; + } + + if (!Networking::Browser::openUrl("http://scummvm.org/")) { + Testsuite::logPrintf("Info! openUrl() says it couldn't open the url (probably not supported on this platform)\n"); + return kTestFailed; + } + + if (Testsuite::handleInteractiveInput("Was ScummVM able to open 'http://scummvm.org/' in your default browser?", "Yes", "No", kOptionRight)) { + Testsuite::logDetailedPrintf("Error! openUrl() is not working!\n"); + return kTestFailed; + } + + Testsuite::logDetailedPrintf("openUrl() is OK\n"); + return kTestPassed; +} + MiscTestSuite::MiscTestSuite() { addTest("Datetime", &MiscTests::testDateTime, false); addTest("Timers", &MiscTests::testTimers, false); addTest("Mutexes", &MiscTests::testMutexes, false); + addTest("openUrl", &MiscTests::testOpenUrl, true); } } // End of namespace Testbed diff --git a/engines/testbed/misc.h b/engines/testbed/misc.h index 23e303d676..adfc322d9f 100644 --- a/engines/testbed/misc.h +++ b/engines/testbed/misc.h @@ -49,6 +49,7 @@ void criticalSection(void *arg); TestExitStatus testDateTime(); TestExitStatus testTimers(); TestExitStatus testMutexes(); +TestExitStatus testOpenUrl(); // add more here } // End of namespace MiscTests @@ -69,7 +70,7 @@ public: return "Misc"; } const char *getDescription() const { - return "Miscellaneous: Timers/Mutexes/Datetime"; + return "Miscellaneous: Timers/Mutexes/Datetime/openUrl"; } }; diff --git a/engines/testbed/module.mk b/engines/testbed/module.mk index ce78a48bc5..99e6157cde 100644 --- a/engines/testbed/module.mk +++ b/engines/testbed/module.mk @@ -14,6 +14,16 @@ MODULE_OBJS := \ testbed.o \ testsuite.o +ifdef USE_LIBCURL +MODULE_OBJS += \ + cloud.o +endif + +ifdef USE_SDL_NET +MODULE_OBJS += \ + webserver.o +endif + MODULE_DIRS += \ engines/testbed diff --git a/engines/testbed/testbed.cpp b/engines/testbed/testbed.cpp index 885429cafd..5943d47c58 100644 --- a/engines/testbed/testbed.cpp +++ b/engines/testbed/testbed.cpp @@ -39,6 +39,12 @@ #include "testbed/savegame.h" #include "testbed/sound.h" #include "testbed/testbed.h" +#ifdef USE_LIBCURL +#include "testbed/cloud.h" +#endif +#ifdef USE_SDL_NET +#include "testbed/webserver.h" +#endif namespace Testbed { @@ -134,6 +140,16 @@ TestbedEngine::TestbedEngine(OSystem *syst) // Midi ts = new MidiTestSuite(); _testsuiteList.push_back(ts); +#ifdef USE_LIBCURL + // Cloud + ts = new CloudTestSuite(); + _testsuiteList.push_back(ts); +#endif +#ifdef USE_SDL_NET + // Webserver + ts = new WebserverTestSuite(); + _testsuiteList.push_back(ts); +#endif } TestbedEngine::~TestbedEngine() { diff --git a/engines/testbed/webserver.cpp b/engines/testbed/webserver.cpp new file mode 100644 index 0000000000..feb2911704 --- /dev/null +++ b/engines/testbed/webserver.cpp @@ -0,0 +1,237 @@ +/* ScummVM - Graphic Adventure Engine +* +* ScummVM is the legal property of its developers, whose names +* are too numerous to list here. Please refer to the COPYRIGHT +* file distributed with this source distribution. +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +* +*/ + +#include "testbed/webserver.h" +#include "backends/networking/sdl_net/localwebserver.h" +#include "common/config-manager.h" +#include <backends/networking/browser/openurl.h> + +namespace Testbed { + +WebserverTestSuite::WebserverTestSuite() { + addTest("ResolveIP", &WebserverTests::testIP, true); + addTest("IndexPage", &WebserverTests::testIndexPage, true); + addTest("FilesPage", &WebserverTests::testFilesPageInvalidParameterValue, true); + addTest("CreateDirectory", &WebserverTests::testFilesPageCreateDirectory, true); + addTest("UploadFile", &WebserverTests::testFilesPageUploadFile, true); + addTest("UploadDirectory", &WebserverTests::testFilesPageUploadDirectory, true); + addTest("DownloadFile", &WebserverTests::testFilesPageDownloadFile, true); +} + +///// TESTS GO HERE ///// + +/** This test calls Storage::info(). */ + +bool WebserverTests::startServer() { + Common::Point pt; + pt.x = 10; pt.y = 10; + Testsuite::writeOnScreen("Starting webserver...", pt); + LocalServer.start(); + g_system->delayMillis(500); + Testsuite::clearScreen(); + return LocalServer.isRunning(); +} + +TestExitStatus WebserverTests::testIP() { + if (!startServer()) { + Testsuite::logPrintf("Error! Can't start local webserver!\n"); + return kTestFailed; + } + + Common::String info = "Welcome to the Webserver test suite!\n" + "You would be visiting different server's pages and saying whether they work like they should.\n\n" + "Testing Webserver's IP resolving.\n" + "In this test we'll try to resolve server's IP."; + + if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) { + Testsuite::logPrintf("Info! Skipping test : IP resolving\n"); + return kTestSkipped; + } + + if (Testsuite::handleInteractiveInput( + Common::String::format("Is this your machine's IP?\n%s", LocalServer.getAddress().c_str()), + "Yes", "No", kOptionRight)) { + Testsuite::logDetailedPrintf("Error! IP was not resolved!\n"); + return kTestFailed; + } + + Testsuite::logDetailedPrintf("IP was resolved\n"); + return kTestPassed; +} + +TestExitStatus WebserverTests::testIndexPage() { + if (!startServer()) { + Testsuite::logPrintf("Error! Can't start local webserver!\n"); + return kTestFailed; + } + + Common::String info = "Testing Webserver's index page.\n" + "In this test we'll try to open server's index page."; + + if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) { + Testsuite::logPrintf("Info! Skipping test : index page\n"); + return kTestSkipped; + } + + Networking::Browser::openUrl(LocalServer.getAddress()); + if (Testsuite::handleInteractiveInput( + Common::String::format("The %s page opens well?", LocalServer.getAddress().c_str()), + "Yes", "No", kOptionRight)) { + Testsuite::logDetailedPrintf("Error! Couldn't open server's index page!\n"); + return kTestFailed; + } + + Testsuite::logDetailedPrintf("Server's index page is OK\n"); + return kTestPassed; +} + +TestExitStatus WebserverTests::testFilesPageInvalidParameterValue() { + if (!startServer()) { + Testsuite::logPrintf("Error! Can't start local webserver!\n"); + return kTestFailed; + } + + Common::String info = "Testing Webserver's files page.\n" + "In this test we'll try to pass invalid parameter to files page."; + + if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) { + Testsuite::logPrintf("Info! Skipping test : files page (invalid parameter)\n"); + return kTestSkipped; + } + + Networking::Browser::openUrl(LocalServer.getAddress()+"files?path=error"); + if (Testsuite::handleInteractiveInput( + Common::String::format("The %sfiles?path=error page displays error message?", LocalServer.getAddress().c_str()), + "Yes", "No", kOptionRight)) { + Testsuite::logDetailedPrintf("Error! No error message on invalid parameter in '/files'!\n"); + return kTestFailed; + } + + Testsuite::logDetailedPrintf("Server's files page detects invalid paramters fine\n"); + return kTestPassed; +} + +TestExitStatus WebserverTests::testFilesPageCreateDirectory() { + if (!startServer()) { + Testsuite::logPrintf("Error! Can't start local webserver!\n"); + return kTestFailed; + } + + Common::String info = "Testing Webserver's files page Create Directory feature.\n" + "In this test you'll try to create directory."; + + if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) { + Testsuite::logPrintf("Info! Skipping test : files page create directory\n"); + return kTestSkipped; + } + + Networking::Browser::openUrl(LocalServer.getAddress() + "files?path=/root/"); + if (Testsuite::handleInteractiveInput( + Common::String::format("You could go to %sfiles page, navigate to some directory with write access and create a directory there?", LocalServer.getAddress().c_str()), + "Yes", "No", kOptionRight)) { + Testsuite::logDetailedPrintf("Error! Create Directory is not working!\n"); + return kTestFailed; + } + + Testsuite::logDetailedPrintf("Create Directory is OK\n"); + return kTestPassed; +} + +TestExitStatus WebserverTests::testFilesPageUploadFile() { + if (!startServer()) { + Testsuite::logPrintf("Error! Can't start local webserver!\n"); + return kTestFailed; + } + + Common::String info = "Testing Webserver's files page Upload Files feature.\n" + "In this test you'll try to upload a file."; + + if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) { + Testsuite::logPrintf("Info! Skipping test : files page file upload\n"); + return kTestSkipped; + } + + Networking::Browser::openUrl(LocalServer.getAddress() + "files?path=/root/"); + if (Testsuite::handleInteractiveInput( + Common::String::format("You're able to upload a file in some directory with write access through %sfiles page?", LocalServer.getAddress().c_str()), + "Yes", "No", kOptionRight)) { + Testsuite::logDetailedPrintf("Error! Upload Files is not working!\n"); + return kTestFailed; + } + + Testsuite::logDetailedPrintf("Upload Files is OK\n"); + return kTestPassed; +} + +TestExitStatus WebserverTests::testFilesPageUploadDirectory() { + if (!startServer()) { + Testsuite::logPrintf("Error! Can't start local webserver!\n"); + return kTestFailed; + } + + Common::String info = "Testing Webserver's files page Upload Directory feature.\n" + "In this test you'll try to upload a directory (works in Chrome only)."; + + if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) { + Testsuite::logPrintf("Info! Skipping test : files page directory upload\n"); + return kTestSkipped; + } + + Networking::Browser::openUrl(LocalServer.getAddress() + "files?path=/root/"); + if (Testsuite::handleInteractiveInput( + Common::String::format("You're able to upload a directory into some directory with write access through %sfiles page using Chrome?", LocalServer.getAddress().c_str()), + "Yes", "No", kOptionRight)) { + Testsuite::logDetailedPrintf("Error! Upload Directory is not working!\n"); + return kTestFailed; + } + + Testsuite::logDetailedPrintf("Upload Directory is OK\n"); + return kTestPassed; +} + +TestExitStatus WebserverTests::testFilesPageDownloadFile() { + if (!startServer()) { + Testsuite::logPrintf("Error! Can't start local webserver!\n"); + return kTestFailed; + } + + Common::String info = "Testing Webserver's files downloading feature.\n" + "In this test you'll try to download a file."; + + if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) { + Testsuite::logPrintf("Info! Skipping test : files page download\n"); + return kTestSkipped; + } + + Networking::Browser::openUrl(LocalServer.getAddress() + "files?path=/root/"); + if (Testsuite::handleInteractiveInput( + Common::String::format("You're able to download a file through %sfiles page?", LocalServer.getAddress().c_str()), + "Yes", "No", kOptionRight)) { + Testsuite::logDetailedPrintf("Error! Files downloading is not working!\n"); + return kTestFailed; + } + + Testsuite::logDetailedPrintf("Files downloading is OK\n"); + return kTestPassed; +} + +} // End of namespace Testbed diff --git a/engines/testbed/webserver.h b/engines/testbed/webserver.h new file mode 100644 index 0000000000..3f3083469e --- /dev/null +++ b/engines/testbed/webserver.h @@ -0,0 +1,69 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef TESTBED_WEBSERVER_H +#define TESTBED_WEBSERVER_H + +#include "testbed/testsuite.h" + +namespace Testbed { + +namespace WebserverTests { + +// Helper functions for Webserver tests + +bool startServer(); +TestExitStatus testIP(); +TestExitStatus testIndexPage(); +TestExitStatus testFilesPageInvalidParameterValue(); +TestExitStatus testFilesPageCreateDirectory(); +TestExitStatus testFilesPageUploadFile(); +TestExitStatus testFilesPageUploadDirectory(); +TestExitStatus testFilesPageDownloadFile(); + +} // End of namespace WebserverTests + +class WebserverTestSuite : public Testsuite { +public: + /** + * The constructor for the WebserverTestSuite + * For every test to be executed one must: + * 1) Create a function that would invoke the test + * 2) Add that test to list by executing addTest() + * + * @see addTest() + */ + WebserverTestSuite(); + ~WebserverTestSuite() {} + const char *getName() const { + return "Webserver"; + } + + const char *getDescription() const { + return "Webserver tests"; + } + +}; + +} // End of namespace Testbed + +#endif // TESTBED_TEMPLATE_H diff --git a/engines/tinsel/detection.cpp b/engines/tinsel/detection.cpp index c44f1f4ef3..d6bcfe5ea0 100644 --- a/engines/tinsel/detection.cpp +++ b/engines/tinsel/detection.cpp @@ -109,7 +109,8 @@ bool TinselMetaEngine::hasFeature(MetaEngineFeature f) const { return (f == kSupportsListSaves) || (f == kSupportsLoadingDuringStartup) || - (f == kSupportsDeleteSave); + (f == kSupportsDeleteSave) || + (f == kSimpleSavesNames); } bool Tinsel::TinselEngine::hasFeature(EngineFeature f) const { diff --git a/engines/titanic/support/simple_file.h b/engines/titanic/support/simple_file.h index f5d0bc7c1b..01aaa86925 100644 --- a/engines/titanic/support/simple_file.h +++ b/engines/titanic/support/simple_file.h @@ -278,7 +278,7 @@ public: * Set up a stream for write access */ virtual void open(Common::OutSaveFile *stream) { - SimpleFile::open(Common::wrapCompressedWriteStream(stream)); + SimpleFile::open(new Common::OutSaveFile(Common::wrapCompressedWriteStream(stream))); } }; diff --git a/engines/toltecs/detection.cpp b/engines/toltecs/detection.cpp index 7c707895e6..cc27341e10 100644 --- a/engines/toltecs/detection.cpp +++ b/engines/toltecs/detection.cpp @@ -234,7 +234,8 @@ bool ToltecsMetaEngine::hasFeature(MetaEngineFeature f) const { (f == kSavesSupportMetaInfo) || (f == kSavesSupportThumbnail) || (f == kSavesSupportCreationDate) || - (f == kSavesSupportPlayTime); + (f == kSavesSupportPlayTime) || + (f == kSimpleSavesNames); } bool Toltecs::ToltecsEngine::hasFeature(EngineFeature f) const { diff --git a/engines/toon/detection.cpp b/engines/toon/detection.cpp index 5d2e0a9bca..e93d676d87 100644 --- a/engines/toon/detection.cpp +++ b/engines/toon/detection.cpp @@ -159,7 +159,8 @@ bool ToonMetaEngine::hasFeature(MetaEngineFeature f) const { (f == kSupportsDeleteSave) || (f == kSavesSupportMetaInfo) || (f == kSavesSupportThumbnail) || - (f == kSavesSupportCreationDate); + (f == kSavesSupportCreationDate) || + (f == kSimpleSavesNames); } void ToonMetaEngine::removeSaveState(const char *target, int slot) const { diff --git a/engines/tsage/detection.cpp b/engines/tsage/detection.cpp index 584ad87742..e476391f71 100644 --- a/engines/tsage/detection.cpp +++ b/engines/tsage/detection.cpp @@ -95,6 +95,7 @@ public: case kSavesSupportThumbnail: case kSavesSupportCreationDate: case kSavesSupportPlayTime: + case kSimpleSavesNames: return true; default: return false; diff --git a/engines/tsage/stP1kAlM b/engines/tsage/stP1kAlM Binary files differnew file mode 100644 index 0000000000..dfbb3b9786 --- /dev/null +++ b/engines/tsage/stP1kAlM diff --git a/engines/voyeur/detection.cpp b/engines/voyeur/detection.cpp index 7b9fa6722e..eefe174e94 100644 --- a/engines/voyeur/detection.cpp +++ b/engines/voyeur/detection.cpp @@ -92,7 +92,8 @@ bool VoyeurMetaEngine::hasFeature(MetaEngineFeature f) const { (f == kSupportsLoadingDuringStartup) || (f == kSupportsDeleteSave) || (f == kSavesSupportMetaInfo) || - (f == kSavesSupportThumbnail); + (f == kSavesSupportThumbnail) || + (f == kSimpleSavesNames); } bool Voyeur::VoyeurEngine::hasFeature(EngineFeature f) const { diff --git a/engines/wage/detection.cpp b/engines/wage/detection.cpp index a27bfd7fde..778cd3c7a7 100644 --- a/engines/wage/detection.cpp +++ b/engines/wage/detection.cpp @@ -78,7 +78,8 @@ bool WageMetaEngine::hasFeature(MetaEngineFeature f) const { return (f == kSupportsListSaves) || (f == kSupportsLoadingDuringStartup) || - (f == kSupportsDeleteSave); + (f == kSupportsDeleteSave) || + (f == kSimpleSavesNames); } bool Wage::WageEngine::hasFeature(EngineFeature f) const { diff --git a/engines/zvision/detection.cpp b/engines/zvision/detection.cpp index cc967070d9..5e535a9954 100644 --- a/engines/zvision/detection.cpp +++ b/engines/zvision/detection.cpp @@ -87,7 +87,8 @@ bool ZVisionMetaEngine::hasFeature(MetaEngineFeature f) const { (f == kSupportsDeleteSave) || (f == kSavesSupportMetaInfo) || (f == kSavesSupportThumbnail) || - (f == kSavesSupportCreationDate); + (f == kSavesSupportCreationDate) || + (f == kSimpleSavesNames); //(f == kSavesSupportPlayTime); } diff --git a/graphics/VectorRenderer.h b/graphics/VectorRenderer.h index 0352808706..5f7b6e60d3 100644 --- a/graphics/VectorRenderer.h +++ b/graphics/VectorRenderer.h @@ -28,6 +28,7 @@ #include "common/str.h" #include "graphics/surface.h" +#include "graphics/transparent_surface.h" #include "gui/ThemeEngine.h" @@ -79,8 +80,11 @@ struct DrawStep { uint32 scale; /**< scale of all the coordinates in FIXED POINT with 16 bits mantissa */ + GUI::ThemeEngine::AutoScaleMode autoscale; /**< scale alphaimage if present */ + DrawingFunctionCallback drawingCall; /**< Pointer to drawing function */ Graphics::Surface *blitSrc; + Graphics::TransparentSurface *blitAlphaSrc; }; VectorRenderer *createRenderer(int mode); @@ -281,7 +285,7 @@ public: * * @param surface Pointer to a Surface object. */ - virtual void setSurface(Surface *surface) { + virtual void setSurface(TransparentSurface *surface) { _activeSurface = surface; } @@ -420,7 +424,13 @@ public: void drawCallback_BITMAP(const Common::Rect &area, const DrawStep &step, const Common::Rect &clip) { uint16 x, y, w, h; stepGetPositions(step, area, x, y, w, h); - blitAlphaBitmapClip(step.blitSrc, Common::Rect(x, y, x + w, y + h), clip); + blitKeyBitmapClip(step.blitSrc, Common::Rect(x, y, x + w, y + h), clip); + } + + void drawCallback_ALPHABITMAP(const Common::Rect &area, const DrawStep &step, const Common::Rect &clip) { + uint16 x, y, w, h; + stepGetPositions(step, area, x, y, w, h); + blitAlphaBitmap(step.blitAlphaSrc, Common::Rect(x, y, x + w, y + h), step.autoscale, step.xAlign, step.yAlign); //TODO } void drawCallback_CROSS(const Common::Rect &area, const DrawStep &step, const Common::Rect &clip) { @@ -482,8 +492,14 @@ public: virtual void blitSubSurface(const Graphics::Surface *source, const Common::Rect &r) = 0; virtual void blitSubSurfaceClip(const Graphics::Surface *source, const Common::Rect &r, const Common::Rect &clipping) = 0; - virtual void blitAlphaBitmap(const Graphics::Surface *source, const Common::Rect &r) = 0; - virtual void blitAlphaBitmapClip(const Graphics::Surface *source, const Common::Rect &r, const Common::Rect &clipping) = 0; + virtual void blitKeyBitmap(const Graphics::Surface *source, const Common::Rect &r) = 0; + virtual void blitKeyBitmapClip(const Graphics::Surface *source, const Common::Rect &r, const Common::Rect &clipping) = 0; + + virtual void blitAlphaBitmap(Graphics::TransparentSurface *source, const Common::Rect &r, + GUI::ThemeEngine::AutoScaleMode autoscale = GUI::ThemeEngine::kAutoScaleNone, + Graphics::DrawStep::VectorAlignment xAlign = Graphics::DrawStep::kVectorAlignManual, + Graphics::DrawStep::VectorAlignment yAlign = Graphics::DrawStep::kVectorAlignManual, + int alpha = 255) = 0; /** * Draws a string into the screen. Wrapper for the Graphics::Font string drawing @@ -507,7 +523,7 @@ public: virtual void applyScreenShading(GUI::ThemeEngine::ShadingStyle) = 0; protected: - Surface *_activeSurface; /**< Pointer to the surface currently being drawn */ + TransparentSurface *_activeSurface; /**< Pointer to the surface currently being drawn */ FillMode _fillMode; /**< Defines in which way (if any) are filled the drawn shapes */ ShadowFillMode _shadowFillMode; diff --git a/graphics/VectorRendererSpec.cpp b/graphics/VectorRendererSpec.cpp index fc741f6e77..9aed3301fa 100644 --- a/graphics/VectorRendererSpec.cpp +++ b/graphics/VectorRendererSpec.cpp @@ -25,6 +25,8 @@ #include "common/frac.h" #include "graphics/surface.h" +#include "graphics/transparent_surface.h" +#include "graphics/nine_patch.h" #include "graphics/colormasks.h" #include "gui/ThemeEngine.h" @@ -848,8 +850,8 @@ blitSubSurfaceClip(const Graphics::Surface *source, const Common::Rect &r, const } template<typename PixelType> -void VectorRendererSpec<PixelType>:: -blitAlphaBitmap(const Graphics::Surface *source, const Common::Rect &r) { +void VectorRendererSpec<PixelType>:: +blitKeyBitmap(const Graphics::Surface *source, const Common::Rect &r) { int16 x = r.left; int16 y = r.top; @@ -885,9 +887,43 @@ blitAlphaBitmap(const Graphics::Surface *source, const Common::Rect &r) { template<typename PixelType> void VectorRendererSpec<PixelType>:: -blitAlphaBitmapClip(const Graphics::Surface *source, const Common::Rect &r, const Common::Rect &clipping) { +blitAlphaBitmap(Graphics::TransparentSurface *source, const Common::Rect &r, GUI::ThemeEngine::AutoScaleMode autoscale, + Graphics::DrawStep::VectorAlignment xAlign, Graphics::DrawStep::VectorAlignment yAlign, int alpha) { + if (autoscale == GUI::ThemeEngine::kAutoScaleStretch) { + source->blit(*_activeSurface, r.left, r.top, Graphics::FLIP_NONE, + nullptr, TS_ARGB(alpha, 255, 255, 255), + r.width(), r.height()); + } else if (autoscale == GUI::ThemeEngine::kAutoScaleFit) { + double ratio = (double)r.width() / source->w; + double ratio2 = (double)r.height() / source->h; + + if (ratio2 < ratio) + ratio = ratio2; + + int offx = 0, offy = 0; + if (xAlign == Graphics::DrawStep::kVectorAlignCenter) + offx = (r.width() - (int)(source->w * ratio)) >> 1; + + if (yAlign == Graphics::DrawStep::kVectorAlignCenter) + offy = (r.height() - (int)(source->h * ratio)) >> 1; + + source->blit(*_activeSurface, r.left + offx, r.top + offy, Graphics::FLIP_NONE, + nullptr, TS_ARGB(alpha, 255, 255, 255), + (int)(source->w * ratio), (int)(source->h * ratio)); + + } else if (autoscale == GUI::ThemeEngine::kAutoScaleNinePatch) { + Graphics::NinePatchBitmap nine(source, false); + nine.blit(*_activeSurface, r.left, r.top, r.width(), r.height()); + } else { + source->blit(*_activeSurface, r.left, r.top); + } +} + +template<typename PixelType> +void VectorRendererSpec<PixelType>:: +blitKeyBitmapClip(const Graphics::Surface *source, const Common::Rect &r, const Common::Rect &clipping) { if (clipping.isEmpty() || clipping.contains(r)) { - blitAlphaBitmap(source, r); + blitKeyBitmap(source, r); return; } diff --git a/graphics/VectorRendererSpec.h b/graphics/VectorRendererSpec.h index bee6d4c425..84c802f6df 100644 --- a/graphics/VectorRendererSpec.h +++ b/graphics/VectorRendererSpec.h @@ -93,8 +93,13 @@ public: void blitSurface(const Graphics::Surface *source, const Common::Rect &r); void blitSubSurface(const Graphics::Surface *source, const Common::Rect &r); void blitSubSurfaceClip(const Graphics::Surface *source, const Common::Rect &r, const Common::Rect &clipping); - void blitAlphaBitmap(const Graphics::Surface *source, const Common::Rect &r); - void blitAlphaBitmapClip(const Graphics::Surface *source, const Common::Rect &r, const Common::Rect &clipping); + void blitKeyBitmap(const Graphics::Surface *source, const Common::Rect &r); + void blitKeyBitmapClip(const Graphics::Surface *source, const Common::Rect &r, const Common::Rect &clipping); + void blitAlphaBitmap(Graphics::TransparentSurface *source, const Common::Rect &r, + GUI::ThemeEngine::AutoScaleMode autoscale = GUI::ThemeEngine::kAutoScaleNone, + Graphics::DrawStep::VectorAlignment xAlign = Graphics::DrawStep::kVectorAlignManual, + Graphics::DrawStep::VectorAlignment yAlign = Graphics::DrawStep::kVectorAlignManual, + int alpha = 255); void applyScreenShading(GUI::ThemeEngine::ShadingStyle shadingStyle); diff --git a/graphics/transparent_surface.cpp b/graphics/transparent_surface.cpp index c2903d42c3..81b7f3d647 100644 --- a/graphics/transparent_surface.cpp +++ b/graphics/transparent_surface.cpp @@ -346,7 +346,7 @@ Common::Rect TransparentSurface::blit(Graphics::Surface &target, int posX, int p TransparentSurface srcImage(*this, false); // TODO: Is the data really in the screen format? if (format.bytesPerPixel != 4) { - warning("TransparentSurface can only blit 32bpp images, but got %d", format.bytesPerPixel * 8); + warning("TransparentSurface can only blit 32bpp images, but got %d", format.bytesPerPixel * 8); return retSize; } @@ -979,4 +979,82 @@ TransparentSurface *TransparentSurface::scale(uint16 newWidth, uint16 newHeight) } +TransparentSurface *TransparentSurface::convertTo(const PixelFormat &dstFormat, const byte *palette) const { + assert(pixels); + + TransparentSurface *surface = new TransparentSurface(); + + // If the target format is the same, just copy + if (format == dstFormat) { + surface->copyFrom(*this); + return surface; + } + + if (format.bytesPerPixel == 0 || format.bytesPerPixel > 4) + error("Surface::convertTo(): Can only convert from 1Bpp, 2Bpp, 3Bpp, and 4Bpp"); + + if (dstFormat.bytesPerPixel != 2 && dstFormat.bytesPerPixel != 4) + error("Surface::convertTo(): Can only convert to 2Bpp and 4Bpp"); + + surface->create(w, h, dstFormat); + + if (format.bytesPerPixel == 1) { + // Converting from paletted to high color + assert(palette); + + for (int y = 0; y < h; y++) { + const byte *srcRow = (const byte *)getBasePtr(0, y); + byte *dstRow = (byte *)surface->getBasePtr(0, y); + + for (int x = 0; x < w; x++) { + byte index = *srcRow++; + byte r = palette[index * 3]; + byte g = palette[index * 3 + 1]; + byte b = palette[index * 3 + 2]; + + uint32 color = dstFormat.RGBToColor(r, g, b); + + if (dstFormat.bytesPerPixel == 2) + *((uint16 *)dstRow) = color; + else + *((uint32 *)dstRow) = color; + + dstRow += dstFormat.bytesPerPixel; + } + } + } else { + // Converting from high color to high color + for (int y = 0; y < h; y++) { + const byte *srcRow = (const byte *)getBasePtr(0, y); + byte *dstRow = (byte *)surface->getBasePtr(0, y); + + for (int x = 0; x < w; x++) { + uint32 srcColor; + if (format.bytesPerPixel == 2) + srcColor = READ_UINT16(srcRow); + else if (format.bytesPerPixel == 3) + srcColor = READ_UINT24(srcRow); + else + srcColor = READ_UINT32(srcRow); + + srcRow += format.bytesPerPixel; + + // Convert that color to the new format + byte r, g, b, a; + format.colorToARGB(srcColor, a, r, g, b); + uint32 color = dstFormat.ARGBToColor(a, r, g, b); + + if (dstFormat.bytesPerPixel == 2) + *((uint16 *)dstRow) = color; + else + *((uint32 *)dstRow) = color; + + dstRow += dstFormat.bytesPerPixel; + } + } + } + + return surface; +} + } // End of namespace Graphics diff --git a/graphics/transparent_surface.h b/graphics/transparent_surface.h index c0d3d26e52..8654183548 100644 --- a/graphics/transparent_surface.h +++ b/graphics/transparent_surface.h @@ -151,6 +151,16 @@ struct TransparentSurface : public Graphics::Surface { * */ TransparentSurface *rotoscale(const TransformStruct &transform) const; + + TransparentSurface *convertTo(const PixelFormat &dstFormat, const byte *palette = 0) const; + + float getRatio() { + if (!w) + return 0; + + return h / (float)w; + } + AlphaType getAlphaMode() const; void setAlphaMode(AlphaType); private: diff --git a/gui/ThemeEngine.cpp b/gui/ThemeEngine.cpp index c850a6a02e..96108bccce 100644 --- a/gui/ThemeEngine.cpp +++ b/gui/ThemeEngine.cpp @@ -31,11 +31,13 @@ #include "graphics/cursorman.h" #include "graphics/fontman.h" #include "graphics/surface.h" +#include "graphics/transparent_surface.h" #include "graphics/VectorRenderer.h" #include "graphics/fonts/bdf.h" #include "graphics/fonts/ttf.h" #include "image/bmp.h" +#include "image/png.h" #include "gui/widget.h" #include "gui/ThemeEngine.h" @@ -59,6 +61,10 @@ const char *const ThemeEngine::kImageStopSmallButton = "stopbtn_small.bmp"; const char *const ThemeEngine::kImageEditSmallButton = "editbtn_small.bmp"; const char *const ThemeEngine::kImageSwitchModeSmallButton = "switchbtn_small.bmp"; const char *const ThemeEngine::kImageFastReplaySmallButton = "fastreplay_small.bmp"; +const char *const ThemeEngine::kImageDropboxLogo = "dropbox.bmp"; +const char *const ThemeEngine::kImageOneDriveLogo = "onedrive.bmp"; +const char *const ThemeEngine::kImageGoogleDriveLogo = "googledrive.bmp"; +const char *const ThemeEngine::kImageBoxLogo = "box.bmp"; struct TextDrawData { const Graphics::Font *_fontPtr; @@ -168,11 +174,23 @@ protected: bool _alpha; }; +class ThemeItemABitmap : public ThemeItem { +public: + ThemeItemABitmap(ThemeEngine *engine, const Common::Rect &area, Graphics::TransparentSurface *bitmap, ThemeEngine::AutoScaleMode autoscale, int alpha) : + ThemeItem(engine, area), _bitmap(bitmap), _autoscale(autoscale), _alpha(alpha) {} + + void drawSelf(bool draw, bool restore); + +protected: + Graphics::TransparentSurface *_bitmap; + ThemeEngine::AutoScaleMode _autoscale; + int _alpha; +}; + class ThemeItemBitmapClip : public ThemeItem { public: ThemeItemBitmapClip(ThemeEngine *engine, const Common::Rect &area, const Common::Rect &clip, const Graphics::Surface *bitmap, bool alpha) : ThemeItem(engine, area), _bitmap(bitmap), _alpha(alpha), _clip(clip) {} - void drawSelf(bool draw, bool restore); protected: @@ -308,7 +326,7 @@ void ThemeItemBitmap::drawSelf(bool draw, bool restore) { if (draw) { if (_alpha) - _engine->renderer()->blitAlphaBitmap(_bitmap, _area); + _engine->renderer()->blitKeyBitmap(_bitmap, _area); else _engine->renderer()->blitSubSurface(_bitmap, _area); } @@ -316,13 +334,23 @@ void ThemeItemBitmap::drawSelf(bool draw, bool restore) { _engine->addDirtyRect(_area); } -void ThemeItemBitmapClip::drawSelf(bool draw, bool restore) { +void ThemeItemABitmap::drawSelf(bool draw, bool restore) { if (restore) _engine->restoreBackground(_area); + if (draw) + _engine->renderer()->blitAlphaBitmap(_bitmap, _area, _autoscale, Graphics::DrawStep::kVectorAlignManual, Graphics::DrawStep::kVectorAlignManual, _alpha); + + _engine->addDirtyRect(_area); +} + +void ThemeItemBitmapClip::drawSelf(bool draw, bool restore) { + if (restore) + _engine->restoreBackground(_area); + if (draw) { if (_alpha) - _engine->renderer()->blitAlphaBitmapClip(_bitmap, _area, _clip); + _engine->renderer()->blitKeyBitmapClip(_bitmap, _area, _clip); else _engine->renderer()->blitSubSurfaceClip(_bitmap, _area, _clip); } @@ -401,6 +429,15 @@ ThemeEngine::~ThemeEngine() { } _bitmaps.clear(); + for (AImagesMap::iterator i = _abitmaps.begin(); i != _abitmaps.end(); ++i) { + Graphics::TransparentSurface *surf = i->_value; + if (surf) { + surf->free(); + delete surf; + } + } + _abitmaps.clear(); + delete _parser; delete _themeEval; delete[] _cursor; @@ -525,6 +562,15 @@ void ThemeEngine::refresh() { } } _bitmaps.clear(); + + for (AImagesMap::iterator i = _abitmaps.begin(); i != _abitmaps.end(); ++i) { + Graphics::TransparentSurface *surf = i->_value; + if (surf) { + surf->free(); + delete surf; + } + } + _abitmaps.clear(); } init(); @@ -706,24 +752,51 @@ bool ThemeEngine::addBitmap(const Common::String &filename) { if (surf) return true; - // If not, try to load the bitmap via the BitmapDecoder class. - Image::BitmapDecoder bitmapDecoder; const Graphics::Surface *srcSurface = 0; - Common::ArchiveMemberList members; - _themeFiles.listMatchingMembers(members, filename); - for (Common::ArchiveMemberList::const_iterator i = members.begin(), end = members.end(); i != end; ++i) { - Common::SeekableReadStream *stream = (*i)->createReadStream(); - if (stream) { - bitmapDecoder.loadStream(*stream); - srcSurface = bitmapDecoder.getSurface(); - delete stream; - if (srcSurface) - break; + + if (filename.hasSuffix(".png")) { + // Maybe it is PNG? +#ifdef USE_PNG + Image::PNGDecoder decoder; + Common::ArchiveMemberList members; + _themeFiles.listMatchingMembers(members, filename); + for (Common::ArchiveMemberList::const_iterator i = members.begin(), end = members.end(); i != end; ++i) { + Common::SeekableReadStream *stream = (*i)->createReadStream(); + if (stream) { + if (!decoder.loadStream(*stream)) + error("Error decoding PNG"); + + srcSurface = decoder.getSurface(); + delete stream; + if (srcSurface) + break; + } } - } - if (srcSurface && srcSurface->format.bytesPerPixel != 1) - surf = srcSurface->convertTo(_overlayFormat); + if (srcSurface && srcSurface->format.bytesPerPixel != 1) + surf = srcSurface->convertTo(_overlayFormat); +#else + error("No PNG support compiled in"); +#endif + } else { + // If not, try to load the bitmap via the BitmapDecoder class. + Image::BitmapDecoder bitmapDecoder; + Common::ArchiveMemberList members; + _themeFiles.listMatchingMembers(members, filename); + for (Common::ArchiveMemberList::const_iterator i = members.begin(), end = members.end(); i != end; ++i) { + Common::SeekableReadStream *stream = (*i)->createReadStream(); + if (stream) { + bitmapDecoder.loadStream(*stream); + srcSurface = bitmapDecoder.getSurface(); + delete stream; + if (srcSurface) + break; + } + } + + if (srcSurface && srcSurface->format.bytesPerPixel != 1) + surf = srcSurface->convertTo(_overlayFormat); + } // Store the surface into our hashmap (attention, may store NULL entries!) _bitmaps[filename] = surf; @@ -731,6 +804,48 @@ bool ThemeEngine::addBitmap(const Common::String &filename) { return surf != 0; } +bool ThemeEngine::addAlphaBitmap(const Common::String &filename) { + // Nothing has to be done if the bitmap already has been loaded. + Graphics::TransparentSurface *surf = _abitmaps[filename]; + if (surf) + return true; + + const Graphics::TransparentSurface *srcSurface = 0; + + if (filename.hasSuffix(".png")) { + // Maybe it is PNG? +#ifdef USE_PNG + Image::PNGDecoder decoder; + Common::ArchiveMemberList members; + _themeFiles.listMatchingMembers(members, filename); + for (Common::ArchiveMemberList::const_iterator i = members.begin(), end = members.end(); i != end; ++i) { + Common::SeekableReadStream *stream = (*i)->createReadStream(); + if (stream) { + if (!decoder.loadStream(*stream)) + error("Error decoding PNG"); + + srcSurface = new Graphics::TransparentSurface(*decoder.getSurface(), true); + delete stream; + if (srcSurface) + break; + } + } + + if (srcSurface && srcSurface->format.bytesPerPixel != 1) + surf = srcSurface->convertTo(_overlayFormat); +#else + error("No PNG support compiled in"); +#endif + } else { + error("Only PNG is supported as alphabitmap"); + } + + // Store the surface into our hashmap (attention, may store NULL entries!) + _abitmaps[filename] = surf; + + return surf != 0; +} + bool ThemeEngine::addDrawData(const Common::String &data, bool cached) { DrawData id = parseDrawDataId(data); @@ -977,7 +1092,10 @@ void ThemeEngine::queueDDTextClip(TextData type, TextColor color, const Common:: area.clip(_screen.w, _screen.h); Common::Rect textArea = drawableTextArea; if (textArea.isEmpty()) textArea = clippingArea; - else textArea.clip(clippingArea); + else { + textArea.clip(clippingArea); + if (textArea.isEmpty()) textArea = Common::Rect(0, 0, 1, 1); // one small pixel should be invisible enough + } ThemeItemTextData *q = new ThemeItemTextData(this, _texts[type], _textColors[color], area, textArea, text, alignH, alignV, ellipsis, restoreBg, deltax); @@ -1004,6 +1122,21 @@ void ThemeEngine::queueBitmap(const Graphics::Surface *bitmap, const Common::Rec } } +void ThemeEngine::queueABitmap(Graphics::TransparentSurface *bitmap, const Common::Rect &r, AutoScaleMode autoscale, int alpha) { + + Common::Rect area = r; + area.clip(_screen.w, _screen.h); + + ThemeItemABitmap *q = new ThemeItemABitmap(this, area, bitmap, autoscale, alpha); + + if (_buffering) { + _screenQueue.push_back(q); + } else { + q->drawSelf(true, false); + delete q; + } +} + void ThemeEngine::queueBitmapClip(const Graphics::Surface *bitmap, const Common::Rect &r, const Common::Rect &clip, bool alpha) { Common::Rect area = r; @@ -1291,6 +1424,8 @@ void ThemeEngine::drawDialogBackground(const Common::Rect &r, DialogBackground b case kDialogBackgroundDefault: queueDD(kDDDefaultBackground, r); break; + case kDialogBackgroundNone: + break; } } @@ -1318,6 +1453,9 @@ void ThemeEngine::drawDialogBackgroundClip(const Common::Rect &r, const Common:: case kDialogBackgroundDefault: queueDDClip(kDDDefaultBackground, r, clip); break; + case kDialogBackgroundNone: + // no op + break; } } @@ -1392,6 +1530,13 @@ void ThemeEngine::drawSurface(const Common::Rect &r, const Graphics::Surface &su queueBitmap(&surface, r, themeTrans); } +void ThemeEngine::drawASurface(const Common::Rect &r, Graphics::TransparentSurface &surface, AutoScaleMode autoscale, int alpha) { + if (!ready()) + return; + + queueABitmap(&surface, r, autoscale, alpha); +} + void ThemeEngine::drawSurfaceClip(const Common::Rect &r, const Common::Rect &clip, const Graphics::Surface &surface, WidgetStateInfo state, int alpha, bool themeTrans) { if (!ready()) return; diff --git a/gui/ThemeEngine.h b/gui/ThemeEngine.h index 3c259b4f9d..7506cee95f 100644 --- a/gui/ThemeEngine.h +++ b/gui/ThemeEngine.h @@ -32,6 +32,7 @@ #include "common/rect.h" #include "graphics/surface.h" +#include "graphics/transparent_surface.h" #include "graphics/font.h" #include "graphics/pixelformat.h" @@ -140,6 +141,7 @@ enum TextColor { class ThemeEngine { protected: typedef Common::HashMap<Common::String, Graphics::Surface *> ImagesMap; + typedef Common::HashMap<Common::String, Graphics::TransparentSurface *> AImagesMap; friend class GUI::Dialog; friend class GUI::GuiObject; @@ -169,7 +171,8 @@ public: kDialogBackgroundSpecial, kDialogBackgroundPlain, kDialogBackgroundTooltip, - kDialogBackgroundDefault + kDialogBackgroundDefault, + kDialogBackgroundNone }; /// State of the widget to be drawn @@ -223,6 +226,14 @@ public: kShadingLuminance ///< Converting colors to luminance for unused areas }; + /// AlphaBitmap scale mode selector + enum AutoScaleMode { + kAutoScaleNone = 0, ///< Use image dimensions + kAutoScaleStretch = 1, ///< Stretch image to full widget size + kAutoScaleFit = 2, ///< Scale image to widget size but keep aspect ratio + kAutoScaleNinePatch = 3 ///< 9-patch image + }; + // Special image ids for images used in the GUI static const char *const kImageLogo; ///< ScummVM logo used in the launcher static const char *const kImageLogoSmall; ///< ScummVM logo used in the GMM @@ -239,6 +250,10 @@ public: static const char *const kImageEditSmallButton; ///< Edit recording metadata in recorder onscreen dialog (for 320xY) static const char *const kImageSwitchModeSmallButton; ///< Switch mode button in recorder onscreen dialog (for 320xY) static const char *const kImageFastReplaySmallButton; ///< Fast playback mode button in recorder onscreen dialog (for 320xY) + static const char *const kImageDropboxLogo; ///< Dropbox logo used in the StorageWizardDialog + static const char *const kImageOneDriveLogo; ///< OneDrive logo used in the StorageWizardDialog + static const char *const kImageGoogleDriveLogo; ///< Google Drive logo used in the StorageWizardDialog + static const char *const kImageBoxLogo; ///< Box logo used in the StorageWizardDialog /** * Graphics mode enumeration. @@ -353,6 +368,8 @@ public: void drawSurfaceClip(const Common::Rect &r, const Common::Rect &clippingRect, const Graphics::Surface &surface, WidgetStateInfo state = kStateEnabled, int alpha = 255, bool themeTrans = false); + void drawASurface(const Common::Rect &r, Graphics::TransparentSurface &surface, AutoScaleMode autoscale, int alpha); + void drawSlider(const Common::Rect &r, int width, WidgetStateInfo state = kStateEnabled); void drawSliderClip(const Common::Rect &r, const Common::Rect &clippingRect, int width, @@ -483,6 +500,14 @@ public: bool addBitmap(const Common::String &filename); /** + * Interface for the ThemeParser class: Loads a bitmap with transparency file to use on the GUI. + * The filename is also used as its identifier. + * + * @param filename Name of the bitmap file. + */ + bool addAlphaBitmap(const Common::String &filename); + + /** * Adds a new TextStep from the ThemeParser. This will be deprecated/removed once the * new Font API is in place. FIXME: Is that so ??? */ @@ -526,10 +551,18 @@ public: return _bitmaps.contains(name) ? _bitmaps[name] : 0; } + Graphics::TransparentSurface *getAlphaBitmap(const Common::String &name) { + return _abitmaps.contains(name) ? _abitmaps[name] : 0; + } + const Graphics::Surface *getImageSurface(const Common::String &name) const { return _bitmaps.contains(name) ? _bitmaps[name] : 0; } + const Graphics::TransparentSurface *getAImageSurface(const Common::String &name) const { + return _abitmaps.contains(name) ? _abitmaps[name] : 0; + } + /** * Interface for the Theme Parser: Creates a new cursor by loading the given * bitmap and sets it as the active cursor. @@ -616,6 +649,7 @@ protected: bool elipsis, Graphics::TextAlign alignH = Graphics::kTextAlignLeft, TextAlignVertical alignV = kTextAlignVTop, int deltax = 0, const Common::Rect &drawableTextArea = Common::Rect(0, 0, 0, 0)); void queueBitmap(const Graphics::Surface *bitmap, const Common::Rect &r, bool alpha); void queueBitmapClip(const Graphics::Surface *bitmap, const Common::Rect &clippingRect, const Common::Rect &r, bool alpha); + void queueABitmap(Graphics::TransparentSurface *bitmap, const Common::Rect &r, AutoScaleMode autoscale, int alpha); /** * DEBUG: Draws a white square and writes some text next to it. @@ -656,10 +690,10 @@ protected: GUI::ThemeEval *_themeEval; /** Main screen surface. This is blitted straight into the overlay. */ - Graphics::Surface _screen; + Graphics::TransparentSurface _screen; /** Backbuffer surface. Stores previous states of the screen to blit back */ - Graphics::Surface _backBuffer; + Graphics::TransparentSurface _backBuffer; /** Sets whether the current drawing is being buffered (stored for later processing) or drawn directly to the screen. */ @@ -688,6 +722,7 @@ protected: TextColorData *_textColors[kTextColorMAX]; ImagesMap _bitmaps; + AImagesMap _abitmaps; Graphics::PixelFormat _overlayFormat; #ifdef USE_RGB_COLOR Graphics::PixelFormat _cursorFormat; diff --git a/gui/ThemeParser.cpp b/gui/ThemeParser.cpp index bd5b406ca8..bd0d2c4898 100644 --- a/gui/ThemeParser.cpp +++ b/gui/ThemeParser.cpp @@ -241,6 +241,18 @@ bool ThemeParser::parserCallback_bitmap(ParserNode *node) { return true; } +bool ThemeParser::parserCallback_alphabitmap(ParserNode *node) { + if (resolutionCheck(node->values["resolution"]) == false) { + node->ignore = true; + return true; + } + + if (!_theme->addAlphaBitmap(node->values["filename"])) + return parserError("Error loading Bitmap file '" + node->values["filename"] + "'"); + + return true; +} + bool ThemeParser::parserCallback_text(ParserNode *node) { Graphics::TextAlign alignH; GUI::ThemeEngine::TextAlignVertical alignV; @@ -323,6 +335,8 @@ static Graphics::DrawingFunctionCallback getDrawingFunctionCallback(const Common return &Graphics::VectorRenderer::drawCallback_BITMAP; if (name == "cross") return &Graphics::VectorRenderer::drawCallback_CROSS; + if (name == "alphabitmap") + return &Graphics::VectorRenderer::drawCallback_ALPHABITMAP; return 0; } @@ -448,6 +462,58 @@ bool ThemeParser::parseDrawStep(ParserNode *stepNode, Graphics::DrawStep *drawst return parserError("The given filename hasn't been loaded into the GUI."); } + if (functionName == "alphabitmap") { + if (!stepNode->values.contains("file")) + return parserError("Need to specify a filename for AlphaBitmap blitting."); + + drawstep->blitAlphaSrc = _theme->getAlphaBitmap(stepNode->values["file"]); + + if (!drawstep->blitAlphaSrc) + return parserError("The given filename hasn't been loaded into the GUI."); + + if (stepNode->values.contains("autoscale")) { + if (stepNode->values["autoscale"] == "true" || stepNode->values["autoscale"] == "stretch") { + drawstep->autoscale = ThemeEngine::kAutoScaleStretch; + } else if (stepNode->values["autoscale"] == "fit") { + drawstep->autoscale = ThemeEngine::kAutoScaleFit; + } else if (stepNode->values["autoscale"] == "9patch") { + drawstep->autoscale = ThemeEngine::kAutoScaleNinePatch; + } else { + drawstep->autoscale = ThemeEngine::kAutoScaleNone; + } + } + + if (stepNode->values.contains("xpos")) { + val = stepNode->values["xpos"]; + + if (parseIntegerKey(val, 1, &x)) + drawstep->x = x; + else if (val == "center") + drawstep->xAlign = Graphics::DrawStep::kVectorAlignCenter; + else if (val == "left") + drawstep->xAlign = Graphics::DrawStep::kVectorAlignLeft; + else if (val == "right") + drawstep->xAlign = Graphics::DrawStep::kVectorAlignRight; + else + return parserError("Invalid value for X Position"); + } + + if (stepNode->values.contains("ypos")) { + val = stepNode->values["ypos"]; + + if (parseIntegerKey(val, 1, &x)) + drawstep->y = x; + else if (val == "center") + drawstep->yAlign = Graphics::DrawStep::kVectorAlignCenter; + else if (val == "top") + drawstep->yAlign = Graphics::DrawStep::kVectorAlignTop; + else if (val == "bottom") + drawstep->yAlign = Graphics::DrawStep::kVectorAlignBottom; + else + return parserError("Invalid value for Y Position"); + } + } + if (functionName == "roundedsq" || functionName == "circle" || functionName == "tab") { if (stepNode->values.contains("radius") && stepNode->values["radius"] == "auto") { drawstep->radius = 0xFF; diff --git a/gui/ThemeParser.h b/gui/ThemeParser.h index 360e3da009..155731467f 100644 --- a/gui/ThemeParser.h +++ b/gui/ThemeParser.h @@ -80,6 +80,10 @@ protected: XML_PROP(filename, true) XML_PROP(resolution, false) KEY_END() + XML_KEY(alphabitmap) + XML_PROP(filename, true) + XML_PROP(resolution, false) + KEY_END() KEY_END() XML_KEY(cursor) @@ -142,6 +146,7 @@ protected: XML_PROP(padding, false) XML_PROP(orientation, false) XML_PROP(file, false) + XML_PROP(autoscale, false) KEY_END() XML_KEY(text) @@ -224,6 +229,7 @@ protected: bool parserCallback_drawdata(ParserNode *node); bool parserCallback_bitmaps(ParserNode *node) { return true; } bool parserCallback_bitmap(ParserNode *node); + bool parserCallback_alphabitmap(ParserNode *node); bool parserCallback_cursor(ParserNode *node); diff --git a/gui/animation/AccelerateInterpolator.h b/gui/animation/AccelerateInterpolator.h new file mode 100644 index 0000000000..31494d369f --- /dev/null +++ b/gui/animation/AccelerateInterpolator.h @@ -0,0 +1,45 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +// Based on code by omergilad. + +#ifndef GUI_ANIMATION_ACCELERATEINTERPOLATOR_H +#define GUI_ANIMATION_ACCELERATEINTERPOLATOR_H + +#include "gui/animation/Interpolator.h" + +namespace GUI { + +class AccelerateInterpolator: public Interpolator { +public: + AccelerateInterpolator() {} + virtual ~AccelerateInterpolator() {} + + virtual float interpolate(float linearValue) { + return pow(linearValue, 2); + } + +}; + +} // End of namespace GUI + +#endif /* GUI_ANIMATION_ACCELERATEINTERPOLATOR_H */ diff --git a/gui/animation/AlphaAnimation.h b/gui/animation/AlphaAnimation.h new file mode 100644 index 0000000000..82cf3d4ba9 --- /dev/null +++ b/gui/animation/AlphaAnimation.h @@ -0,0 +1,53 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +// Based on code by omergilad. + +#ifndef GUI_ANIMATION_ANIMATION_H +#define GUI_ANIMATION_ANIMATION_H + +#include "gui/animation/Animation.h" + +namespace GUI { + +class AlphaAnimation: public Animation { +public: + AlphaAnimation() {} + virtual ~AlphaAnimation() {} + float getEndAlpha() const { return _endAlpha; } + void setEndAlpha(float endAlpha) { _endAlpha = endAlpha; } + float getStartAlpha() const { return _startAlpha; } + void setStartAlpha(float startAlpha) { _startAlpha = startAlpha; } + +protected: + virtual void updateInternal(Drawable* drawable, float interpolation) { + // Calculate alpha value based on properties and interpolation + drawable->setAlpha(_startAlpha * (1 - interpolation) + _endAlpha * interpolation); + } + + float _startAlpha; + float _endAlpha; +}; + +} // End of namespace GUI + +#endif /* GUI_ANIMATION_ANIMATION_H */ diff --git a/gui/animation/Animation.cpp b/gui/animation/Animation.cpp new file mode 100644 index 0000000000..ee4d900b4d --- /dev/null +++ b/gui/animation/Animation.cpp @@ -0,0 +1,98 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +// Based on code by omergilad. + +#include "gui/animation/Animation.h" + +namespace GUI { + +Animation::Animation() + : _startTime(0), _duration(0), _finished(false), _finishOnEnd(true) { +} + +Animation::~Animation() { +} + +void Animation::start(long currentTime) { + _finished = false; + _startTime = currentTime; +} + +void Animation::setDuration(long duration) { + _duration = duration; +} + +void Animation::update(Drawable *drawable, long currentTime) { + float interpolation; + + if (currentTime < _startTime) { + // If the start time is in the future, nothing changes - the interpolated value is 0 + interpolation = 0; + } else if (currentTime > _startTime + _duration) { + // If the animation is finished, the interpolated value is 1 and the animation is marked as finished + interpolation = 1; + finishAnimation(); + } else { + // Calculate the interpolated value + interpolation = (currentTime - _startTime) / (float) (_duration); + } + + // Activate the interpolator if present + if (_interpolator.get() != NULL) { + interpolation = _interpolator->interpolate(interpolation); + } + + updateInternal(drawable, interpolation); +} + +void Animation::finishAnimation() { + if (_finishOnEnd) { + _finished = true; + } +} + +void Animation::updateInternal(Drawable *drawable, float interpolation) { + // Default implementation +} + +bool Animation::isFinished() const { + return _finished; +} + +bool Animation::isFinishOnEnd() const { + return _finishOnEnd; +} + +void Animation::setFinishOnEnd(bool finishOnEnd) { + _finishOnEnd = finishOnEnd; +} + +InterpolatorPtr Animation::getInterpolator() const { + return _interpolator; +} + +void Animation::setInterpolator(InterpolatorPtr interpolator) { + _interpolator = interpolator; +} + +} // End of namespace GUI diff --git a/gui/animation/Animation.h b/gui/animation/Animation.h new file mode 100644 index 0000000000..300720b419 --- /dev/null +++ b/gui/animation/Animation.h @@ -0,0 +1,76 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +// Based on code by omergilad. + +#ifndef GUI_ANIMATION_ANIMATION_H +#define GUI_ANIMATION_ANIMATION_H + +#include "gui/animation/Interpolator.h" + +namespace GUI { + +class Drawable; + +class Animation { +public: + Animation(); + virtual ~Animation() = 0; + + virtual void update(Drawable *drawable, long currentTime); + + /** + * Set start time in millis + */ + virtual void start(long currentTime); + + /** + * Set duration in millis + */ + virtual void setDuration(long duration); + + virtual bool isFinished() const; + + bool isFinishOnEnd() const; + + void setFinishOnEnd(bool finishOnEnd); + + InterpolatorPtr getInterpolator() const; + void setInterpolator(InterpolatorPtr interpolator); + +protected: + void finishAnimation(); + + virtual void updateInternal(Drawable *drawable, float interpolation); + + long _startTime; + long _duration; + bool _finished; + bool _finishOnEnd; + InterpolatorPtr _interpolator; +}; + +typedef Common::SharedPtr<Animation> AnimationPtr; + +} // End of namespace GUI + +#endif /* GUI_ANIMATION_ANIMATION_H */ diff --git a/gui/animation/DeccelerateInterpolator.h b/gui/animation/DeccelerateInterpolator.h new file mode 100644 index 0000000000..e25ff6a4ba --- /dev/null +++ b/gui/animation/DeccelerateInterpolator.h @@ -0,0 +1,41 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +// Based on code by omergilad. + +#ifndef GUI_ANIMATION_DECCELERATEINTERPOLATOR_H +#define GUI_ANIMATION_DECCELERATEINTERPOLATOR_H + +#include "gui/animation/Interpolator.h" + +namespace GUI { + +class DeccelerateInterpolator: public Interpolator { +public: + DeccelerateInterpolator() {} + virtual ~DeccelerateInterpolator() {} + virtual float interpolate(float linearValue) { return sqrt(linearValue); } +}; + +} // End of namespace GUI + +#endif /* GUI_ANIMATION_DECCELERATEINTERPOLATOR_H */ diff --git a/gui/animation/Drawable.h b/gui/animation/Drawable.h new file mode 100644 index 0000000000..cf604270aa --- /dev/null +++ b/gui/animation/Drawable.h @@ -0,0 +1,109 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +// Based on code by omergilad. + +#ifndef GUI_ANIMATION_DRAWABLE_H +#define GUI_ANIMATION_DRAWABLE_H + +#include "common/ptr.h" +#include "graphics/transparent_surface.h" + +#include "gui/animation/Animation.h" + +namespace GUI { + +class Animation; +typedef Common::SharedPtr<Animation> AnimationPtr; + +class Drawable { +public: + Drawable() : + _bitmap(NULL), _positionX(0), _positionY(0), _width(0), _height(0), _alpha(1), + _usingSnapshot(false), _shouldCenter(false) { + _displayRatio = 1.0; + } + + virtual ~Drawable() { + if (_usingSnapshot) + delete _bitmap; + } + + void updateAnimation(long currentTime) { + if (_animation.get() != NULL) { + _animation->update(this, currentTime); + } + } + + bool isAnimationFinished() { + if (_animation.get() != NULL) + return _animation->isFinished(); + + return false; + } + + float getAlpha() const { return _alpha; } + void setAlpha(float alpha) { _alpha = alpha; } + AnimationPtr getAnimation() const { return _animation; } + void setAnimation(AnimationPtr animation) { _animation = animation; } + Graphics::TransparentSurface *getBitmap() const { return _bitmap; } + void setBitmap(Graphics::TransparentSurface *bitmap) { _bitmap = bitmap; } + float getPositionX() const { return _positionX; } + void setPositionX(float positionX) { _positionX = positionX; } + float getPositionY() const { return _positionY; } + void setPositionY(float positionY) { _positionY = positionY; } + virtual float getWidth() const { return _width; } + void setWidth(float width) { _width = width; } + + virtual float getHeight() const { + if (_height == 0) + return getWidth() * _bitmap->getRatio() * _displayRatio; + + return _height; + } + + void setHeight(float height) { _height = height; } + void setDisplayRatio(float ratio) { _displayRatio = ratio; } + inline bool shouldCenter() const { return _shouldCenter; } + void setShouldCenter(bool shouldCenter) { _shouldCenter = shouldCenter; } + +protected: + bool _usingSnapshot; + +private: + Graphics::TransparentSurface *_bitmap; + float _positionX; + float _positionY; + float _width; + float _height; + float _alpha; + bool _shouldCenter; + AnimationPtr _animation; + + float _displayRatio; +}; + +typedef Common::SharedPtr<Drawable> DrawablePtr; + +} // End of namespace GUI + +#endif /* GUI_ANIMATION_DRAWABLE_H */ diff --git a/gui/animation/Interpolator.h b/gui/animation/Interpolator.h new file mode 100644 index 0000000000..72d7acb8d4 --- /dev/null +++ b/gui/animation/Interpolator.h @@ -0,0 +1,44 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +// Based on code by omergilad. + +#ifndef GUI_ANIMATION_INTERPOLATOR_H +#define GUI_ANIMATION_INTERPOLATOR_H + +#include "common/ptr.h" + +namespace GUI { + +class Interpolator { +public: + Interpolator() {} + virtual ~Interpolator() {} + + virtual float interpolate(float linearValue) = 0; +}; + +typedef Common::SharedPtr<Interpolator> InterpolatorPtr; + +} // End of namespace GUI + +#endif /* GUI_ANIMATION_INTERPOLATOR_H */ diff --git a/gui/animation/ParallelAnimation.h b/gui/animation/ParallelAnimation.h new file mode 100644 index 0000000000..ce1f599fb1 --- /dev/null +++ b/gui/animation/ParallelAnimation.h @@ -0,0 +1,72 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +// Based on code by omergilad. + +#ifndef GUI_ANIMATION_PARALLELANIMATION_H +#define GUI_ANIMATION_PARALLELANIMATION_H + +#include "gui/animation/Animation.h" +#include "common/array.h" + +namespace GUI { + +class ParallelAnimation: public Animation { +public: + ParallelAnimation() {} + virtual ~ParallelAnimation() {} + + virtual void addAnimation(AnimationPtr animation) { + _animations.push_back(animation); + } + + virtual void update(Drawable *drawable, long currentTime) { + for (AnimationPtr anim : _animations) { + anim->update(drawable, currentTime); + if (anim->isFinished()) { + finishAnimation(); + } + } + } + + virtual void start(long currentTime) { + Animation::start(currentTime); + + for (AnimationPtr anim : _animations) + anim->start(currentTime); + } + + virtual void setDuration(long duration) { + Animation::setDuration(duration); + + for (AnimationPtr anim : _animations) + anim->setDuration(duration); + } + +private: + + Common::Array<AnimationPtr> _animations; +}; + +} // End of namespace GUI + +#endif /* GUI_ANIMATION_PARALLELANIMATION_H */ diff --git a/gui/animation/RepeatAnimationWrapper.cpp b/gui/animation/RepeatAnimationWrapper.cpp new file mode 100644 index 0000000000..a7e1413093 --- /dev/null +++ b/gui/animation/RepeatAnimationWrapper.cpp @@ -0,0 +1,52 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +// Based on code by omergilad. + +#include "gui/animation/RepeatAnimationWrapper.h" + +namespace GUI { + +void RepeatAnimationWrapper::update(Drawable* drawable, long currentTime) { + // Update wrapped animation + _animation->update(drawable, currentTime); + + // If the animation is finished, increase the repeat count and restart it if needed + if (_animation->isFinished()) { + ++_repeatCount; + if (_timesToRepeat > 0 && _repeatCount >= _timesToRepeat) { + finishAnimation(); + } else { + _animation->start(currentTime); + } + } +} + +void RepeatAnimationWrapper::start(long currentTime) { + Animation::start(currentTime); + _repeatCount = 0; + + // Start wrapped animation + _animation->start(currentTime); +} + +} // End of namespace GUI diff --git a/gui/animation/RepeatAnimationWrapper.h b/gui/animation/RepeatAnimationWrapper.h new file mode 100644 index 0000000000..3d766dd1c5 --- /dev/null +++ b/gui/animation/RepeatAnimationWrapper.h @@ -0,0 +1,61 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +// Based on code by omergilad. + +#ifndef GUI_ANIMATION_REPEATANIMATIONWRAPPER_H +#define GUI_ANIMATION_REPEATANIMATIONWRAPPER_H + +#include "gui/animation/Animation.h" + +namespace GUI { + +class RepeatAnimationWrapper: public Animation { +public: + /** + * Animation - animation to repeat + * + * timesToRepeat - 0 means infinite + */ + RepeatAnimationWrapper(AnimationPtr animation, uint16 timesToRepeat) : + _animation(animation), _timesToRepeat(timesToRepeat) {} + + virtual ~RepeatAnimationWrapper() {} + + virtual void update(Drawable* drawable, long currentTime); + + /** + * Set start time in millis + */ + virtual void start(long currentTime); + +private: + uint16 _timesToRepeat; + uint16 _repeatCount; + + AnimationPtr _animation; + +}; + +} // End of namespace GUI + +#endif /* GUI_ANIMATION_REPEATANIMATIONWRAPPER_H */ diff --git a/gui/animation/ScaleAnimation.h b/gui/animation/ScaleAnimation.h new file mode 100644 index 0000000000..80a4ae6305 --- /dev/null +++ b/gui/animation/ScaleAnimation.h @@ -0,0 +1,69 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +// Based on code by omergilad. + +#ifndef GUI_ANIMATION_SCALEANIMATION_H +#define GUI_ANIMATION_SCALEANIMATION_H + +#include "gui/animation/Animation.h" + +namespace GUI { + +class ScaleAnimation: public Animation { +public: + ScaleAnimation() : _endWidth(0), _endWidthFactor(0) {} + + virtual ~ScaleAnimation() {} + + float getEndWidth() const { return _endWidth; } + void setEndWidth(float endWidth) { _endWidth = endWidth; } + float getEndWidthFactor() const { return _endWidthFactor; } + void setEndWidthFactor(float endWidthFactor) { _endWidthFactor = endWidthFactor; } + float getStartWidth() const { return _startWidth; } + void setStartWidth(float startWidth) { _startWidth = startWidth; } + + void updateInternal(Drawable *drawable, float interpolation) { + // If start width was set as 0 -> use the current width as the start dimension + if (_startWidth == 0) + _startWidth = drawable->getWidth(); + + // If end width was set as 0 - multiply the start width by the given factor + if (_endWidth == 0) + _endWidth = _startWidth * _endWidthFactor; + + // Calculate width based on interpolation + float width = _startWidth * (1 - interpolation) + _endWidth * interpolation; + drawable->setWidth(width); + } + +private: + virtual void updateInternal(Drawable *drawable, float interpolation); + float _startWidth; + float _endWidth; + float _endWidthFactor; +}; + +} // End of namespace GUI + + +#endif /* GUI_ANIMATION_SCALEANIMATION_H */ diff --git a/gui/animation/SequenceAnimationComposite.cpp b/gui/animation/SequenceAnimationComposite.cpp new file mode 100644 index 0000000000..9ecfeebc8c --- /dev/null +++ b/gui/animation/SequenceAnimationComposite.cpp @@ -0,0 +1,72 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +// Based on code by omergilad. + +#include "gui/animation/SequenceAnimationComposite.h" + +namespace GUI { + +void SequenceAnimationComposite::start(long currentTime) { + Animation::start(currentTime); + + // The first animation in the sequence should a start time equal to this sequence + if (_sequence.size() >= 1) + _sequence[0]->start(currentTime); + + // Set the index to 0 + _index = 0; +} + +void SequenceAnimationComposite::addAnimation(AnimationPtr animation) { + _sequence.push_back(animation); +} + +void SequenceAnimationComposite::update(Drawable *drawable, long currentTime) { + uint16 sequenceSize = _sequence.size(); + + // Check index bounds + if (_index >= sequenceSize) + return; + + // Get the current animation in the sequence + AnimationPtr anim = _sequence[_index]; + + // Update the drawable + anim->update(drawable, currentTime); + + // Check if the current animation is finished + if (anim->isFinished()) { + // Increase the index - move to the next animation + ++_index; + + if (_index >= sequenceSize) { + // Finished the sequence + finishAnimation(); + } else { + // Set the start time for the next animation + _sequence[_index]->start(currentTime); + } + } +} + +} // End of namespace GUI diff --git a/gui/animation/SequenceAnimationComposite.h b/gui/animation/SequenceAnimationComposite.h new file mode 100644 index 0000000000..4ec0331751 --- /dev/null +++ b/gui/animation/SequenceAnimationComposite.h @@ -0,0 +1,51 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +// Based on code by omergilad. + +#ifndef GUI_ANIMATION_SEQUENCEANIMATION_H +#define GUI_ANIMATION_SEQUENCEANIMATION_H + +#include "gui/animation/Animation.h" +#include "common/array.h" + +namespace GUI { + +class SequenceAnimationComposite: public Animation { +public: + SequenceAnimationComposite() {} + virtual ~SequenceAnimationComposite() {} + + virtual void addAnimation(AnimationPtr animation); + + virtual void update(Drawable* drawable, long currentTime); + + virtual void start(long currentTime); + +private: + uint16 _index; + Common::Array<AnimationPtr> _sequence; +}; + +} // End of namespace GUI + +#endif /* GUI_ANIMATION_SEQUENCEANIMATION_H */ diff --git a/gui/animation/WaitForConditionAnimation.h b/gui/animation/WaitForConditionAnimation.h new file mode 100644 index 0000000000..5a67a37493 --- /dev/null +++ b/gui/animation/WaitForConditionAnimation.h @@ -0,0 +1,71 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +// Based on code by omergilad. + +#ifndef GUI_ANIMATION_WAITFORCONDITIONANIMATION_H +#define GUI_ANIMATION_WAITFORCONDITIONANIMATION_H + +#include "gui/animation/Animation.h" + +namespace GUI { + +class Condition { + +public: + virtual ~Condition() {} + + virtual bool evaluate() = 0; +}; + +typedef Common::SharedPtr<Condition> ConditionPtr; + +/** + * Used for delaying the animation sequence until a certain condition has been met + */ +class WaitForConditionAnimation: public Animation { +public: + WaitForConditionAnimation() {} + virtual ~WaitForConditionAnimation() {} + + virtual void update(Drawable *drawable, long currentTime) { + // Check the condition - if it has been met, finish. + if (_condition.get() != NULL && _condition->evaluate()) { + finishAnimation(); + } + } + + ConditionPtr getCondition() const { + return _condition; + } + + void setCondition(ConditionPtr condition) { + _condition = condition; + } + +private: + ConditionPtr _condition; +}; + +} // End of namespace GUI + +#endif /* GUI_ANIMATION_WAITFORCONDITIONANIMATION_H */ diff --git a/gui/debugger.cpp b/gui/debugger.cpp index 72d05e2973..7595efcfbc 100644 --- a/gui/debugger.cpp +++ b/gui/debugger.cpp @@ -584,7 +584,7 @@ bool Debugger::cmdMd5(int argc, const char **argv) { length = atoi(argv[2]); paramOffset = 2; } - + // Assume that spaces are part of a single filename. Common::String filename = argv[1 + paramOffset]; for (int i = 2 + paramOffset; i < argc; i++) { @@ -624,7 +624,7 @@ bool Debugger::cmdMd5Mac(int argc, const char **argv) { length = atoi(argv[2]); paramOffset = 2; } - + // Assume that spaces are part of a single filename. Common::String filename = argv[1 + paramOffset]; for (int i = 2 + paramOffset; i < argc; i++) { diff --git a/gui/downloaddialog.cpp b/gui/downloaddialog.cpp new file mode 100644 index 0000000000..ea7ace2f2a --- /dev/null +++ b/gui/downloaddialog.cpp @@ -0,0 +1,272 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "gui/downloaddialog.h" +#include "backends/cloud/cloudmanager.h" +#include "backends/networking/connection/islimited.h" +#include "common/config-manager.h" +#include "common/translation.h" +#include "engines/metaengine.h" +#include "gui/browser.h" +#include "gui/chooser.h" +#include "gui/editgamedialog.h" +#include "gui/launcher.h" +#include "gui/message.h" +#include "gui/remotebrowser.h" +#include "gui/widgets/edittext.h" +#include "gui/widgets/list.h" + +namespace GUI { + +enum { + kDownloadDialogButtonCmd = 'Dldb' +}; + +DownloadDialog::DownloadDialog(uint32 storageId, LauncherDialog *launcher) : + Dialog("GlobalOptions_Cloud_DownloadDialog"), _launcher(launcher), _close(false) { + _backgroundType = GUI::ThemeEngine::kDialogBackgroundPlain; + + _browser = new BrowserDialog(_("Select directory where to download game data"), true); + _remoteBrowser = new RemoteBrowserDialog(_("Select directory with game data")); + + _remoteDirectoryLabel = new StaticTextWidget(this, "GlobalOptions_Cloud_DownloadDialog.RemoteDirectory", _("From: ")); + _localDirectoryLabel = new StaticTextWidget(this, "GlobalOptions_Cloud_DownloadDialog.LocalDirectory", _("To: ")); + uint32 progress = (uint32)(100 * CloudMan.getDownloadingProgress()); + _progressBar = new SliderWidget(this, "GlobalOptions_Cloud_DownloadDialog.ProgressBar"); + _progressBar->setMinValue(0); + _progressBar->setMaxValue(100); + _progressBar->setValue(progress); + _progressBar->setEnabled(false); + _percentLabel = new StaticTextWidget(this, "GlobalOptions_Cloud_DownloadDialog.PercentText", Common::String::format("%u %%", progress)); + _downloadSizeLabel = new StaticTextWidget(this, "GlobalOptions_Cloud_DownloadDialog.DownloadSize", ""); + _downloadSpeedLabel = new StaticTextWidget(this, "GlobalOptions_Cloud_DownloadDialog.DownloadSpeed", ""); + if (g_system->getOverlayWidth() > 320) + _cancelButton = new ButtonWidget(this, "GlobalOptions_Cloud_DownloadDialog.MainButton", _("Cancel download"), 0, kDownloadDialogButtonCmd); + else + _cancelButton = new ButtonWidget(this, "GlobalOptions_Cloud_DownloadDialog.MainButton", _c("Cancel download", "lowres"), 0, kDownloadDialogButtonCmd); + + _closeButton = new ButtonWidget(this, "GlobalOptions_Cloud_DownloadDialog.CloseButton", _("Hide"), 0, kCloseCmd); + refreshWidgets(); + + CloudMan.setDownloadTarget(this); +} + +DownloadDialog::~DownloadDialog() { + CloudMan.setDownloadTarget(nullptr); +} + +void DownloadDialog::open() { + Dialog::open(); + CloudMan.setDownloadTarget(this); + if (!CloudMan.isDownloading()) + if (!selectDirectories()) + close(); + reflowLayout(); + draw(); +} + +void DownloadDialog::close() { + CloudMan.setDownloadTarget(nullptr); + Dialog::close(); +} + +void DownloadDialog::handleCommand(CommandSender *sender, uint32 cmd, uint32 data) { + switch (cmd) { + case kDownloadDialogButtonCmd: + { + CloudMan.setDownloadTarget(nullptr); + CloudMan.cancelDownload(); + close(); + break; + } + case kDownloadProgressCmd: + if (!_close) { + refreshWidgets(); + draw(); + } + break; + case kDownloadEndedCmd: + _close = true; + break; + default: + Dialog::handleCommand(sender, cmd, data); + } +} + +bool DownloadDialog::selectDirectories() { + if (Networking::Connection::isLimited()) { + MessageDialog alert(_("It looks like your connection is limited. " + "Do you really want to download files with it?"), _("Yes"), _("No")); + if (alert.runModal() != GUI::kMessageOK) + return false; + } + + //first user should select remote directory to download + if (_remoteBrowser->runModal() <= 0) + return false; + + Cloud::StorageFile remoteDirectory = _remoteBrowser->getResult(); + + //now user should select local directory to download into + if (_browser->runModal() <= 0) + return false; + + Common::FSNode dir(_browser->getResult()); + Common::FSList files; + if (!dir.getChildren(files, Common::FSNode::kListAll)) { + MessageDialog alert(_("ScummVM couldn't open the specified directory!")); + alert.runModal(); + return false; + } + + //check that there is no file with the remote directory's name in the local one + for (Common::FSList::iterator i = files.begin(); i != files.end(); ++i) { + if (i->getName().equalsIgnoreCase(remoteDirectory.name())) { + //if there is, ask user whether it's OK + if (!i->isDirectory()) { + GUI::MessageDialog alert(_("Cannot create a directory to download - the specified directory has a file with the same name."), _("OK")); + alert.runModal(); + return false; + } + GUI::MessageDialog alert( + Common::String::format(_("The \"%s\" already exists in the specified directory.\nDo you really want to download files into that directory?"), remoteDirectory.name().c_str()), + _("Yes"), + _("No") + ); + if (alert.runModal() != GUI::kMessageOK) + return false; + break; + } + } + + //make a local path + Common::String localPath = dir.getPath(); + + //simple heuristic to determine which path separator to use + if (localPath.size() && localPath.lastChar() != '/' && localPath.lastChar() != '\\') { + int backslashes = 0; + for (uint32 i = 0; i < localPath.size(); ++i) + if (localPath[i] == '/') + --backslashes; + else if (localPath[i] == '\\') + ++backslashes; + + if (backslashes > 0) + localPath += '\\' + remoteDirectory.name(); + else + localPath += '/' + remoteDirectory.name(); + } else { + localPath += remoteDirectory.name(); + } + + CloudMan.startDownload(remoteDirectory.path(), localPath); + CloudMan.setDownloadTarget(this); + _localDirectory = localPath; + return true; +} + +void DownloadDialog::handleTickle() { + if (_close) { + if (_launcher) + _launcher->doGameDetection(_localDirectory); + close(); + _close = false; + return; + } + + uint32 progress = (uint32)(100 * CloudMan.getDownloadingProgress()); + if (_progressBar->getValue() != progress) { + refreshWidgets(); + draw(); + } + + Dialog::handleTickle(); +} + +void DownloadDialog::reflowLayout() { + Dialog::reflowLayout(); + refreshWidgets(); +} + +namespace { +Common::String getHumanReadableBytes(uint64 bytes, Common::String &unitsOut) { + Common::String result = Common::String::format("%lu", bytes); + unitsOut = "B"; + + if (bytes >= 1024) { + bytes /= 1024; + result = Common::String::format("%lu", bytes); + unitsOut = "KB"; + } + + double floating = bytes; + + if (bytes >= 1024) { + bytes /= 1024; + floating /= 1024.0; + unitsOut = "MB"; + } + + if (bytes >= 1024) { + bytes /= 1024; + floating /= 1024.0; + unitsOut = "GB"; + } + + if (bytes >= 1024) { // woah + bytes /= 1024; + floating /= 1024.0; + unitsOut = "TB"; + } + + // print one digit after floating point + result = Common::String::format("%.1lf", floating); + return result; +} +} + +Common::String DownloadDialog::getSizeLabelText() { + Common::String downloaded, downloadedUnits, total, totalUnits; + downloaded = getHumanReadableBytes(CloudMan.getDownloadBytesNumber(), downloadedUnits); + total = getHumanReadableBytes(CloudMan.getDownloadTotalBytesNumber(), totalUnits); + return Common::String::format(_("Downloaded %s %s / %s %s"), downloaded.c_str(), _(downloadedUnits.c_str()), total.c_str(), _(totalUnits.c_str())); +} + +Common::String DownloadDialog::getSpeedLabelText() { + Common::String speed, speedUnits; + speed = getHumanReadableBytes(CloudMan.getDownloadSpeed(), speedUnits); + speedUnits += "/s"; + return Common::String::format("Download speed: %s %s", speed.c_str(), _(speedUnits.c_str())); +} + +void DownloadDialog::refreshWidgets() { + _localDirectory = CloudMan.getDownloadLocalDirectory(); + _remoteDirectoryLabel->setLabel(_("From: ") + CloudMan.getDownloadRemoteDirectory()); + _localDirectoryLabel->setLabel(_("To: ") + _localDirectory); + uint32 progress = (uint32)(100 * CloudMan.getDownloadingProgress()); + _percentLabel->setLabel(Common::String::format("%u %%", progress)); + _downloadSizeLabel->setLabel(getSizeLabelText()); + _downloadSpeedLabel->setLabel(getSpeedLabelText()); + _progressBar->setValue(progress); +} + +} // End of namespace GUI diff --git a/gui/downloaddialog.h b/gui/downloaddialog.h new file mode 100644 index 0000000000..9080717195 --- /dev/null +++ b/gui/downloaddialog.h @@ -0,0 +1,81 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef GUI_DOWNLOADDIALOG_H +#define GUI_DOWNLOADDIALOG_H + +#include "gui/dialog.h" +#include "common/str.h" + +namespace GUI { +class LauncherDialog; + +class CommandSender; +class EditTextWidget; +class StaticTextWidget; +class ButtonWidget; +class SliderWidget; +class BrowserDialog; +class RemoteBrowserDialog; + +enum DownloadProgress { + kDownloadProgressCmd = 'DLPR', + kDownloadEndedCmd = 'DLEN' +}; + +class DownloadDialog : public Dialog { + LauncherDialog *_launcher; + BrowserDialog *_browser; + RemoteBrowserDialog *_remoteBrowser; + + StaticTextWidget *_remoteDirectoryLabel; + StaticTextWidget *_localDirectoryLabel; + StaticTextWidget *_percentLabel; + StaticTextWidget *_downloadSizeLabel; + StaticTextWidget *_downloadSpeedLabel; + SliderWidget *_progressBar; + ButtonWidget *_cancelButton; + ButtonWidget *_closeButton; + + Common::String _localDirectory; + bool _close; + + Common::String getSizeLabelText(); + Common::String getSpeedLabelText(); + + void refreshWidgets(); + bool selectDirectories(); + +public: + DownloadDialog(uint32 storageId, LauncherDialog *launcher); + virtual ~DownloadDialog(); + + virtual void open(); + virtual void close(); + virtual void handleCommand(CommandSender *sender, uint32 cmd, uint32 data); + virtual void handleTickle(); + virtual void reflowLayout(); +}; + +} // End of namespace GUI + +#endif diff --git a/gui/editgamedialog.cpp b/gui/editgamedialog.cpp new file mode 100644 index 0000000000..fbe76d228a --- /dev/null +++ b/gui/editgamedialog.cpp @@ -0,0 +1,550 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "gui/editgamedialog.h" + +#include "common/config-manager.h" +#include "common/gui_options.h" +#include "common/translation.h" +#include "common/system.h" + +#include "gui/browser.h" +#include "gui/message.h" +#ifdef ENABLE_EVENTRECORDER +#include "gui/onscreendialog.h" +#include "gui/recorderdialog.h" +#include "gui/EventRecorder.h" +#endif +#include "gui/widgets/edittext.h" +#include "gui/widgets/tab.h" +#include "gui/widgets/popup.h" + +#ifdef USE_LIBCURL +#include "backends/cloud/cloudmanager.h" +#endif + +using Common::ConfigManager; + +namespace GUI { + +enum { + kStartCmd = 'STRT', + kAboutCmd = 'ABOU', + kOptionsCmd = 'OPTN', + kAddGameCmd = 'ADDG', + kEditGameCmd = 'EDTG', + kRemoveGameCmd = 'REMG', + kLoadGameCmd = 'LOAD', + kQuitCmd = 'QUIT', + kSearchCmd = 'SRCH', + kListSearchCmd = 'LSSR', + kSearchClearCmd = 'SRCL', + + kCmdGlobalGraphicsOverride = 'OGFX', + kCmdGlobalAudioOverride = 'OSFX', + kCmdGlobalMIDIOverride = 'OMID', + kCmdGlobalMT32Override = 'OM32', + kCmdGlobalVolumeOverride = 'OVOL', + + kCmdChooseSoundFontCmd = 'chsf', + + kCmdExtraBrowser = 'PEXT', + kCmdExtraPathClear = 'PEXC', + kCmdGameBrowser = 'PGME', + kCmdSaveBrowser = 'PSAV', + kCmdSavePathClear = 'PSAC' +}; + +/* +* TODO: Clean up this ugly design: we subclass EditTextWidget to perform +* input validation. It would be much more elegant to use a decorator pattern, +* or a validation callback, or something like that. +*/ +class DomainEditTextWidget : public EditTextWidget { +public: + DomainEditTextWidget(GuiObject *boss, const String &name, const String &text, const char *tooltip = 0) + : EditTextWidget(boss, name, text, tooltip) {} + +protected: + bool tryInsertChar(byte c, int pos) { + if (Common::isAlnum(c) || c == '-' || c == '_') { + _editString.insertChar(c, pos); + return true; + } + return false; + } +}; + +EditGameDialog::EditGameDialog(const String &domain, const String &desc) + : OptionsDialog(domain, "GameOptions") { + // Retrieve all game specific options. + const EnginePlugin *plugin = 0; + // To allow for game domains without a gameid. + // TODO: Is it intentional that this is still supported? + String gameId(ConfMan.get("gameid", domain)); + if (gameId.empty()) + gameId = domain; + // Retrieve the plugin, since we need to access the engine's MetaEngine + // implementation. + EngineMan.findGame(gameId, &plugin); + if (plugin) { + _engineOptions = (*plugin)->getExtraGuiOptions(domain); + } else { + warning("Plugin for target \"%s\" not found! Game specific settings might be missing", domain.c_str()); + } + + // GAME: Path to game data (r/o), extra data (r/o), and save data (r/w) + String gamePath(ConfMan.get("path", _domain)); + String extraPath(ConfMan.get("extrapath", _domain)); + String savePath(ConfMan.get("savepath", _domain)); + + // GAME: Determine the description string + String description(ConfMan.get("description", domain)); + if (description.empty() && !desc.empty()) { + description = desc; + } + + // GUI: Add tab widget + TabWidget *tab = new TabWidget(this, "GameOptions.TabWidget"); + + // + // 1) The game tab + // + tab->addTab(_("Game")); + + // GUI: Label & edit widget for the game ID + if (g_system->getOverlayWidth() > 320) + new StaticTextWidget(tab, "GameOptions_Game.Id", _("ID:"), _("Short game identifier used for referring to saved games and running the game from the command line")); + else + new StaticTextWidget(tab, "GameOptions_Game.Id", _c("ID:", "lowres"), _("Short game identifier used for referring to saved games and running the game from the command line")); + _domainWidget = new DomainEditTextWidget(tab, "GameOptions_Game.Domain", _domain, _("Short game identifier used for referring to saved games and running the game from the command line")); + + // GUI: Label & edit widget for the description + if (g_system->getOverlayWidth() > 320) + new StaticTextWidget(tab, "GameOptions_Game.Name", _("Name:"), _("Full title of the game")); + else + new StaticTextWidget(tab, "GameOptions_Game.Name", _c("Name:", "lowres"), _("Full title of the game")); + _descriptionWidget = new EditTextWidget(tab, "GameOptions_Game.Desc", description, _("Full title of the game")); + + // Language popup + _langPopUpDesc = new StaticTextWidget(tab, "GameOptions_Game.LangPopupDesc", _("Language:"), _("Language of the game. This will not turn your Spanish game version into English")); + _langPopUp = new PopUpWidget(tab, "GameOptions_Game.LangPopup", _("Language of the game. This will not turn your Spanish game version into English")); + _langPopUp->appendEntry(_("<default>"), (uint32)Common::UNK_LANG); + _langPopUp->appendEntry("", (uint32)Common::UNK_LANG); + const Common::LanguageDescription *l = Common::g_languages; + for (; l->code; ++l) { + if (checkGameGUIOptionLanguage(l->id, _guioptionsString)) + _langPopUp->appendEntry(l->description, l->id); + } + + // Platform popup + if (g_system->getOverlayWidth() > 320) + _platformPopUpDesc = new StaticTextWidget(tab, "GameOptions_Game.PlatformPopupDesc", _("Platform:"), _("Platform the game was originally designed for")); + else + _platformPopUpDesc = new StaticTextWidget(tab, "GameOptions_Game.PlatformPopupDesc", _c("Platform:", "lowres"), _("Platform the game was originally designed for")); + _platformPopUp = new PopUpWidget(tab, "GameOptions_Game.PlatformPopup", _("Platform the game was originally designed for")); + _platformPopUp->appendEntry(_("<default>")); + _platformPopUp->appendEntry(""); + const Common::PlatformDescription *p = Common::g_platforms; + for (; p->code; ++p) { + _platformPopUp->appendEntry(p->description, p->id); + } + + // + // 2) The engine tab (shown only if there are custom engine options) + // + if (_engineOptions.size() > 0) { + tab->addTab(_("Engine")); + + addEngineControls(tab, "GameOptions_Engine.", _engineOptions); + } + + // + // 3) The graphics tab + // + _graphicsTabId = tab->addTab(g_system->getOverlayWidth() > 320 ? _("Graphics") : _("GFX")); + + if (g_system->getOverlayWidth() > 320) + _globalGraphicsOverride = new CheckboxWidget(tab, "GameOptions_Graphics.EnableTabCheckbox", _("Override global graphic settings"), 0, kCmdGlobalGraphicsOverride); + else + _globalGraphicsOverride = new CheckboxWidget(tab, "GameOptions_Graphics.EnableTabCheckbox", _c("Override global graphic settings", "lowres"), 0, kCmdGlobalGraphicsOverride); + + addGraphicControls(tab, "GameOptions_Graphics."); + + // + // 4) The audio tab + // + tab->addTab(_("Audio")); + + if (g_system->getOverlayWidth() > 320) + _globalAudioOverride = new CheckboxWidget(tab, "GameOptions_Audio.EnableTabCheckbox", _("Override global audio settings"), 0, kCmdGlobalAudioOverride); + else + _globalAudioOverride = new CheckboxWidget(tab, "GameOptions_Audio.EnableTabCheckbox", _c("Override global audio settings", "lowres"), 0, kCmdGlobalAudioOverride); + + addAudioControls(tab, "GameOptions_Audio."); + addSubtitleControls(tab, "GameOptions_Audio."); + + // + // 5) The volume tab + // + if (g_system->getOverlayWidth() > 320) + tab->addTab(_("Volume")); + else + tab->addTab(_c("Volume", "lowres")); + + if (g_system->getOverlayWidth() > 320) + _globalVolumeOverride = new CheckboxWidget(tab, "GameOptions_Volume.EnableTabCheckbox", _("Override global volume settings"), 0, kCmdGlobalVolumeOverride); + else + _globalVolumeOverride = new CheckboxWidget(tab, "GameOptions_Volume.EnableTabCheckbox", _c("Override global volume settings", "lowres"), 0, kCmdGlobalVolumeOverride); + + addVolumeControls(tab, "GameOptions_Volume."); + + // + // 6) The MIDI tab + // + _globalMIDIOverride = NULL; + if (!_guioptions.contains(GUIO_NOMIDI)) { + tab->addTab(_("MIDI")); + + if (g_system->getOverlayWidth() > 320) + _globalMIDIOverride = new CheckboxWidget(tab, "GameOptions_MIDI.EnableTabCheckbox", _("Override global MIDI settings"), 0, kCmdGlobalMIDIOverride); + else + _globalMIDIOverride = new CheckboxWidget(tab, "GameOptions_MIDI.EnableTabCheckbox", _c("Override global MIDI settings", "lowres"), 0, kCmdGlobalMIDIOverride); + + addMIDIControls(tab, "GameOptions_MIDI."); + } + + // + // 7) The MT-32 tab + // + _globalMT32Override = NULL; + if (!_guioptions.contains(GUIO_NOMIDI)) { + tab->addTab(_("MT-32")); + + if (g_system->getOverlayWidth() > 320) + _globalMT32Override = new CheckboxWidget(tab, "GameOptions_MT32.EnableTabCheckbox", _("Override global MT-32 settings"), 0, kCmdGlobalMT32Override); + else + _globalMT32Override = new CheckboxWidget(tab, "GameOptions_MT32.EnableTabCheckbox", _c("Override global MT-32 settings", "lowres"), 0, kCmdGlobalMT32Override); + + addMT32Controls(tab, "GameOptions_MT32."); + } + + // + // 8) The Paths tab + // + if (g_system->getOverlayWidth() > 320) + tab->addTab(_("Paths")); + else + tab->addTab(_c("Paths", "lowres")); + + // These buttons have to be extra wide, or the text will be truncated + // in the small version of the GUI. + + // GUI: Button + Label for the game path + if (g_system->getOverlayWidth() > 320) + new ButtonWidget(tab, "GameOptions_Paths.Gamepath", _("Game Path:"), 0, kCmdGameBrowser); + else + new ButtonWidget(tab, "GameOptions_Paths.Gamepath", _c("Game Path:", "lowres"), 0, kCmdGameBrowser); + _gamePathWidget = new StaticTextWidget(tab, "GameOptions_Paths.GamepathText", gamePath); + + // GUI: Button + Label for the additional path + if (g_system->getOverlayWidth() > 320) + new ButtonWidget(tab, "GameOptions_Paths.Extrapath", _("Extra Path:"), _("Specifies path to additional data used by the game"), kCmdExtraBrowser); + else + new ButtonWidget(tab, "GameOptions_Paths.Extrapath", _c("Extra Path:", "lowres"), _("Specifies path to additional data used by the game"), kCmdExtraBrowser); + _extraPathWidget = new StaticTextWidget(tab, "GameOptions_Paths.ExtrapathText", extraPath, _("Specifies path to additional data used by the game")); + + _extraPathClearButton = addClearButton(tab, "GameOptions_Paths.ExtraPathClearButton", kCmdExtraPathClear); + + // GUI: Button + Label for the save path + if (g_system->getOverlayWidth() > 320) + new ButtonWidget(tab, "GameOptions_Paths.Savepath", _("Save Path:"), _("Specifies where your saved games are put"), kCmdSaveBrowser); + else + new ButtonWidget(tab, "GameOptions_Paths.Savepath", _c("Save Path:", "lowres"), _("Specifies where your saved games are put"), kCmdSaveBrowser); + _savePathWidget = new StaticTextWidget(tab, "GameOptions_Paths.SavepathText", savePath, _("Specifies where your saved games are put")); + + _savePathClearButton = addClearButton(tab, "GameOptions_Paths.SavePathClearButton", kCmdSavePathClear); + + // Activate the first tab + tab->setActiveTab(0); + _tabWidget = tab; + + // Add OK & Cancel buttons + new ButtonWidget(this, "GameOptions.Cancel", _("Cancel"), 0, kCloseCmd); + new ButtonWidget(this, "GameOptions.Ok", _("OK"), 0, kOKCmd); +} + +void EditGameDialog::open() { + OptionsDialog::open(); + + String extraPath(ConfMan.get("extrapath", _domain)); + if (extraPath.empty() || !ConfMan.hasKey("extrapath", _domain)) { + _extraPathWidget->setLabel(_c("None", "path")); + } + + String savePath(ConfMan.get("savepath", _domain)); + if (savePath.empty() || !ConfMan.hasKey("savepath", _domain)) { + _savePathWidget->setLabel(_("Default")); + } + + int sel, i; + bool e; + + // En-/disable dialog items depending on whether overrides are active or not. + + e = ConfMan.hasKey("gfx_mode", _domain) || + ConfMan.hasKey("render_mode", _domain) || + ConfMan.hasKey("fullscreen", _domain) || + ConfMan.hasKey("aspect_ratio", _domain); + _globalGraphicsOverride->setState(e); + + e = ConfMan.hasKey("music_driver", _domain) || + ConfMan.hasKey("output_rate", _domain) || + ConfMan.hasKey("opl_driver", _domain) || + ConfMan.hasKey("subtitles", _domain) || + ConfMan.hasKey("talkspeed", _domain); + _globalAudioOverride->setState(e); + + e = ConfMan.hasKey("music_volume", _domain) || + ConfMan.hasKey("sfx_volume", _domain) || + ConfMan.hasKey("speech_volume", _domain); + _globalVolumeOverride->setState(e); + + if (!_guioptions.contains(GUIO_NOMIDI)) { + e = ConfMan.hasKey("soundfont", _domain) || + ConfMan.hasKey("multi_midi", _domain) || + ConfMan.hasKey("midi_gain", _domain); + _globalMIDIOverride->setState(e); + } + + if (!_guioptions.contains(GUIO_NOMIDI)) { + e = ConfMan.hasKey("native_mt32", _domain) || + ConfMan.hasKey("enable_gs", _domain); + _globalMT32Override->setState(e); + } + + // TODO: game path + + const Common::Language lang = Common::parseLanguage(ConfMan.get("language", _domain)); + + if (ConfMan.hasKey("language", _domain)) { + _langPopUp->setSelectedTag(lang); + } else { + _langPopUp->setSelectedTag((uint32)Common::UNK_LANG); + } + + if (_langPopUp->numEntries() <= 3) { // If only one language is avaliable + _langPopUpDesc->setEnabled(false); + _langPopUp->setEnabled(false); + } + + // Set the state of engine-specific checkboxes + for (uint j = 0; j < _engineOptions.size(); ++j) { + // The default values for engine-specific checkboxes are not set when + // ScummVM starts, as this would require us to load and poll all of the + // engine plugins on startup. Thus, we set the state of each custom + // option checkbox to what is specified by the engine plugin, and + // update it only if a value has been set in the configuration of the + // currently selected game. + bool isChecked = _engineOptions[j].defaultState; + if (ConfMan.hasKey(_engineOptions[j].configOption, _domain)) + isChecked = ConfMan.getBool(_engineOptions[j].configOption, _domain); + _engineCheckboxes[j]->setState(isChecked); + } + + const Common::PlatformDescription *p = Common::g_platforms; + const Common::Platform platform = Common::parsePlatform(ConfMan.get("platform", _domain)); + sel = 0; + for (i = 0; p->code; ++p, ++i) { + if (platform == p->id) + sel = i + 2; + } + _platformPopUp->setSelected(sel); +} + + +void EditGameDialog::close() { + if (getResult()) { + ConfMan.set("description", _descriptionWidget->getEditString(), _domain); + + Common::Language lang = (Common::Language)_langPopUp->getSelectedTag(); + if (lang < 0) + ConfMan.removeKey("language", _domain); + else + ConfMan.set("language", Common::getLanguageCode(lang), _domain); + + String gamePath(_gamePathWidget->getLabel()); + if (!gamePath.empty()) + ConfMan.set("path", gamePath, _domain); + + String extraPath(_extraPathWidget->getLabel()); + if (!extraPath.empty() && (extraPath != _c("None", "path"))) + ConfMan.set("extrapath", extraPath, _domain); + else + ConfMan.removeKey("extrapath", _domain); + + String savePath(_savePathWidget->getLabel()); + if (!savePath.empty() && (savePath != _("Default"))) + ConfMan.set("savepath", savePath, _domain); + else + ConfMan.removeKey("savepath", _domain); + + Common::Platform platform = (Common::Platform)_platformPopUp->getSelectedTag(); + if (platform < 0) + ConfMan.removeKey("platform", _domain); + else + ConfMan.set("platform", Common::getPlatformCode(platform), _domain); + + // Set the state of engine-specific checkboxes + for (uint i = 0; i < _engineOptions.size(); i++) { + ConfMan.setBool(_engineOptions[i].configOption, _engineCheckboxes[i]->getState(), _domain); + } + } + OptionsDialog::close(); +} + +void EditGameDialog::handleCommand(CommandSender *sender, uint32 cmd, uint32 data) { + switch (cmd) { + case kCmdGlobalGraphicsOverride: + setGraphicSettingsState(data != 0); + draw(); + break; + case kCmdGlobalAudioOverride: + setAudioSettingsState(data != 0); + setSubtitleSettingsState(data != 0); + if (_globalVolumeOverride == NULL) + setVolumeSettingsState(data != 0); + draw(); + break; + case kCmdGlobalMIDIOverride: + setMIDISettingsState(data != 0); + draw(); + break; + case kCmdGlobalMT32Override: + setMT32SettingsState(data != 0); + draw(); + break; + case kCmdGlobalVolumeOverride: + setVolumeSettingsState(data != 0); + draw(); + break; + case kCmdChooseSoundFontCmd: + { + BrowserDialog browser(_("Select SoundFont"), false); + + if (browser.runModal() > 0) { + // User made this choice... + Common::FSNode file(browser.getResult()); + _soundFont->setLabel(file.getPath()); + + if (!file.getPath().empty() && (file.getPath() != _c("None", "path"))) + _soundFontClearButton->setEnabled(true); + else + _soundFontClearButton->setEnabled(false); + + draw(); + } + break; + } + + // Change path for the game + case kCmdGameBrowser: + { + BrowserDialog browser(_("Select directory with game data"), true); + if (browser.runModal() > 0) { + // User made his choice... + Common::FSNode dir(browser.getResult()); + + // TODO: Verify the game can be found in the new directory... Best + // done with optional specific gameid to pluginmgr detectgames? + // FSList files = dir.listDir(FSNode::kListFilesOnly); + + _gamePathWidget->setLabel(dir.getPath()); + draw(); + } + draw(); + break; + } + + // Change path for extra game data (eg, using sword cutscenes when playing via CD) + case kCmdExtraBrowser: + { + BrowserDialog browser(_("Select additional game directory"), true); + if (browser.runModal() > 0) { + // User made his choice... + Common::FSNode dir(browser.getResult()); + _extraPathWidget->setLabel(dir.getPath()); + draw(); + } + draw(); + break; + } + // Change path for stored save game (perm and temp) data + case kCmdSaveBrowser: + { + BrowserDialog browser(_("Select directory for saved games"), true); + if (browser.runModal() > 0) { + // User made his choice... + Common::FSNode dir(browser.getResult()); + _savePathWidget->setLabel(dir.getPath()); +#ifdef USE_LIBCURL + MessageDialog warningMessage(_("Saves sync feature doesn't work with non-default directories. If you want your saves to sync, use default directory.")); + warningMessage.runModal(); +#endif + draw(); + } + draw(); + break; + } + + case kCmdExtraPathClear: + _extraPathWidget->setLabel(_c("None", "path")); + break; + + case kCmdSavePathClear: + _savePathWidget->setLabel(_("Default")); + break; + + case kOKCmd: + { + // Write back changes made to config object + String newDomain(_domainWidget->getEditString()); + if (newDomain != _domain) { + if (newDomain.empty() + || newDomain.hasPrefix("_") + || newDomain == ConfigManager::kApplicationDomain + || ConfMan.hasGameDomain(newDomain)) { + MessageDialog alert(_("This game ID is already taken. Please choose another one.")); + alert.runModal(); + return; + } + ConfMan.renameGameDomain(_domain, newDomain); + _domain = newDomain; + } + } + // FALL THROUGH to default case + default: + OptionsDialog::handleCommand(sender, cmd, data); + } +} + +} // End of namespace GUI diff --git a/gui/editgamedialog.h b/gui/editgamedialog.h new file mode 100644 index 0000000000..0be6c1650e --- /dev/null +++ b/gui/editgamedialog.h @@ -0,0 +1,97 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef GUI_EDITGAMEDIALOG_H +#define GUI_EDITGAMEDIALOG_H + +#include "engines/game.h" +#include "gui/dialog.h" +#include "gui/options.h" + +namespace GUI { + +class BrowserDialog; +class CommandSender; +class DomainEditTextWidget; +class ListWidget; +class ButtonWidget; +class PicButtonWidget; +class GraphicsWidget; +class StaticTextWidget; +class EditTextWidget; +class SaveLoadChooser; + +Common::String addGameToConf(const GameDescriptor &result); + +/* +* A dialog that allows the user to edit a config game entry. +* TODO: add widgets for some/all of the following +* - Maybe scaler/graphics mode. But there are two problems: +* 1) Different backends can have different scalers with different names, +* so we first have to add a way to query those... no Ender, I don't +* think a bitmasked property() value is nice for this, because we would +* have to add to the bitmask values whenever a backends adds a new scaler). +* 2) At the time the launcher is running, the GFX backend is already setup. +* So when a game is run via the launcher, the custom scaler setting for it won't be +* used. So we'd also have to add an API to change the scaler during runtime +* (the SDL backend can already do that based on user input, but there is no API +* to achieve it) +* If the APIs for 1&2 are in place, we can think about adding this to the Edit&Option dialogs +*/ + +class EditGameDialog : public OptionsDialog { + typedef Common::String String; + typedef Common::Array<Common::String> StringArray; +public: + EditGameDialog(const String &domain, const String &desc); + + void open(); + void close(); + virtual void handleCommand(CommandSender *sender, uint32 cmd, uint32 data); + +protected: + EditTextWidget *_descriptionWidget; + DomainEditTextWidget *_domainWidget; + + StaticTextWidget *_gamePathWidget; + StaticTextWidget *_extraPathWidget; + StaticTextWidget *_savePathWidget; + ButtonWidget *_extraPathClearButton; + ButtonWidget *_savePathClearButton; + + StaticTextWidget *_langPopUpDesc; + PopUpWidget *_langPopUp; + StaticTextWidget *_platformPopUpDesc; + PopUpWidget *_platformPopUp; + + CheckboxWidget *_globalGraphicsOverride; + CheckboxWidget *_globalAudioOverride; + CheckboxWidget *_globalMIDIOverride; + CheckboxWidget *_globalMT32Override; + CheckboxWidget *_globalVolumeOverride; + + ExtraGuiOptions _engineOptions; +}; + +} // End of namespace GUI + +#endif diff --git a/gui/gui-manager.cpp b/gui/gui-manager.cpp index 9acd9434ff..9d68d76c9c 100644 --- a/gui/gui-manager.cpp +++ b/gui/gui-manager.cpp @@ -216,7 +216,7 @@ void GuiManager::redraw() { // Tanoku: Do not apply shading more than once when opening many dialogs // on top of each other. Screen ends up being too dark and it's a // performance hog. - if (_redrawStatus == kRedrawOpenDialog && _dialogStack.size() > 2) + if (_redrawStatus == kRedrawOpenDialog && _dialogStack.size() > 3) shading = ThemeEngine::kShadingNone; switch (_redrawStatus) { diff --git a/gui/launcher.cpp b/gui/launcher.cpp index 9a3300b11f..50f060de65 100644 --- a/gui/launcher.cpp +++ b/gui/launcher.cpp @@ -33,6 +33,7 @@ #include "gui/about.h" #include "gui/browser.h" #include "gui/chooser.h" +#include "gui/editgamedialog.h" #include "gui/launcher.h" #include "gui/massadd.h" #include "gui/message.h" @@ -51,6 +52,9 @@ #include "gui/ThemeEval.h" #include "graphics/cursorman.h" +#ifdef USE_LIBCURL +#include "backends/cloud/cloudmanager.h" +#endif using Common::ConfigManager; @@ -84,521 +88,6 @@ enum { kCmdSavePathClear = 'PSAC' }; -/* - * TODO: Clean up this ugly design: we subclass EditTextWidget to perform - * input validation. It would be much more elegant to use a decorator pattern, - * or a validation callback, or something like that. - */ -class DomainEditTextWidget : public EditTextWidget { -public: - DomainEditTextWidget(GuiObject *boss, const String &name, const String &text, const char *tooltip = 0) - : EditTextWidget(boss, name, text, tooltip) { - } - -protected: - bool tryInsertChar(byte c, int pos) { - if (Common::isAlnum(c) || c == '-' || c == '_') { - _editString.insertChar(c, pos); - return true; - } - return false; - } -}; - -/* - * A dialog that allows the user to edit a config game entry. - * TODO: add widgets for some/all of the following - * - Maybe scaler/graphics mode. But there are two problems: - * 1) Different backends can have different scalers with different names, - * so we first have to add a way to query those... no Ender, I don't - * think a bitmasked property() value is nice for this, because we would - * have to add to the bitmask values whenever a backends adds a new scaler). - * 2) At the time the launcher is running, the GFX backend is already setup. - * So when a game is run via the launcher, the custom scaler setting for it won't be - * used. So we'd also have to add an API to change the scaler during runtime - * (the SDL backend can already do that based on user input, but there is no API - * to achieve it) - * If the APIs for 1&2 are in place, we can think about adding this to the Edit&Option dialogs - */ - -class EditGameDialog : public OptionsDialog { - typedef Common::String String; - typedef Common::Array<Common::String> StringArray; -public: - EditGameDialog(const String &domain, const String &desc); - - void open(); - void close(); - virtual void handleCommand(CommandSender *sender, uint32 cmd, uint32 data); - -protected: - EditTextWidget *_descriptionWidget; - DomainEditTextWidget *_domainWidget; - - StaticTextWidget *_gamePathWidget; - StaticTextWidget *_extraPathWidget; - StaticTextWidget *_savePathWidget; - ButtonWidget *_extraPathClearButton; - ButtonWidget *_savePathClearButton; - - StaticTextWidget *_langPopUpDesc; - PopUpWidget *_langPopUp; - StaticTextWidget *_platformPopUpDesc; - PopUpWidget *_platformPopUp; - - CheckboxWidget *_globalGraphicsOverride; - CheckboxWidget *_globalAudioOverride; - CheckboxWidget *_globalMIDIOverride; - CheckboxWidget *_globalMT32Override; - CheckboxWidget *_globalVolumeOverride; - - ExtraGuiOptions _engineOptions; -}; - -EditGameDialog::EditGameDialog(const String &domain, const String &desc) - : OptionsDialog(domain, "GameOptions") { - // Retrieve all game specific options. - const EnginePlugin *plugin = 0; - // To allow for game domains without a gameid. - // TODO: Is it intentional that this is still supported? - String gameId(ConfMan.get("gameid", domain)); - if (gameId.empty()) - gameId = domain; - // Retrieve the plugin, since we need to access the engine's MetaEngine - // implementation. - EngineMan.findGame(gameId, &plugin); - if (plugin) { - _engineOptions = (*plugin)->getExtraGuiOptions(domain); - } else { - warning("Plugin for target \"%s\" not found! Game specific settings might be missing", domain.c_str()); - } - - // GAME: Path to game data (r/o), extra data (r/o), and save data (r/w) - String gamePath(ConfMan.get("path", _domain)); - String extraPath(ConfMan.get("extrapath", _domain)); - String savePath(ConfMan.get("savepath", _domain)); - - // GAME: Determine the description string - String description(ConfMan.get("description", domain)); - if (description.empty() && !desc.empty()) { - description = desc; - } - - // GUI: Add tab widget - TabWidget *tab = new TabWidget(this, "GameOptions.TabWidget"); - - // - // 1) The game tab - // - tab->addTab(_("Game")); - - // GUI: Label & edit widget for the game ID - if (g_system->getOverlayWidth() > 320) - new StaticTextWidget(tab, "GameOptions_Game.Id", _("ID:"), _("Short game identifier used for referring to saved games and running the game from the command line")); - else - new StaticTextWidget(tab, "GameOptions_Game.Id", _c("ID:", "lowres"), _("Short game identifier used for referring to saved games and running the game from the command line")); - _domainWidget = new DomainEditTextWidget(tab, "GameOptions_Game.Domain", _domain, _("Short game identifier used for referring to saved games and running the game from the command line")); - - // GUI: Label & edit widget for the description - if (g_system->getOverlayWidth() > 320) - new StaticTextWidget(tab, "GameOptions_Game.Name", _("Name:"), _("Full title of the game")); - else - new StaticTextWidget(tab, "GameOptions_Game.Name", _c("Name:", "lowres"), _("Full title of the game")); - _descriptionWidget = new EditTextWidget(tab, "GameOptions_Game.Desc", description, _("Full title of the game")); - - // Language popup - _langPopUpDesc = new StaticTextWidget(tab, "GameOptions_Game.LangPopupDesc", _("Language:"), _("Language of the game. This will not turn your Spanish game version into English")); - _langPopUp = new PopUpWidget(tab, "GameOptions_Game.LangPopup", _("Language of the game. This will not turn your Spanish game version into English")); - _langPopUp->appendEntry(_("<default>"), (uint32)Common::UNK_LANG); - _langPopUp->appendEntry("", (uint32)Common::UNK_LANG); - const Common::LanguageDescription *l = Common::g_languages; - for (; l->code; ++l) { - if (checkGameGUIOptionLanguage(l->id, _guioptionsString)) - _langPopUp->appendEntry(l->description, l->id); - } - - // Platform popup - if (g_system->getOverlayWidth() > 320) - _platformPopUpDesc = new StaticTextWidget(tab, "GameOptions_Game.PlatformPopupDesc", _("Platform:"), _("Platform the game was originally designed for")); - else - _platformPopUpDesc = new StaticTextWidget(tab, "GameOptions_Game.PlatformPopupDesc", _c("Platform:", "lowres"), _("Platform the game was originally designed for")); - _platformPopUp = new PopUpWidget(tab, "GameOptions_Game.PlatformPopup", _("Platform the game was originally designed for")); - _platformPopUp->appendEntry(_("<default>")); - _platformPopUp->appendEntry(""); - const Common::PlatformDescription *p = Common::g_platforms; - for (; p->code; ++p) { - _platformPopUp->appendEntry(p->description, p->id); - } - - // - // 2) The engine tab (shown only if there are custom engine options) - // - if (_engineOptions.size() > 0) { - tab->addTab(_("Engine")); - - addEngineControls(tab, "GameOptions_Engine.", _engineOptions); - } - - // - // 3) The graphics tab - // - _graphicsTabId = tab->addTab(g_system->getOverlayWidth() > 320 ? _("Graphics") : _("GFX")); - - if (g_system->getOverlayWidth() > 320) - _globalGraphicsOverride = new CheckboxWidget(tab, "GameOptions_Graphics.EnableTabCheckbox", _("Override global graphic settings"), 0, kCmdGlobalGraphicsOverride); - else - _globalGraphicsOverride = new CheckboxWidget(tab, "GameOptions_Graphics.EnableTabCheckbox", _c("Override global graphic settings", "lowres"), 0, kCmdGlobalGraphicsOverride); - - addGraphicControls(tab, "GameOptions_Graphics."); - - // - // 4) The audio tab - // - tab->addTab(_("Audio")); - - if (g_system->getOverlayWidth() > 320) - _globalAudioOverride = new CheckboxWidget(tab, "GameOptions_Audio.EnableTabCheckbox", _("Override global audio settings"), 0, kCmdGlobalAudioOverride); - else - _globalAudioOverride = new CheckboxWidget(tab, "GameOptions_Audio.EnableTabCheckbox", _c("Override global audio settings", "lowres"), 0, kCmdGlobalAudioOverride); - - addAudioControls(tab, "GameOptions_Audio."); - addSubtitleControls(tab, "GameOptions_Audio."); - - // - // 5) The volume tab - // - if (g_system->getOverlayWidth() > 320) - tab->addTab(_("Volume")); - else - tab->addTab(_c("Volume", "lowres")); - - if (g_system->getOverlayWidth() > 320) - _globalVolumeOverride = new CheckboxWidget(tab, "GameOptions_Volume.EnableTabCheckbox", _("Override global volume settings"), 0, kCmdGlobalVolumeOverride); - else - _globalVolumeOverride = new CheckboxWidget(tab, "GameOptions_Volume.EnableTabCheckbox", _c("Override global volume settings", "lowres"), 0, kCmdGlobalVolumeOverride); - - addVolumeControls(tab, "GameOptions_Volume."); - - // - // 6) The MIDI tab - // - _globalMIDIOverride = NULL; - if (!_guioptions.contains(GUIO_NOMIDI)) { - tab->addTab(_("MIDI")); - - if (g_system->getOverlayWidth() > 320) - _globalMIDIOverride = new CheckboxWidget(tab, "GameOptions_MIDI.EnableTabCheckbox", _("Override global MIDI settings"), 0, kCmdGlobalMIDIOverride); - else - _globalMIDIOverride = new CheckboxWidget(tab, "GameOptions_MIDI.EnableTabCheckbox", _c("Override global MIDI settings", "lowres"), 0, kCmdGlobalMIDIOverride); - - addMIDIControls(tab, "GameOptions_MIDI."); - } - - // - // 7) The MT-32 tab - // - _globalMT32Override = NULL; - if (!_guioptions.contains(GUIO_NOMIDI)) { - tab->addTab(_("MT-32")); - - if (g_system->getOverlayWidth() > 320) - _globalMT32Override = new CheckboxWidget(tab, "GameOptions_MT32.EnableTabCheckbox", _("Override global MT-32 settings"), 0, kCmdGlobalMT32Override); - else - _globalMT32Override = new CheckboxWidget(tab, "GameOptions_MT32.EnableTabCheckbox", _c("Override global MT-32 settings", "lowres"), 0, kCmdGlobalMT32Override); - - addMT32Controls(tab, "GameOptions_MT32."); - } - - // - // 8) The Paths tab - // - if (g_system->getOverlayWidth() > 320) - tab->addTab(_("Paths")); - else - tab->addTab(_c("Paths", "lowres")); - - // These buttons have to be extra wide, or the text will be truncated - // in the small version of the GUI. - - // GUI: Button + Label for the game path - if (g_system->getOverlayWidth() > 320) - new ButtonWidget(tab, "GameOptions_Paths.Gamepath", _("Game Path:"), 0, kCmdGameBrowser); - else - new ButtonWidget(tab, "GameOptions_Paths.Gamepath", _c("Game Path:", "lowres"), 0, kCmdGameBrowser); - _gamePathWidget = new StaticTextWidget(tab, "GameOptions_Paths.GamepathText", gamePath); - - // GUI: Button + Label for the additional path - if (g_system->getOverlayWidth() > 320) - new ButtonWidget(tab, "GameOptions_Paths.Extrapath", _("Extra Path:"), _("Specifies path to additional data used by the game"), kCmdExtraBrowser); - else - new ButtonWidget(tab, "GameOptions_Paths.Extrapath", _c("Extra Path:", "lowres"), _("Specifies path to additional data used by the game"), kCmdExtraBrowser); - _extraPathWidget = new StaticTextWidget(tab, "GameOptions_Paths.ExtrapathText", extraPath, _("Specifies path to additional data used by the game")); - - _extraPathClearButton = addClearButton(tab, "GameOptions_Paths.ExtraPathClearButton", kCmdExtraPathClear); - - // GUI: Button + Label for the save path - if (g_system->getOverlayWidth() > 320) - new ButtonWidget(tab, "GameOptions_Paths.Savepath", _("Save Path:"), _("Specifies where your saved games are put"), kCmdSaveBrowser); - else - new ButtonWidget(tab, "GameOptions_Paths.Savepath", _c("Save Path:", "lowres"), _("Specifies where your saved games are put"), kCmdSaveBrowser); - _savePathWidget = new StaticTextWidget(tab, "GameOptions_Paths.SavepathText", savePath, _("Specifies where your saved games are put")); - - _savePathClearButton = addClearButton(tab, "GameOptions_Paths.SavePathClearButton", kCmdSavePathClear); - - // Activate the first tab - tab->setActiveTab(0); - _tabWidget = tab; - - // Add OK & Cancel buttons - new ButtonWidget(this, "GameOptions.Cancel", _("Cancel"), 0, kCloseCmd); - new ButtonWidget(this, "GameOptions.Ok", _("OK"), 0, kOKCmd); -} - -void EditGameDialog::open() { - OptionsDialog::open(); - - String extraPath(ConfMan.get("extrapath", _domain)); - if (extraPath.empty() || !ConfMan.hasKey("extrapath", _domain)) { - _extraPathWidget->setLabel(_c("None", "path")); - } - - String savePath(ConfMan.get("savepath", _domain)); - if (savePath.empty() || !ConfMan.hasKey("savepath", _domain)) { - _savePathWidget->setLabel(_("Default")); - } - - int sel, i; - bool e; - - // En-/disable dialog items depending on whether overrides are active or not. - - e = ConfMan.hasKey("gfx_mode", _domain) || - ConfMan.hasKey("render_mode", _domain) || - ConfMan.hasKey("fullscreen", _domain) || - ConfMan.hasKey("aspect_ratio", _domain); - _globalGraphicsOverride->setState(e); - - e = ConfMan.hasKey("music_driver", _domain) || - ConfMan.hasKey("output_rate", _domain) || - ConfMan.hasKey("opl_driver", _domain) || - ConfMan.hasKey("subtitles", _domain) || - ConfMan.hasKey("talkspeed", _domain); - _globalAudioOverride->setState(e); - - e = ConfMan.hasKey("music_volume", _domain) || - ConfMan.hasKey("sfx_volume", _domain) || - ConfMan.hasKey("speech_volume", _domain); - _globalVolumeOverride->setState(e); - - if (!_guioptions.contains(GUIO_NOMIDI)) { - e = ConfMan.hasKey("soundfont", _domain) || - ConfMan.hasKey("multi_midi", _domain) || - ConfMan.hasKey("midi_gain", _domain); - _globalMIDIOverride->setState(e); - } - - if (!_guioptions.contains(GUIO_NOMIDI)) { - e = ConfMan.hasKey("native_mt32", _domain) || - ConfMan.hasKey("enable_gs", _domain); - _globalMT32Override->setState(e); - } - - // TODO: game path - - const Common::Language lang = Common::parseLanguage(ConfMan.get("language", _domain)); - - if (ConfMan.hasKey("language", _domain)) { - _langPopUp->setSelectedTag(lang); - } else { - _langPopUp->setSelectedTag((uint32)Common::UNK_LANG); - } - - if (_langPopUp->numEntries() <= 3) { // If only one language is avaliable - _langPopUpDesc->setEnabled(false); - _langPopUp->setEnabled(false); - } - - // Set the state of engine-specific checkboxes - for (uint j = 0; j < _engineOptions.size(); ++j) { - // The default values for engine-specific checkboxes are not set when - // ScummVM starts, as this would require us to load and poll all of the - // engine plugins on startup. Thus, we set the state of each custom - // option checkbox to what is specified by the engine plugin, and - // update it only if a value has been set in the configuration of the - // currently selected game. - bool isChecked = _engineOptions[j].defaultState; - if (ConfMan.hasKey(_engineOptions[j].configOption, _domain)) - isChecked = ConfMan.getBool(_engineOptions[j].configOption, _domain); - _engineCheckboxes[j]->setState(isChecked); - } - - const Common::PlatformDescription *p = Common::g_platforms; - const Common::Platform platform = Common::parsePlatform(ConfMan.get("platform", _domain)); - sel = 0; - for (i = 0; p->code; ++p, ++i) { - if (platform == p->id) - sel = i + 2; - } - _platformPopUp->setSelected(sel); -} - - -void EditGameDialog::close() { - if (getResult()) { - ConfMan.set("description", _descriptionWidget->getEditString(), _domain); - - Common::Language lang = (Common::Language)_langPopUp->getSelectedTag(); - if (lang < 0) - ConfMan.removeKey("language", _domain); - else - ConfMan.set("language", Common::getLanguageCode(lang), _domain); - - String gamePath(_gamePathWidget->getLabel()); - if (!gamePath.empty()) - ConfMan.set("path", gamePath, _domain); - - String extraPath(_extraPathWidget->getLabel()); - if (!extraPath.empty() && (extraPath != _c("None", "path"))) - ConfMan.set("extrapath", extraPath, _domain); - else - ConfMan.removeKey("extrapath", _domain); - - String savePath(_savePathWidget->getLabel()); - if (!savePath.empty() && (savePath != _("Default"))) - ConfMan.set("savepath", savePath, _domain); - else - ConfMan.removeKey("savepath", _domain); - - Common::Platform platform = (Common::Platform)_platformPopUp->getSelectedTag(); - if (platform < 0) - ConfMan.removeKey("platform", _domain); - else - ConfMan.set("platform", Common::getPlatformCode(platform), _domain); - - // Set the state of engine-specific checkboxes - for (uint i = 0; i < _engineOptions.size(); i++) { - ConfMan.setBool(_engineOptions[i].configOption, _engineCheckboxes[i]->getState(), _domain); - } - } - OptionsDialog::close(); -} - -void EditGameDialog::handleCommand(CommandSender *sender, uint32 cmd, uint32 data) { - switch (cmd) { - case kCmdGlobalGraphicsOverride: - setGraphicSettingsState(data != 0); - draw(); - break; - case kCmdGlobalAudioOverride: - setAudioSettingsState(data != 0); - setSubtitleSettingsState(data != 0); - if (_globalVolumeOverride == NULL) - setVolumeSettingsState(data != 0); - draw(); - break; - case kCmdGlobalMIDIOverride: - setMIDISettingsState(data != 0); - draw(); - break; - case kCmdGlobalMT32Override: - setMT32SettingsState(data != 0); - draw(); - break; - case kCmdGlobalVolumeOverride: - setVolumeSettingsState(data != 0); - draw(); - break; - case kCmdChooseSoundFontCmd: { - BrowserDialog browser(_("Select SoundFont"), false); - - if (browser.runModal() > 0) { - // User made this choice... - Common::FSNode file(browser.getResult()); - _soundFont->setLabel(file.getPath()); - - if (!file.getPath().empty() && (file.getPath() != _c("None", "path"))) - _soundFontClearButton->setEnabled(true); - else - _soundFontClearButton->setEnabled(false); - - draw(); - } - break; - } - - // Change path for the game - case kCmdGameBrowser: { - BrowserDialog browser(_("Select directory with game data"), true); - if (browser.runModal() > 0) { - // User made his choice... - Common::FSNode dir(browser.getResult()); - - // TODO: Verify the game can be found in the new directory... Best - // done with optional specific gameid to pluginmgr detectgames? - // FSList files = dir.listDir(FSNode::kListFilesOnly); - - _gamePathWidget->setLabel(dir.getPath()); - draw(); - } - draw(); - break; - } - - // Change path for extra game data (eg, using sword cutscenes when playing via CD) - case kCmdExtraBrowser: { - BrowserDialog browser(_("Select additional game directory"), true); - if (browser.runModal() > 0) { - // User made his choice... - Common::FSNode dir(browser.getResult()); - _extraPathWidget->setLabel(dir.getPath()); - draw(); - } - draw(); - break; - } - // Change path for stored save game (perm and temp) data - case kCmdSaveBrowser: { - BrowserDialog browser(_("Select directory for saved games"), true); - if (browser.runModal() > 0) { - // User made his choice... - Common::FSNode dir(browser.getResult()); - _savePathWidget->setLabel(dir.getPath()); - draw(); - } - draw(); - break; - } - - case kCmdExtraPathClear: - _extraPathWidget->setLabel(_c("None", "path")); - break; - - case kCmdSavePathClear: - _savePathWidget->setLabel(_("Default")); - break; - - case kOKCmd: { - // Write back changes made to config object - String newDomain(_domainWidget->getEditString()); - if (newDomain != _domain) { - if (newDomain.empty() - || newDomain.hasPrefix("_") - || newDomain == ConfigManager::kApplicationDomain - || ConfMan.hasGameDomain(newDomain)) { - MessageDialog alert(_("This game ID is already taken. Please choose another one.")); - alert.runModal(); - return; - } - ConfMan.renameGameDomain(_domain, newDomain); - _domain = newDomain; - } - } - // FALL THROUGH to default case - default: - OptionsDialog::handleCommand(sender, cmd, data); - } -} - #pragma mark - LauncherDialog::LauncherDialog() @@ -839,65 +328,25 @@ void LauncherDialog::addGame() { if (_browser->runModal() > 0) { // User made his choice... - Common::FSNode dir(_browser->getResult()); - Common::FSList files; - if (!dir.getChildren(files, Common::FSNode::kListAll)) { - MessageDialog alert(_("ScummVM couldn't open the specified directory!")); - alert.runModal(); - return; - } - - // ...so let's determine a list of candidates, games that - // could be contained in the specified directory. - GameList candidates(EngineMan.detectGames(files)); - - int idx; - if (candidates.empty()) { - // No game was found in the specified directory - MessageDialog alert(_("ScummVM could not find any game in the specified directory!")); - alert.runModal(); - idx = -1; - - looping = true; - } else if (candidates.size() == 1) { - // Exact match - idx = 0; - } else { - // Display the candidates to the user and let her/him pick one - StringArray list; - for (idx = 0; idx < (int)candidates.size(); idx++) - list.push_back(candidates[idx].description()); - - ChooserDialog dialog(_("Pick the game:")); - dialog.setList(list); - idx = dialog.runModal(); - } - if (0 <= idx && idx < (int)candidates.size()) { - GameDescriptor result = candidates[idx]; - - // TODO: Change the detectors to set "path" ! - result["path"] = dir.getPath(); - - Common::String domain = addGameToConf(result); - - // Display edit dialog for the new entry - EditGameDialog editDialog(domain, result.description()); - if (editDialog.runModal() > 0) { - // User pressed OK, so make changes permanent - - // Write config to disk - ConfMan.flushToDisk(); - - // Update the ListWidget, select the new item, and force a redraw - updateListing(); - selectTarget(editDialog.getDomain()); - draw(); +#ifdef USE_LIBCURL + String selectedDirectory = _browser->getResult().getPath(); + String bannedDirectory = CloudMan.getDownloadLocalDirectory(); + if (selectedDirectory.size() && selectedDirectory.lastChar() != '/' && selectedDirectory.lastChar() != '\\') + selectedDirectory += '/'; + if (bannedDirectory.size() && bannedDirectory.lastChar() != '/' && bannedDirectory.lastChar() != '\\') { + if (selectedDirectory.size()) { + bannedDirectory += selectedDirectory.lastChar(); } else { - // User aborted, remove the the new domain again - ConfMan.removeGameDomain(domain); + bannedDirectory += '/'; } - } + if (selectedDirectory.equalsIgnoreCase(bannedDirectory)) { + MessageDialog alert(_("This directory cannot be used yet, it is being downloaded into!")); + alert.runModal(); + return; + } +#endif + looping = !doGameDetection(_browser->getResult().getPath()); } } while (looping); } @@ -1076,6 +525,81 @@ void LauncherDialog::handleKeyUp(Common::KeyState state) { updateButtons(); } +bool LauncherDialog::doGameDetection(const Common::String &path) { + // Allow user to add a new game to the list. + // 2) try to auto detect which game is in the directory, if we cannot + // determine it uniquely present a list of candidates to the user + // to pick from + // 3) Display the 'Edit' dialog for that item, letting the user specify + // an alternate description (to distinguish multiple versions of the + // game, e.g. 'Monkey German' and 'Monkey English') and set default + // options for that game + // 4) If no game is found in the specified directory, return to the + // dialog. + + // User made his choice... + Common::FSNode dir(path); + Common::FSList files; + if (!dir.getChildren(files, Common::FSNode::kListAll)) { + MessageDialog alert(_("ScummVM couldn't open the specified directory!")); + alert.runModal(); + return true; + } + + // ...so let's determine a list of candidates, games that + // could be contained in the specified directory. + GameList candidates(EngineMan.detectGames(files)); + + int idx; + if (candidates.empty()) { + // No game was found in the specified directory + MessageDialog alert(_("ScummVM could not find any game in the specified directory!")); + alert.runModal(); + idx = -1; + return false; + } else if (candidates.size() == 1) { + // Exact match + idx = 0; + } else { + // Display the candidates to the user and let her/him pick one + StringArray list; + for (idx = 0; idx < (int)candidates.size(); idx++) + list.push_back(candidates[idx].description()); + + ChooserDialog dialog(_("Pick the game:")); + dialog.setList(list); + idx = dialog.runModal(); + } + if (0 <= idx && idx < (int)candidates.size()) { + GameDescriptor result = candidates[idx]; + + // TODO: Change the detectors to set "path" ! + result["path"] = dir.getPath(); + + Common::String domain = addGameToConf(result); + + // Display edit dialog for the new entry + EditGameDialog editDialog(domain, result.description()); + if (editDialog.runModal() > 0) { + // User pressed OK, so make changes permanent + + // Write config to disk + ConfMan.flushToDisk(); + + // Update the ListWidget, select the new item, and force a redraw + updateListing(); + selectTarget(editDialog.getDomain()); + draw(); + } else { + // User aborted, remove the the new domain again + ConfMan.removeGameDomain(domain); + } + + } + + return true; +} + void LauncherDialog::handleCommand(CommandSender *sender, uint32 cmd, uint32 data) { int item = _list->getSelected(); @@ -1093,7 +617,7 @@ void LauncherDialog::handleCommand(CommandSender *sender, uint32 cmd, uint32 dat loadGameButtonPressed(item); break; case kOptionsCmd: { - GlobalOptionsDialog options; + GlobalOptionsDialog options(this); options.runModal(); } break; diff --git a/gui/launcher.h b/gui/launcher.h index e9c76a5320..58f1c930ed 100644 --- a/gui/launcher.h +++ b/gui/launcher.h @@ -51,7 +51,7 @@ public: virtual void handleKeyDown(Common::KeyState state); virtual void handleKeyUp(Common::KeyState state); - + bool doGameDetection(const Common::String &path); protected: EditTextWidget *_searchWidget; ListWidget *_list; diff --git a/gui/module.mk b/gui/module.mk index 6cbc63d24d..54818c264e 100644 --- a/gui/module.mk +++ b/gui/module.mk @@ -6,6 +6,7 @@ MODULE_OBJS := \ console.o \ debugger.o \ dialog.o \ + editgamedialog.o \ error.o \ EventRecorder.o \ filebrowser-dialog.o \ @@ -24,6 +25,9 @@ MODULE_OBJS := \ ThemeLayout.o \ ThemeParser.o \ Tooltip.o \ + animation/Animation.o \ + animation/RepeatAnimationWrapper.o \ + animation/SequenceAnimationComposite.o \ widget.o \ widgets/editable.o \ widgets/edittext.o \ @@ -54,6 +58,13 @@ MODULE_OBJS += \ endif endif +ifdef USE_LIBCURL +MODULE_OBJS += \ + downloaddialog.o \ + remotebrowser.o \ + storagewizarddialog.o +endif + ifdef ENABLE_EVENTRECORDER MODULE_OBJS += \ editrecorddialog.o \ diff --git a/gui/object.h b/gui/object.h index 219bf77f69..1541c35aa8 100644 --- a/gui/object.h +++ b/gui/object.h @@ -76,6 +76,8 @@ public: virtual void setTextDrawableArea(const Common::Rect &r) { _textDrawableArea = r; } + virtual int16 getRelX() const { return _x; } + virtual int16 getRelY() const { return _y; } virtual int16 getAbsX() const { return _x; } virtual int16 getAbsY() const { return _y; } virtual int16 getChildX() const { return getAbsX(); } @@ -91,6 +93,10 @@ public: virtual void removeWidget(Widget *widget); + virtual bool isPointIn(int x, int y) { + return (x >= _x && x < (_x + _w) && (y >= _y) && (y < _y + _h)); + } + protected: virtual void releaseFocus() = 0; }; diff --git a/gui/options.cpp b/gui/options.cpp index e410971818..3f2674b016 100644 --- a/gui/options.cpp +++ b/gui/options.cpp @@ -42,6 +42,18 @@ #include "audio/musicplugin.h" #include "audio/mixer.h" #include "audio/fmopl.h" +#include "widgets/scrollcontainer.h" +#include "widgets/edittext.h" + +#ifdef USE_LIBCURL +#include "backends/cloud/cloudmanager.h" +#include "gui/downloaddialog.h" +#include "gui/storagewizarddialog.h" +#endif + +#ifdef USE_SDL_NET +#include "backends/networking/sdl_net/localwebserver.h" +#endif namespace GUI { @@ -84,6 +96,19 @@ enum { }; #endif +#ifdef USE_CLOUD +enum { + kConfigureStorageCmd = 'cfst', + kRefreshStorageCmd = 'rfst', + kDownloadStorageCmd = 'dlst', + kRunServerCmd = 'rnsv', + kCloudTabContainerReflowCmd = 'ctcr', + kServerPortClearCmd = 'spcl', + kChooseRootDirCmd = 'chrp', + kRootPathClearCmd = 'clrp' +}; +#endif + static const char *savePeriodLabels[] = { _s("Never"), _s("every 5 mins"), _s("every 10 mins"), _s("every 15 mins"), _s("every 30 mins"), 0 }; static const int savePeriodValues[] = { 0, 5 * 60, 10 * 60, 15 * 60, 30 * 60, -1 }; static const char *outputRateLabels[] = { _s("<default>"), _s("8 kHz"), _s("11 kHz"), _s("22 kHz"), _s("44 kHz"), _s("48 kHz"), 0 }; @@ -1078,8 +1103,8 @@ void OptionsDialog::reflowLayout() { #pragma mark - -GlobalOptionsDialog::GlobalOptionsDialog() - : OptionsDialog(Common::ConfigManager::kApplicationDomain, "GlobalOptions") { +GlobalOptionsDialog::GlobalOptionsDialog(LauncherDialog *launcher) + : OptionsDialog(Common::ConfigManager::kApplicationDomain, "GlobalOptions"), _launcher(launcher) { // The tab widget TabWidget *tab = new TabWidget(this, "GlobalOptions.TabWidget"); @@ -1251,6 +1276,76 @@ GlobalOptionsDialog::GlobalOptionsDialog() new ButtonWidget(tab, "GlobalOptions_Misc.UpdatesCheckManuallyButton", _("Check now"), 0, kUpdatesCheckCmd); #endif +#ifdef USE_CLOUD + // + // 7) The cloud tab + // + if (g_system->getOverlayWidth() > 320) + tab->addTab(_("Cloud")); + else + tab->addTab(_c("Cloud", "lowres")); + + ScrollContainerWidget *container = new ScrollContainerWidget(tab, "GlobalOptions_Cloud.Container", kCloudTabContainerReflowCmd); + container->setTarget(this); + +#ifdef USE_LIBCURL + _selectedStorageIndex = CloudMan.getStorageIndex(); +#else + _selectedStorageIndex = 0; +#endif + + _storagePopUpDesc = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StoragePopupDesc", _("Storage:"), _("Active cloud storage")); + _storagePopUp = new PopUpWidget(container, "GlobalOptions_Cloud_Container.StoragePopup"); +#ifdef USE_LIBCURL + Common::StringArray list = CloudMan.listStorages(); + for (uint32 i = 0; i < list.size(); ++i) + _storagePopUp->appendEntry(list[i], i); +#else + _storagePopUp->appendEntry(_("<none>"), 0); +#endif + _storagePopUp->setSelected(_selectedStorageIndex); + + _storageUsernameDesc = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageUsernameDesc", _("Username:"), _("Username used by this storage")); + _storageUsername = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageUsernameLabel", "<none>"); + + _storageUsedSpaceDesc = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageUsedSpaceDesc", _("Used space:"), _("Space used by ScummVM's saves on this storage")); + _storageUsedSpace = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageUsedSpaceLabel", "0 bytes"); + + _storageLastSyncDesc = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageLastSyncDesc", _("Last sync time:"), _("When this storage did saves sync last time")); + _storageLastSync = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.StorageLastSyncLabel", "<never>"); + + _storageConnectButton = new ButtonWidget(container, "GlobalOptions_Cloud_Container.ConnectButton", _("Connect"), _("Open wizard dialog to connect your cloud storage account"), kConfigureStorageCmd); + _storageRefreshButton = new ButtonWidget(container, "GlobalOptions_Cloud_Container.RefreshButton", _("Refresh"), _("Refresh current cloud storage information (username and usage)"), kRefreshStorageCmd); + _storageDownloadButton = new ButtonWidget(container, "GlobalOptions_Cloud_Container.DownloadButton", _("Downloads"), _("Open downloads manager dialog"), kDownloadStorageCmd); + + _runServerButton = new ButtonWidget(container, "GlobalOptions_Cloud_Container.RunServerButton", _("Run server"), _("Run local webserver"), kRunServerCmd); + _serverInfoLabel = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.ServerInfoLabel", _("Not running")); + + // Root path + if (g_system->getOverlayWidth() > 320) + _rootPathButton = new ButtonWidget(container, "GlobalOptions_Cloud_Container.RootPathButton", _("/root/ Path:"), _("Specifies where Files Manager can access to"), kChooseRootDirCmd); + else + _rootPathButton = new ButtonWidget(container, "GlobalOptions_Cloud_Container.RootPathButton", _c("/root/ Path:", "lowres"), _("Specifies where Files Manager can access to"), kChooseRootDirCmd); + _rootPath = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.RootPath", "/foo/bar", _("Specifies where Files Manager can access to")); + + _rootPathClearButton = addClearButton(container, "GlobalOptions_Cloud_Container.RootPathClearButton", kRootPathClearCmd); + +#ifdef USE_SDL_NET + uint32 port = Networking::LocalWebserver::getPort(); +#else + uint32 port = 0; // the following widgets are hidden anyway +#endif + _serverPortDesc = new StaticTextWidget(container, "GlobalOptions_Cloud_Container.ServerPortDesc", _("Server's port:"), _("Which port is used by server\nAuth with server is not available with non-default port")); + _serverPort = new EditTextWidget(container, "GlobalOptions_Cloud_Container.ServerPortEditText", Common::String::format("%u", port), 0); + _serverPortClearButton = addClearButton(container, "GlobalOptions_Cloud_Container.ServerPortClearButton", kServerPortClearCmd); + + setupCloudTab(); + _redrawCloudTab = false; +#ifdef USE_SDL_NET + _serverWasRunning = false; +#endif +#endif + // Activate the first tab tab->setActiveTab(0); _tabWidget = tab; @@ -1327,6 +1422,15 @@ void GlobalOptionsDialog::open() { if (mode == ThemeEngine::kGfxDisabled) mode = ThemeEngine::_defaultRendererMode; _rendererPopUp->setSelectedTag(mode); + +#ifdef USE_CLOUD + Common::String rootPath(ConfMan.get("rootpath", "cloud")); + if (rootPath.empty() || !ConfMan.hasKey("rootpath", "cloud")) { + _rootPath->setLabel(_c("None", "path")); + } else { + _rootPath->setLabel(rootPath); + } +#endif } void GlobalOptionsDialog::close() { @@ -1357,6 +1461,14 @@ void GlobalOptionsDialog::close() { ConfMan.removeKey("pluginspath", _domain); #endif +#ifdef USE_CLOUD + Common::String rootPath(_rootPath->getLabel()); + if (!rootPath.empty() && (rootPath != _c("None", "path"))) + ConfMan.set("rootpath", rootPath, "cloud"); + else + ConfMan.removeKey("rootpath", "cloud"); +#endif + ConfMan.setInt("autosave_period", _autosavePeriodPopUp->getSelectedTag(), _domain); GUI::ThemeEngine::GraphicsMode selected = (GUI::ThemeEngine::GraphicsMode)_rendererPopUp->getSelectedTag(); @@ -1402,7 +1514,38 @@ void GlobalOptionsDialog::close() { } #endif +#ifdef USE_LIBCURL + if (CloudMan.getStorageIndex() != _selectedStorageIndex) { + if (!CloudMan.switchStorage(_selectedStorageIndex)) { + bool anotherStorageIsWorking = CloudMan.isWorking(); + Common::String message = _("Failed to change cloud storage!"); + if (anotherStorageIsWorking) { + message += "\n"; + message += _("Current cloud storage is working at the moment."); + } + MessageDialog dialog(message); + dialog.runModal(); + } + } +#endif +#ifdef USE_SDL_NET +#ifdef NETWORKING_LOCALWEBSERVER_ENABLE_PORT_OVERRIDE + // save server's port + uint32 port = Networking::LocalWebserver::getPort(); + if (_serverPort) { + uint64 contents = _serverPort->getEditString().asUint64(); + if (contents != 0) + port = contents; + } + ConfMan.setInt("local_server_port", port); +#endif +#endif } +#ifdef USE_SDL_NET + if (LocalServer.isRunning()) { + LocalServer.stop(); + } +#endif OptionsDialog::close(); } @@ -1456,6 +1599,21 @@ void GlobalOptionsDialog::handleCommand(CommandSender *sender, uint32 cmd, uint3 break; } #endif +#ifdef USE_CLOUD + case kChooseRootDirCmd: { + BrowserDialog browser(_("Select directory for Files Manager /root/"), true); + if (browser.runModal() > 0) { + // User made his choice... + Common::FSNode dir(browser.getResult()); + Common::String path = dir.getPath(); + if (path.empty()) + path = "/"; // absolute root + _rootPath->setLabel(path); + draw(); + } + break; + } +#endif case kThemePathClearCmd: _themePath->setLabel(_c("None", "path")); break; @@ -1465,6 +1623,11 @@ void GlobalOptionsDialog::handleCommand(CommandSender *sender, uint32 cmd, uint3 case kSavePathClearCmd: _savePath->setLabel(_("Default")); break; +#ifdef USE_CLOUD + case kRootPathClearCmd: + _rootPath->setLabel(_c("None", "path")); + break; +#endif case kChooseSoundFontCmd: { BrowserDialog browser(_("Select SoundFont"), false); if (browser.runModal() > 0) { @@ -1481,7 +1644,8 @@ void GlobalOptionsDialog::handleCommand(CommandSender *sender, uint32 cmd, uint3 } break; } - case kChooseThemeCmd: { + case kChooseThemeCmd: + { ThemeBrowser browser; if (browser.runModal() > 0) { // User made his choice... @@ -1513,6 +1677,91 @@ void GlobalOptionsDialog::handleCommand(CommandSender *sender, uint32 cmd, uint3 } break; } +#ifdef USE_CLOUD + case kCloudTabContainerReflowCmd: + setupCloudTab(); + break; +#endif +#ifdef USE_LIBCURL + case kPopUpItemSelectedCmd: + { + //update container's scrollbar + reflowLayout(); + break; + } + case kConfigureStorageCmd: + { +#ifdef NETWORKING_LOCALWEBSERVER_ENABLE_PORT_OVERRIDE + // save server's port + uint32 port = Networking::LocalWebserver::getPort(); + if (_serverPort) { + uint64 contents = _serverPort->getEditString().asUint64(); + if (contents != 0) + port = contents; + } + ConfMan.setInt("local_server_port", port); + ConfMan.flushToDisk(); +#endif + StorageWizardDialog dialog(_selectedStorageIndex); + dialog.runModal(); + //update container's scrollbar + reflowLayout(); + break; + } + case kRefreshStorageCmd: + { + CloudMan.info( + new Common::Callback<GlobalOptionsDialog, Cloud::Storage::StorageInfoResponse>(this, &GlobalOptionsDialog::storageInfoCallback), + new Common::Callback<GlobalOptionsDialog, Networking::ErrorResponse>(this, &GlobalOptionsDialog::storageErrorCallback) + ); + Common::String dir = CloudMan.savesDirectoryPath(); + if (dir.lastChar() == '/') + dir.deleteLastChar(); + CloudMan.listDirectory( + dir, + new Common::Callback<GlobalOptionsDialog, Cloud::Storage::ListDirectoryResponse>(this, &GlobalOptionsDialog::storageListDirectoryCallback), + new Common::Callback<GlobalOptionsDialog, Networking::ErrorResponse>(this, &GlobalOptionsDialog::storageErrorCallback) + ); + break; + } + case kDownloadStorageCmd: + { + DownloadDialog dialog(_selectedStorageIndex, _launcher); + dialog.runModal(); + break; + } +#endif +#ifdef USE_SDL_NET + case kRunServerCmd: + { +#ifdef NETWORKING_LOCALWEBSERVER_ENABLE_PORT_OVERRIDE + // save server's port + uint32 port = Networking::LocalWebserver::getPort(); + if (_serverPort) { + uint64 contents = _serverPort->getEditString().asUint64(); + if (contents != 0) + port = contents; + } + ConfMan.setInt("local_server_port", port); + ConfMan.flushToDisk(); +#endif + + if (LocalServer.isRunning()) + LocalServer.stopOnIdle(); + else + LocalServer.start(); + + break; + } + + case kServerPortClearCmd: { + if (_serverPort) { + _serverPort->setEditString(Common::String::format("%u", Networking::LocalWebserver::DEFAULT_SERVER_PORT)); + } + draw(); + break; + } +#endif #ifdef GUI_ENABLE_KEYSDIALOG case kChooseKeyMappingCmd: _keysDialog->runModal(); @@ -1534,6 +1783,23 @@ void GlobalOptionsDialog::handleCommand(CommandSender *sender, uint32 cmd, uint3 } } +void GlobalOptionsDialog::handleTickle() { + OptionsDialog::handleTickle(); +#ifdef USE_CLOUD +#ifdef USE_SDL_NET + if (LocalServer.isRunning() != _serverWasRunning) { + _serverWasRunning = !_serverWasRunning; + _redrawCloudTab = true; + } +#endif + if (_redrawCloudTab) { + setupCloudTab(); + draw(); + _redrawCloudTab = false; + } +#endif +} + void GlobalOptionsDialog::reflowLayout() { int activeTab = _tabWidget->getActiveTab(); @@ -1567,6 +1833,218 @@ void GlobalOptionsDialog::reflowLayout() { _tabWidget->setActiveTab(activeTab); OptionsDialog::reflowLayout(); +#ifdef USE_CLOUD + setupCloudTab(); +#endif } +#ifdef USE_CLOUD +void GlobalOptionsDialog::setupCloudTab() { + int serverLabelPosition = -1; //no override +#ifdef USE_LIBCURL + _selectedStorageIndex = _storagePopUp->getSelectedTag(); + + if (_storagePopUpDesc) _storagePopUpDesc->setVisible(true); + if (_storagePopUp) _storagePopUp->setVisible(true); + + bool shown = (_selectedStorageIndex != Cloud::kStorageNoneId); + if (_storageUsernameDesc) _storageUsernameDesc->setVisible(shown); + if (_storageUsername) { + Common::String username = CloudMan.getStorageUsername(_selectedStorageIndex); + if (username == "") + username = _("<none>"); + _storageUsername->setLabel(username); + _storageUsername->setVisible(shown); + } + if (_storageUsedSpaceDesc) _storageUsedSpaceDesc->setVisible(shown); + if (_storageUsedSpace) { + uint64 usedSpace = CloudMan.getStorageUsedSpace(_selectedStorageIndex); + _storageUsedSpace->setLabel(Common::String::format(_("%llu bytes"), usedSpace)); + _storageUsedSpace->setVisible(shown); + } + if (_storageLastSyncDesc) _storageLastSyncDesc->setVisible(shown); + if (_storageLastSync) { + Common::String sync = CloudMan.getStorageLastSync(_selectedStorageIndex); + if (sync == "") { + if (_selectedStorageIndex == CloudMan.getStorageIndex() && CloudMan.isSyncing()) + sync = _("<right now>"); + else + sync = _("<never>"); + } + _storageLastSync->setLabel(sync); + _storageLastSync->setVisible(shown); + } + if (_storageConnectButton) + _storageConnectButton->setVisible(shown); + if (_storageRefreshButton) + _storageRefreshButton->setVisible(shown && _selectedStorageIndex == CloudMan.getStorageIndex()); + if (_storageDownloadButton) + _storageDownloadButton->setVisible(shown && _selectedStorageIndex == CloudMan.getStorageIndex()); + if (!shown) + serverLabelPosition = (_storageUsernameDesc ? _storageUsernameDesc->getRelY() : 0); +#else + _selectedStorageIndex = 0; + + if (_storagePopUpDesc) + _storagePopUpDesc->setVisible(false); + if (_storagePopUp) + _storagePopUp->setVisible(false); + if (_storageUsernameDesc) + _storageUsernameDesc->setVisible(false); + if (_storageUsernameDesc) + _storageUsernameDesc->setVisible(false); + if (_storageUsername) + _storageUsername->setVisible(false); + if (_storageUsedSpaceDesc) + _storageUsedSpaceDesc->setVisible(false); + if (_storageUsedSpace) + _storageUsedSpace->setVisible(false); + if (_storageLastSyncDesc) + _storageLastSyncDesc->setVisible(false); + if (_storageLastSync) + _storageLastSync->setVisible(false); + if (_storageConnectButton) + _storageConnectButton->setVisible(false); + if (_storageRefreshButton) + _storageRefreshButton->setVisible(false); + if (_storageDownloadButton) + _storageDownloadButton->setVisible(false); + + serverLabelPosition = (_storagePopUpDesc ? _storagePopUpDesc->getRelY() : 0); +#endif +#ifdef USE_SDL_NET + //determine original widget's positions + int16 x, y; + uint16 w, h; + int serverButtonY, serverInfoY; + int serverRootButtonY, serverRootY, serverRootClearButtonY; + int serverPortDescY, serverPortY, serverPortClearButtonY; + if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.RunServerButton", x, y, w, h)) + warning("GlobalOptions_Cloud_Container.RunServerButton's position is undefined"); + serverButtonY = y; + if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.ServerInfoLabel", x, y, w, h)) + warning("GlobalOptions_Cloud_Container.ServerInfoLabel's position is undefined"); + serverInfoY = y; + + if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.RootPathButton", x, y, w, h)) + warning("GlobalOptions_Cloud_Container.RootPathButton's position is undefined"); + serverRootButtonY = y; + if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.RootPath", x, y, w, h)) + warning("GlobalOptions_Cloud_Container.RootPath's position is undefined"); + serverRootY = y; + if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.RootPathClearButton", x, y, w, h)) + warning("GlobalOptions_Cloud_Container.RootPathClearButton's position is undefined"); + serverRootClearButtonY = y; + + if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.ServerPortDesc", x, y, w, h)) + warning("GlobalOptions_Cloud_Container.ServerPortDesc's position is undefined"); + serverPortDescY = y; + if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.ServerPortEditText", x, y, w, h)) + warning("GlobalOptions_Cloud_Container.ServerPortEditText's position is undefined"); + serverPortY = y; + if (!g_gui.xmlEval()->getWidgetData("GlobalOptions_Cloud_Container.ServerPortClearButton", x, y, w, h)) + warning("GlobalOptions_Cloud_Container.ServerPortClearButton's position is undefined"); + serverPortClearButtonY = y; + + bool serverIsRunning = LocalServer.isRunning(); + + if (serverLabelPosition < 0) + serverLabelPosition = serverInfoY; + if (_runServerButton) { + _runServerButton->setVisible(true); + _runServerButton->setPos(_runServerButton->getRelX(), serverLabelPosition + serverButtonY - serverInfoY); + _runServerButton->setLabel(_(serverIsRunning ? "Stop server" : "Run server")); + _runServerButton->setTooltip(_(serverIsRunning ? "Stop local webserver" : "Run local webserver")); + } + if (_serverInfoLabel) { + _serverInfoLabel->setVisible(true); + _serverInfoLabel->setPos(_serverInfoLabel->getRelX(), serverLabelPosition); + if (serverIsRunning) + _serverInfoLabel->setLabel(LocalServer.getAddress()); + else + _serverInfoLabel->setLabel(_("Not running")); + } + if (_rootPathButton) { + _rootPathButton->setVisible(true); + _rootPathButton->setPos(_rootPathButton->getRelX(), serverLabelPosition + serverRootButtonY - serverInfoY); + } + if (_rootPath) { + _rootPath->setVisible(true); + _rootPath->setPos(_rootPath->getRelX(), serverLabelPosition + serverRootY - serverInfoY); + } + if (_rootPathClearButton) { + _rootPathClearButton->setVisible(true); + _rootPathClearButton->setPos(_rootPathClearButton->getRelX(), serverLabelPosition + serverRootClearButtonY - serverInfoY); + } +#ifdef NETWORKING_LOCALWEBSERVER_ENABLE_PORT_OVERRIDE + if (_serverPortDesc) { + _serverPortDesc->setVisible(true); + _serverPortDesc->setPos(_serverPortDesc->getRelX(), serverLabelPosition + serverPortDescY - serverInfoY); + _serverPortDesc->setEnabled(!serverIsRunning); + } + if (_serverPort) { + _serverPort->setVisible(true); + _serverPort->setPos(_serverPort->getRelX(), serverLabelPosition + serverPortY - serverInfoY); + _serverPort->setEnabled(!serverIsRunning); + } + if (_serverPortClearButton) { + _serverPortClearButton->setVisible(true); + _serverPortClearButton->setPos(_serverPortClearButton->getRelX(), serverLabelPosition + serverPortClearButtonY - serverInfoY); + _serverPortClearButton->setEnabled(!serverIsRunning); + } +#else + if (_serverPortDesc) + _serverPortDesc->setVisible(false); + if (_serverPort) + _serverPort->setVisible(false); + if (_serverPortClearButton) + _serverPortClearButton->setVisible(false); +#endif +#else + if (_runServerButton) + _runServerButton->setVisible(false); + if (_serverInfoLabel) + _serverInfoLabel->setVisible(false); + if (_rootPathButton) + _rootPathButton->setVisible(false); + if (_rootPath) + _rootPath->setVisible(false); + if (_rootPathClearButton) + _rootPathClearButton->setVisible(false); + if (_serverPortDesc) + _serverPortDesc->setVisible(false); + if (_serverPort) + _serverPort->setVisible(false); + if (_serverPortClearButton) + _serverPortClearButton->setVisible(false); +#endif +} +#endif +#ifdef USE_LIBCURL +void GlobalOptionsDialog::storageInfoCallback(Cloud::Storage::StorageInfoResponse response) { + //we could've used response.value.email() + //but Storage already notified CloudMan + //so we just set the flag to redraw our cloud tab + _redrawCloudTab = true; +} + +void GlobalOptionsDialog::storageListDirectoryCallback(Cloud::Storage::ListDirectoryResponse response) { + Common::Array<Cloud::StorageFile> &files = response.value; + uint64 totalSize = 0; + for (uint32 i = 0; i < files.size(); ++i) + if (!files[i].isDirectory()) + totalSize += files[i].size(); + CloudMan.setStorageUsedSpace(CloudMan.getStorageIndex(), totalSize); + _redrawCloudTab = true; +} + +void GlobalOptionsDialog::storageErrorCallback(Networking::ErrorResponse response) { + debug(9, "GlobalOptionsDialog: error response (%s, %ld):", (response.failed ? "failed" : "interrupted"), response.httpResponseCode); + debug(9, "%s", response.response.c_str()); + + if (!response.interrupted) + g_system->displayMessageOnOSD(_("Request failed.\nCheck your Internet connection.")); +} +#endif + } // End of namespace GUI diff --git a/gui/options.h b/gui/options.h index 294b41794b..03dbdac492 100644 --- a/gui/options.h +++ b/gui/options.h @@ -37,9 +37,15 @@ #include "gui/fluidsynth-dialog.h" #endif +#ifdef USE_LIBCURL +#include "backends/cloud/storage.h" +#endif + namespace GUI { +class LauncherDialog; class CheckboxWidget; +class EditTextWidget; class PopUpWidget; class SliderWidget; class StaticTextWidget; @@ -200,16 +206,18 @@ protected: class GlobalOptionsDialog : public OptionsDialog { public: - GlobalOptionsDialog(); + GlobalOptionsDialog(LauncherDialog *launcher); ~GlobalOptionsDialog(); void open(); void close(); void handleCommand(CommandSender *sender, uint32 cmd, uint32 data); + void handleTickle(); virtual void reflowLayout(); protected: + LauncherDialog *_launcher; #ifdef GUI_ENABLE_KEYSDIALOG KeysDialog *_keysDialog; #endif @@ -241,6 +249,43 @@ protected: StaticTextWidget *_updatesPopUpDesc; PopUpWidget *_updatesPopUp; #endif + +#ifdef USE_CLOUD + // + // Cloud controls + // + uint32 _selectedStorageIndex; + StaticTextWidget *_storagePopUpDesc; + PopUpWidget *_storagePopUp; + StaticTextWidget *_storageUsernameDesc; + StaticTextWidget *_storageUsername; + StaticTextWidget *_storageUsedSpaceDesc; + StaticTextWidget *_storageUsedSpace; + StaticTextWidget *_storageLastSyncDesc; + StaticTextWidget *_storageLastSync; + ButtonWidget *_storageConnectButton; + ButtonWidget *_storageRefreshButton; + ButtonWidget *_storageDownloadButton; + ButtonWidget *_runServerButton; + StaticTextWidget *_serverInfoLabel; + ButtonWidget *_rootPathButton; + StaticTextWidget *_rootPath; + ButtonWidget *_rootPathClearButton; + StaticTextWidget *_serverPortDesc; + EditTextWidget *_serverPort; + ButtonWidget *_serverPortClearButton; + bool _redrawCloudTab; +#ifdef USE_SDL_NET + bool _serverWasRunning; +#endif + + void setupCloudTab(); +#endif +#ifdef USE_LIBCURL + void storageInfoCallback(Cloud::Storage::StorageInfoResponse response); + void storageListDirectoryCallback(Cloud::Storage::ListDirectoryResponse response); + void storageErrorCallback(Networking::ErrorResponse response); +#endif }; } // End of namespace GUI diff --git a/gui/remotebrowser.cpp b/gui/remotebrowser.cpp new file mode 100644 index 0000000000..b87fc60f7e --- /dev/null +++ b/gui/remotebrowser.cpp @@ -0,0 +1,232 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "gui/remotebrowser.h" +#include "gui/widgets/list.h" + +#include "common/config-manager.h" +#include "common/system.h" +#include "common/algorithm.h" + +#include "common/translation.h" +#include <backends/networking/curl/request.h> +#include <backends/cloud/storage.h> +#include <backends/cloud/cloudmanager.h> +#include "message.h" + +namespace GUI { + +enum { + kChooseCmd = 'Chos', + kGoUpCmd = 'GoUp' +}; + +RemoteBrowserDialog::RemoteBrowserDialog(const char *title): + Dialog("Browser"), _navigationLocked(false), _updateList(false), _showError(false), + _workingRequest(nullptr), _ignoreCallback(false) { + _backgroundType = GUI::ThemeEngine::kDialogBackgroundPlain; + + new StaticTextWidget(this, "Browser.Headline", title); + _currentPath = new StaticTextWidget(this, "Browser.Path", "DUMMY"); + + _fileList = new ListWidget(this, "Browser.List"); + _fileList->setNumberingMode(kListNumberingOff); + _fileList->setEditable(false); + + if (g_system->getOverlayWidth() > 320) + new ButtonWidget(this, "Browser.Up", _("Go up"), _("Go to previous directory level"), kGoUpCmd); + else + new ButtonWidget(this, "Browser.Up", _c("Go up", "lowres"), _("Go to previous directory level"), kGoUpCmd); + new ButtonWidget(this, "Browser.Cancel", _("Cancel"), 0, kCloseCmd); + new ButtonWidget(this, "Browser.Choose", _("Choose"), 0, kChooseCmd); +} + +RemoteBrowserDialog::~RemoteBrowserDialog() { + if (_workingRequest) { + _ignoreCallback = true; + _workingRequest->finish(); + } +} + +void RemoteBrowserDialog::open() { + Dialog::open(); + listDirectory(Cloud::StorageFile()); +} + +void RemoteBrowserDialog::close() { + Dialog::close(); + if (_workingRequest) { + _ignoreCallback = true; + _workingRequest->finish(); + _ignoreCallback = false; + } +} + +void RemoteBrowserDialog::handleCommand(CommandSender *sender, uint32 cmd, uint32 data) { + switch (cmd) { + case kChooseCmd: { + // If nothing is selected in the list widget, choose the current dir. + // Else, choose the dir that is selected. + int selection = _fileList->getSelected(); + if (selection >= 0) + _choice = _nodeContent[selection]; + else + _choice = _node; + setResult(1); + close(); + break; + } + case kGoUpCmd: + goUp(); + break; + case kListItemActivatedCmd: + case kListItemDoubleClickedCmd: + if (_nodeContent[data].isDirectory()) { + _rememberedNodeContents[_node.path()] = _nodeContent; + listDirectory(_nodeContent[data]); + } + break; + case kListSelectionChangedCmd: + // We do not allow selecting directories, + // thus we will invalidate the selection + // when the user selects a directory over here. + if (data != (uint32)-1 && !_nodeContent[data].isDirectory()) + _fileList->setSelected(-1); + break; + default: + Dialog::handleCommand(sender, cmd, data); + } +} + +void RemoteBrowserDialog::handleTickle() { + if (_updateList) { + updateListing(); + _updateList = false; + } + + if (_showError) { + _showError = false; + MessageDialog alert(_("ScummVM couldn't list the directory!")); + alert.runModal(); + } + + Dialog::handleTickle(); +} + +void RemoteBrowserDialog::updateListing() { + // Update the path display + Common::String path = _node.path(); + if (path.empty()) + path = "/"; //root + if (_navigationLocked) + path = "Loading... " + path; + _currentPath->setLabel(path); + + if (!_navigationLocked) { + // Populate the ListWidget + ListWidget::StringArray list; + ListWidget::ColorList colors; + for (Common::Array<Cloud::StorageFile>::iterator i = _nodeContent.begin(); i != _nodeContent.end(); ++i) { + if (i->isDirectory()) { + list.push_back(i->name() + "/"); + colors.push_back(ThemeEngine::kFontColorNormal); + } else { + list.push_back(i->name()); + colors.push_back(ThemeEngine::kFontColorAlternate); + } + } + + _fileList->setList(list, &colors); + _fileList->scrollTo(0); + } + + _fileList->setEnabled(!_navigationLocked); + + // Finally, redraw + draw(); +} + +void RemoteBrowserDialog::goUp() { + if (_rememberedNodeContents.contains(_node.path())) + _rememberedNodeContents.erase(_node.path()); + + Common::String path = _node.path(); + if (path.size() && (path.lastChar() == '/' || path.lastChar() == '\\')) + path.deleteLastChar(); + if (path.empty()) { + _rememberedNodeContents.erase(""); + } else { + for (int i = path.size() - 1; i >= 0; --i) + if (i == 0 || path[i] == '/' || path[i] == '\\') { + path.erase(i); + break; + } + } + + listDirectory(Cloud::StorageFile(path, 0, 0, true)); +} + +void RemoteBrowserDialog::listDirectory(Cloud::StorageFile node) { + if (_navigationLocked || _workingRequest) + return; + + if (_rememberedNodeContents.contains(node.path())) { + _nodeContent = _rememberedNodeContents[node.path()]; + } else { + _navigationLocked = true; + + _workingRequest = CloudMan.listDirectory( + node.path(), + new Common::Callback<RemoteBrowserDialog, Cloud::Storage::ListDirectoryResponse>(this, &RemoteBrowserDialog::directoryListedCallback), + new Common::Callback<RemoteBrowserDialog, Networking::ErrorResponse>(this, &RemoteBrowserDialog::directoryListedErrorCallback), + false + ); + } + + _backupNode = _node; + _node = node; + updateListing(); +} + +void RemoteBrowserDialog::directoryListedCallback(Cloud::Storage::ListDirectoryResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + _navigationLocked = false; + _nodeContent = response.value; + Common::sort(_nodeContent.begin(), _nodeContent.end(), FileListOrder()); + _updateList = true; +} + +void RemoteBrowserDialog::directoryListedErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + _navigationLocked = false; + _node = _backupNode; + _updateList = true; + _showError = true; +} + +} // End of namespace GUI diff --git a/gui/remotebrowser.h b/gui/remotebrowser.h new file mode 100644 index 0000000000..190d8c6895 --- /dev/null +++ b/gui/remotebrowser.h @@ -0,0 +1,84 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef GUI_REMOTEBROWSER_DIALOG_H +#define GUI_REMOTEBROWSER_DIALOG_H + +#include "gui/dialog.h" +#include "common/fs.h" +#include <backends/cloud/storagefile.h> +#include <backends/networking/curl/request.h> +#include <backends/cloud/storage.h> + +namespace GUI { + +class ListWidget; +class StaticTextWidget; +class CheckboxWidget; +class CommandSender; + +class RemoteBrowserDialog : public Dialog { +public: + RemoteBrowserDialog(const char *title); + virtual ~RemoteBrowserDialog(); + + virtual void open(); + virtual void close(); + virtual void handleCommand(CommandSender *sender, uint32 cmd, uint32 data); + virtual void handleTickle(); + + const Cloud::StorageFile &getResult() { return _choice; } + +protected: + ListWidget *_fileList; + StaticTextWidget *_currentPath; + Cloud::StorageFile _node, _backupNode; + Common::Array<Cloud::StorageFile> _nodeContent; + Common::HashMap<Common::String, Common::Array<Cloud::StorageFile> > _rememberedNodeContents; + Cloud::StorageFile _choice; + bool _navigationLocked; + bool _updateList; + bool _showError; + + Networking::Request *_workingRequest; + bool _ignoreCallback; + + void updateListing(); + void goUp(); + void listDirectory(Cloud::StorageFile node); + void directoryListedCallback(Cloud::Storage::ListDirectoryResponse response); + void directoryListedErrorCallback(Networking::ErrorResponse error); + + struct FileListOrder : public Common::BinaryFunction<Cloud::StorageFile, Cloud::StorageFile, bool> { + bool operator()(const Cloud::StorageFile &x, const Cloud::StorageFile &y) const { + if (x.isDirectory() != y.isDirectory()) { + return x.isDirectory(); //x < y (directory < not directory) or x > y (not directory > directory) + } + + return x.name() < y.name(); + } + }; +}; + +} // End of namespace GUI + +#endif diff --git a/gui/saveload-dialog.cpp b/gui/saveload-dialog.cpp index 3d4adfff2b..ae3612f778 100644 --- a/gui/saveload-dialog.cpp +++ b/gui/saveload-dialog.cpp @@ -21,6 +21,13 @@ */ #include "gui/saveload-dialog.h" + +#ifdef USE_LIBCURL +#include "backends/cloud/cloudmanager.h" +#include "backends/cloud/savessyncrequest.h" +#include "backends/networking/curl/connectionmanager.h" +#endif + #include "common/translation.h" #include "common/config-manager.h" @@ -30,9 +37,68 @@ #include "gui/widgets/edittext.h" #include "graphics/scaler.h" +#include <common/savefile.h> namespace GUI { +#ifdef USE_LIBCURL + +enum { + kCancelSyncCmd = 'PDCS', + kBackgroundSyncCmd = 'PDBS' +}; + +SaveLoadCloudSyncProgressDialog::SaveLoadCloudSyncProgressDialog(bool canRunInBackground): Dialog("SaveLoadCloudSyncProgress"), _close(false) { + _label = new StaticTextWidget(this, "SaveLoadCloudSyncProgress.TitleText", "Downloading saves..."); + uint32 progress = (uint32)(100 * CloudMan.getSyncDownloadingProgress()); + _progressBar = new SliderWidget(this, "SaveLoadCloudSyncProgress.ProgressBar"); + _progressBar->setMinValue(0); + _progressBar->setMaxValue(100); + _progressBar->setValue(progress); + _progressBar->setEnabled(false); + _percentLabel = new StaticTextWidget(this, "SaveLoadCloudSyncProgress.PercentText", Common::String::format("%u %%", progress)); + new ButtonWidget(this, "SaveLoadCloudSyncProgress.Cancel", "Cancel", 0, kCancelSyncCmd, Common::ASCII_ESCAPE); // Cancel dialog + ButtonWidget *backgroundButton = new ButtonWidget(this, "SaveLoadCloudSyncProgress.Background", "Run in background", 0, kBackgroundSyncCmd, Common::ASCII_RETURN); // Confirm dialog + backgroundButton->setEnabled(canRunInBackground); + draw(); +} + +SaveLoadCloudSyncProgressDialog::~SaveLoadCloudSyncProgressDialog() { + CloudMan.setSyncTarget(nullptr); //not that dialog, at least +} + +void SaveLoadCloudSyncProgressDialog::handleCommand(CommandSender *sender, uint32 cmd, uint32 data) { + switch(cmd) { + case kSavesSyncProgressCmd: + _percentLabel->setLabel(Common::String::format("%u%%", data)); + _progressBar->setValue(data); + _progressBar->draw(); + break; + + case kCancelSyncCmd: + setResult(kCancelSyncCmd); + close(); + break; + + case kSavesSyncEndedCmd: + case kBackgroundSyncCmd: + _close = true; + break; + } + + Dialog::handleCommand(sender, cmd, data); +} + +void SaveLoadCloudSyncProgressDialog::handleTickle() { + if (_close) { + setResult(kBackgroundSyncCmd); + close(); + } + + Dialog::handleTickle(); +} +#endif + #ifndef DISABLE_SAVELOADCHOOSER_GRID SaveLoadChooserType getRequestedSaveLoadDialog(const MetaEngine &metaEngine) { const Common::String &userConfig = ConfMan.get("gui_saveload_chooser", Common::ConfigManager::kApplicationDomain); @@ -45,9 +111,9 @@ SaveLoadChooserType getRequestedSaveLoadDialog(const MetaEngine &metaEngine) { g_gui.checkScreenChange(); if (g_gui.getWidth() >= 640 && g_gui.getHeight() >= 400 - && metaEngine.hasFeature(MetaEngine::kSavesSupportMetaInfo) - && metaEngine.hasFeature(MetaEngine::kSavesSupportThumbnail) - && userConfig.equalsIgnoreCase("grid")) { + && metaEngine.hasFeature(MetaEngine::kSavesSupportMetaInfo) + && metaEngine.hasFeature(MetaEngine::kSavesSupportThumbnail) + && userConfig.equalsIgnoreCase("grid")) { // In case we are 640x400 or higher, this dialog is not in save mode, // the user requested the grid dialog and the engines supports it we // try to set it up. @@ -66,7 +132,8 @@ enum { SaveLoadChooserDialog::SaveLoadChooserDialog(const Common::String &dialogName, const bool saveMode) : Dialog(dialogName), _metaEngine(0), _delSupport(false), _metaInfoSupport(false), - _thumbnailSupport(false), _saveDateSupport(false), _playTimeSupport(false), _saveMode(saveMode) + _thumbnailSupport(false), _saveDateSupport(false), _playTimeSupport(false), _saveMode(saveMode), + _dialogWasShown(false) #ifndef DISABLE_SAVELOADCHOOSER_GRID , _listButton(0), _gridButton(0) #endif // !DISABLE_SAVELOADCHOOSER_GRID @@ -78,7 +145,8 @@ SaveLoadChooserDialog::SaveLoadChooserDialog(const Common::String &dialogName, c SaveLoadChooserDialog::SaveLoadChooserDialog(int x, int y, int w, int h, const bool saveMode) : Dialog(x, y, w, h), _metaEngine(0), _delSupport(false), _metaInfoSupport(false), - _thumbnailSupport(false), _saveDateSupport(false), _playTimeSupport(false), _saveMode(saveMode) + _thumbnailSupport(false), _saveDateSupport(false), _playTimeSupport(false), _saveMode(saveMode), + _dialogWasShown(false) #ifndef DISABLE_SAVELOADCHOOSER_GRID , _listButton(0), _gridButton(0) #endif // !DISABLE_SAVELOADCHOOSER_GRID @@ -88,12 +156,27 @@ SaveLoadChooserDialog::SaveLoadChooserDialog(int x, int y, int w, int h, const b #endif // !DISABLE_SAVELOADCHOOSER_GRID } +SaveLoadChooserDialog::~SaveLoadChooserDialog() { +#ifdef USE_LIBCURL + CloudMan.setSyncTarget(nullptr); //not that dialog, at least +#endif +} + void SaveLoadChooserDialog::open() { Dialog::open(); // So that quitting ScummVM will not cause the dialog result to say a // saved game was selected. setResult(-1); + + _dialogWasShown = false; +} + +void SaveLoadChooserDialog::close() { +#ifdef USE_LIBCURL + CloudMan.setSyncTarget(nullptr); //not that dialog, at least +#endif + Dialog::close(); } int SaveLoadChooserDialog::run(const Common::String &target, const MetaEngine *metaEngine) { @@ -132,9 +215,53 @@ void SaveLoadChooserDialog::handleCommand(CommandSender *sender, uint32 cmd, uin } #endif // !DISABLE_SAVELOADCHOOSER_GRID +#ifdef USE_LIBCURL + if (cmd == kSavesSyncProgressCmd || cmd == kSavesSyncEndedCmd) { + //this dialog only gets these commands if the progress dialog was shown and user clicked "run in background" + return updateSaveList(); + } +#endif + return Dialog::handleCommand(sender, cmd, data); } +#ifdef USE_LIBCURL +void SaveLoadChooserDialog::runSaveSync(bool hasSavepathOverride) { + if (!CloudMan.isSyncing()) { + if (hasSavepathOverride) { + ConnMan.showCloudDisabledIcon(); + } else { + Cloud::SavesSyncRequest *request = CloudMan.syncSaves(); + if (request) + request->setTarget(this); + } + } +} +#endif + +void SaveLoadChooserDialog::handleTickle() { +#ifdef USE_LIBCURL + if (!_dialogWasShown && CloudMan.isSyncing()) { + Common::Array<Common::String> files = CloudMan.getSyncingFiles(); + if (!files.empty()) { + { + SaveLoadCloudSyncProgressDialog dialog(_metaEngine ? _metaEngine->hasFeature(MetaEngine::kSimpleSavesNames) : false); + CloudMan.setSyncTarget(&dialog); + int result = dialog.runModal(); + if (result == kCancelSyncCmd) { + CloudMan.cancelSync(); + } + } + //dialog changes syncTarget to nullptr after that } + CloudMan.setSyncTarget(this); + _dialogWasShown = true; + updateSaveList(); + } + } +#endif + Dialog::handleTickle(); +} + void SaveLoadChooserDialog::reflowLayout() { #ifndef DISABLE_SAVELOADCHOOSER_GRID addChooserButtons(); @@ -152,6 +279,46 @@ void SaveLoadChooserDialog::reflowLayout() { Dialog::reflowLayout(); } +void SaveLoadChooserDialog::updateSaveList() { +#ifdef USE_LIBCURL + Common::Array<Common::String> files = CloudMan.getSyncingFiles(); //returns empty array if not syncing + g_system->getSavefileManager()->updateSavefilesList(files); +#endif + listSaves(); +} + +void SaveLoadChooserDialog::listSaves() { + if (!_metaEngine) return; //very strange + _saveList = _metaEngine->listSaves(_target.c_str()); + +#ifdef USE_LIBCURL + //if there is Cloud support, add currently synced files as "locked" saves in the list + if (_metaEngine->hasFeature(MetaEngine::kSimpleSavesNames)) { + Common::String pattern = _target + ".###"; + Common::Array<Common::String> files = CloudMan.getSyncingFiles(); //returns empty array if not syncing + for (uint32 i = 0; i < files.size(); ++i) { + if (!files[i].matchString(pattern, true)) + continue; + + //make up some slot number + int slotNum = 0; + for (uint32 j = (files[i].size() > 3 ? files[i].size() - 3 : 0); j < files[i].size(); ++j) { //3 last chars + char c = files[i][j]; + if (c < '0' || c > '9') + continue; + slotNum = slotNum * 10 + (c - '0'); + } + + SaveStateDescriptor slot(slotNum, files[i]); + slot.setLocked(true); + _saveList.push_back(slot); + } + + Common::sort(_saveList.begin(), _saveList.end(), SaveStateDescriptorSlotComparator()); + } +#endif +} + #ifndef DISABLE_SAVELOADCHOOSER_GRID void SaveLoadChooserDialog::addChooserButtons() { if (_listButton) { @@ -353,6 +520,7 @@ void SaveLoadChooserSimple::updateSelection(bool redraw) { bool isDeletable = _delSupport; bool isWriteProtected = false; bool startEditMode = _list->isEditable(); + bool isLocked = false; // We used to support letting the themes specify the fill color with our // initial theme based GUI. But this support was dropped. @@ -362,10 +530,11 @@ void SaveLoadChooserSimple::updateSelection(bool redraw) { _playtime->setLabel(_("No playtime saved")); if (selItem >= 0 && _metaInfoSupport) { - SaveStateDescriptor desc = _metaEngine->querySaveMetaInfos(_target.c_str(), _saveList[selItem].getSaveSlot()); + SaveStateDescriptor desc = (_saveList[selItem].getLocked() ? _saveList[selItem] : _metaEngine->querySaveMetaInfos(_target.c_str(), _saveList[selItem].getSaveSlot())); isDeletable = desc.getDeletableFlag() && _delSupport; isWriteProtected = desc.getWriteProtectedFlag(); + isLocked = desc.getLocked(); // Don't allow the user to change the description of write protected games if (isWriteProtected) @@ -398,9 +567,9 @@ void SaveLoadChooserSimple::updateSelection(bool redraw) { if (_list->isEditable()) { - // Disable the save button if nothing is selected, or if the selected - // game is write protected - _chooseButton->setEnabled(selItem >= 0 && !isWriteProtected); + // Disable the save button if slot is locked, nothing is selected, + // or if the selected game is write protected + _chooseButton->setEnabled(!isLocked && selItem >= 0 && !isWriteProtected); if (startEditMode) { _list->startEditMode(); @@ -412,13 +581,13 @@ void SaveLoadChooserSimple::updateSelection(bool redraw) { } } } else { - // Disable the load button if nothing is selected, or if an empty - // list item is selected. - _chooseButton->setEnabled(selItem >= 0 && !_list->getSelectedString().empty()); + // Disable the load button if slot is locked, nothing is selected, + // or if an empty list item is selected. + _chooseButton->setEnabled(!isLocked && selItem >= 0 && !_list->getSelectedString().empty()); } // Delete will always be disabled if the engine doesn't support it. - _deleteButton->setEnabled(isDeletable && (selItem >= 0) && (!_list->getSelectedString().empty())); + _deleteButton->setEnabled(isDeletable && !isLocked && (selItem >= 0) && (!_list->getSelectedString().empty())); if (redraw) { _gfxWidget->draw(); @@ -463,7 +632,7 @@ void SaveLoadChooserSimple::close() { } void SaveLoadChooserSimple::updateSaveList() { - _saveList = _metaEngine->listSaves(_target.c_str()); + SaveLoadChooserDialog::updateSaveList(); int curSlot = 0; int saveSlot = 0; @@ -496,7 +665,7 @@ void SaveLoadChooserSimple::updateSaveList() { description = _("Untitled savestate"); colors.push_back(ThemeEngine::kFontColorAlternate); } else { - colors.push_back(ThemeEngine::kFontColorNormal); + colors.push_back((x->getLocked() ? ThemeEngine::kFontColorAlternate : ThemeEngine::kFontColorNormal)); } saveNames.push_back(description); @@ -524,6 +693,7 @@ void SaveLoadChooserSimple::updateSaveList() { } _list->setList(saveNames, &colors); + draw(); } // SaveLoadChooserGrid implementation @@ -619,10 +789,16 @@ void SaveLoadChooserGrid::handleMouseWheel(int x, int y, int direction) { } } +void SaveLoadChooserGrid::updateSaveList() { + SaveLoadChooserDialog::updateSaveList(); + updateSaves(); + draw(); +} + void SaveLoadChooserGrid::open() { SaveLoadChooserDialog::open(); - _saveList = _metaEngine->listSaves(_target.c_str()); + listSaves(); _resultString.clear(); // Load information to restore the last page the user had open. @@ -863,7 +1039,7 @@ void SaveLoadChooserGrid::updateSaves() { for (uint i = _curPage * _entriesPerPage, curNum = 0; i < _saveList.size() && curNum < _entriesPerPage; ++i, ++curNum) { const uint saveSlot = _saveList[i].getSaveSlot(); - SaveStateDescriptor desc = _metaEngine->querySaveMetaInfos(_target.c_str(), saveSlot); + SaveStateDescriptor desc = (_saveList[i].getLocked() ? _saveList[i] : _metaEngine->querySaveMetaInfos(_target.c_str(), saveSlot)); SlotButton &curButton = _buttons[curNum]; curButton.setVisible(true); const Graphics::Surface *thumbnail = desc.getThumbnail(); @@ -908,6 +1084,10 @@ void SaveLoadChooserGrid::updateSaves() { } else { curButton.button->setEnabled(true); } + + //that would make it look "disabled" if slot is locked + curButton.button->setEnabled(!desc.getLocked()); + curButton.description->setEnabled(!desc.getLocked()); } const uint numPages = (_entriesPerPage != 0 && !_saveList.empty()) ? ((_saveList.size() + _entriesPerPage - 1) / _entriesPerPage) : 1; diff --git a/gui/saveload-dialog.h b/gui/saveload-dialog.h index 31f28f6452..fb2833355f 100644 --- a/gui/saveload-dialog.h +++ b/gui/saveload-dialog.h @@ -30,6 +30,25 @@ namespace GUI { +#ifdef USE_LIBCURL +enum SaveLoadCloudSyncProgress { + kSavesSyncProgressCmd = 'SSPR', + kSavesSyncEndedCmd = 'SSEN' +}; + +class SaveLoadCloudSyncProgressDialog : public Dialog { //protected? + StaticTextWidget *_label, *_percentLabel; + SliderWidget *_progressBar; + bool _close; +public: + SaveLoadCloudSyncProgressDialog(bool canRunInBackground); + virtual ~SaveLoadCloudSyncProgressDialog(); + + virtual void handleCommand(CommandSender *sender, uint32 cmd, uint32 data); + virtual void handleTickle(); +}; +#endif + #define kSwitchSaveLoadDialog -2 // TODO: We might want to disable the grid based save/load chooser for more @@ -53,13 +72,21 @@ class SaveLoadChooserDialog : protected Dialog { public: SaveLoadChooserDialog(const Common::String &dialogName, const bool saveMode); SaveLoadChooserDialog(int x, int y, int w, int h, const bool saveMode); + virtual ~SaveLoadChooserDialog(); virtual void open(); + virtual void close(); virtual void reflowLayout(); virtual void handleCommand(CommandSender *sender, uint32 cmd, uint32 data); +#ifdef USE_LIBCURL + virtual void runSaveSync(bool hasSavepathOverride); +#endif + + virtual void handleTickle(); + #ifndef DISABLE_SAVELOADCHOOSER_GRID virtual SaveLoadChooserType getType() const = 0; #endif // !DISABLE_SAVELOADCHOOSER_GRID @@ -70,6 +97,19 @@ public: protected: virtual int runIntern() = 0; + /** Common function to refresh the list on the screen. */ + virtual void updateSaveList(); + + /** + * Common function to get saves list from MetaEngine. + * + * It also checks whether there are some locked saves + * because of saves sync and adds such saves as locked + * slots. User sees these slots, but is unable to save + * or load from these. + */ + virtual void listSaves(); + const bool _saveMode; const MetaEngine *_metaEngine; bool _delSupport; @@ -78,6 +118,8 @@ protected: bool _saveDateSupport; bool _playTimeSupport; Common::String _target; + bool _dialogWasShown; + SaveStateList _saveList; #ifndef DISABLE_SAVELOADCHOOSER_GRID ButtonWidget *_listButton; @@ -106,6 +148,8 @@ public: virtual void open(); virtual void close(); +protected: + virtual void updateSaveList(); private: virtual int runIntern(); @@ -118,10 +162,8 @@ private: StaticTextWidget *_time; StaticTextWidget *_playtime; - SaveStateList _saveList; String _resultString; - void updateSaveList(); void updateSelection(bool redraw); }; @@ -164,13 +206,13 @@ public: protected: virtual void handleCommand(CommandSender *sender, uint32 cmd, uint32 data); virtual void handleMouseWheel(int x, int y, int direction); + virtual void updateSaveList(); private: virtual int runIntern(); uint _columns, _lines; uint _entriesPerPage; uint _curPage; - SaveStateList _saveList; ButtonWidget *_nextButton; ButtonWidget *_prevButton; diff --git a/gui/saveload.cpp b/gui/saveload.cpp index b94d30289b..3072aa6082 100644 --- a/gui/saveload.cpp +++ b/gui/saveload.cpp @@ -87,6 +87,10 @@ int SaveLoadChooser::runModalWithPluginAndTarget(const EnginePlugin *plugin, con if (!_impl) return -1; +#ifdef USE_LIBCURL + _impl->runSaveSync(ConfMan.hasKey("savepath", target)); +#endif + // Set up the game domain as newly active domain, so // target specific savepath will be checked String oldDomain = ConfMan.getActiveDomainName(); diff --git a/gui/storagewizarddialog.cpp b/gui/storagewizarddialog.cpp new file mode 100644 index 0000000000..60f31d1e38 --- /dev/null +++ b/gui/storagewizarddialog.cpp @@ -0,0 +1,356 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "gui/storagewizarddialog.h" +#include "gui/gui-manager.h" +#include "gui/message.h" +#include "gui/widget.h" +#include "gui/widgets/edittext.h" +#include "gui/widgets/scrollcontainer.h" +#include "backends/cloud/cloudmanager.h" +#ifdef USE_SDL_NET +#include "backends/networking/sdl_net/localwebserver.h" +#endif +#include "backends/networking/browser/openurl.h" +#include "common/translation.h" + +namespace GUI { + +enum { + kConnectCmd = 'Cnnt', + kCodeBoxCmd = 'CdBx', + kOpenUrlCmd = 'OpUr', + kPasteCodeCmd = 'PsCd', + kStorageWizardContainerReflowCmd = 'SWCr' +}; + +StorageWizardDialog::StorageWizardDialog(uint32 storageId): + Dialog("GlobalOptions_Cloud_ConnectionWizard"), _storageId(storageId), _close(false) { +#ifdef USE_SDL_NET + _stopServerOnClose = false; +#endif + _backgroundType = GUI::ThemeEngine::kDialogBackgroundPlain; + + ScrollContainerWidget *container = new ScrollContainerWidget(this, "GlobalOptions_Cloud_ConnectionWizard.Container", kStorageWizardContainerReflowCmd); + container->setTarget(this); + + Common::String headline = Common::String::format(_("%s Storage Connection Wizard"), CloudMan.listStorages()[_storageId].c_str()); + _headlineWidget = new StaticTextWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.Headline", headline); + + _navigateLineWidget = new StaticTextWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.NavigateLine", _s("Navigate to the following URL:")); + _urlLineWidget = new StaticTextWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.URLLine", getUrl()); + + _returnLine1 = new StaticTextWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.ReturnLine1", _s("Obtain the code from the storage, enter it")); + _returnLine2 = new StaticTextWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.ReturnLine2", _s("in the following field and press 'Connect':")); + for (uint32 i = 0; i < CODE_FIELDS; ++i) + _codeWidget[i] = new EditTextWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.CodeBox" + Common::String::format("%d", i+1), "", 0, kCodeBoxCmd); + _messageWidget = new StaticTextWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.MessageLine", ""); + + // Buttons + _cancelWidget = new ButtonWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.CancelButton", _("Cancel"), 0, kCloseCmd); + _openUrlWidget = new ButtonWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.OpenUrlButton", _("Open URL"), 0, kOpenUrlCmd); + _pasteCodeWidget = new ButtonWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.PasteCodeButton", _("Paste"), _("Pastes clipboard contents into fields"), kPasteCodeCmd); + _connectWidget = new ButtonWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.ConnectButton", _("Connect"), 0, kConnectCmd); + + if (Cloud::CloudManager::couldUseLocalServer()) { + // hide fields and even the button if local webserver is on + _returnLine1->setLabel(_s("You would be navigated to ScummVM's page")); + _returnLine2->setLabel(_s("when you'd allow it to use your storage.")); + } + + _picture = new GraphicsWidget(container, "GlobalOptions_Cloud_ConnectionWizard_Container.Picture"); +#ifndef DISABLE_FANCY_THEMES + if (g_gui.theme()->supportsImages()) { + _picture->useThemeTransparency(true); + switch (_storageId) { + case Cloud::kStorageDropboxId: + _picture->setGfx(g_gui.theme()->getImageSurface(ThemeEngine::kImageDropboxLogo)); + break; + case Cloud::kStorageOneDriveId: + _picture->setGfx(g_gui.theme()->getImageSurface(ThemeEngine::kImageOneDriveLogo)); + break; + case Cloud::kStorageGoogleDriveId: + _picture->setGfx(g_gui.theme()->getImageSurface(ThemeEngine::kImageGoogleDriveLogo)); + break; + case Cloud::kStorageBoxId: + _picture->setGfx(g_gui.theme()->getImageSurface(ThemeEngine::kImageBoxLogo)); + break; + } + } +#endif + + containerWidgetsReflow(); +} + +void StorageWizardDialog::open() { + Dialog::open(); + + if (CloudMan.isWorking()) { + bool doClose = true; + + MessageDialog alert(_("The other Storage is working. Do you want to interrupt it?"), _("Yes"), _("No")); + if (alert.runModal() == GUI::kMessageOK) { + if (CloudMan.isDownloading()) + CloudMan.cancelDownload(); + if (CloudMan.isSyncing()) + CloudMan.cancelSync(); + + // I believe it still would return `true` here, but just in case + if (CloudMan.isWorking()) { + MessageDialog alert2(_("Wait until current Storage finishes up and try again.")); + alert2.runModal(); + } else { + doClose = false; + } + } + + if (doClose) { + close(); + return; + } + } + +#ifdef USE_SDL_NET + if (Cloud::CloudManager::couldUseLocalServer()) { + _stopServerOnClose = !LocalServer.isRunning(); + LocalServer.start(true); // using "minimal mode" (no "/files", "/download", etc available) + LocalServer.indexPageHandler().setTarget(this); + } +#endif +} + +void StorageWizardDialog::close() { +#ifdef USE_SDL_NET + if (Cloud::CloudManager::couldUseLocalServer()) { + if (_stopServerOnClose) + LocalServer.stopOnIdle(); + LocalServer.indexPageHandler().setTarget(nullptr); + } +#endif + Dialog::close(); +} + +void StorageWizardDialog::handleCommand(CommandSender *sender, uint32 cmd, uint32 data) { + switch (cmd) { + case kCodeBoxCmd: { + Common::String code, message; + uint32 correctFields = 0; + for (uint32 i = 0; i < CODE_FIELDS; ++i) { + Common::String subcode = _codeWidget[i]->getEditString(); + if (subcode.size() == 0) { + ++correctFields; + continue; + } + bool correct = correctChecksum(subcode); + if (correct) { + code += subcode; + code.deleteLastChar(); + ++correctFields; + } else { + if (i == correctFields) { //first incorrect field + message += Common::String::format("#%d", i + 1); + } else { + message += Common::String::format(", #%d", i + 1); + } + } + } + + if (message.size() > 0) { + Common::String messageTemplate; + if (CODE_FIELDS - correctFields == 1) + messageTemplate = _("Field %s has a mistake in it."); + else + messageTemplate = _("Fields %s have mistakes in them."); + message = Common::String::format(messageTemplate.c_str(), message.c_str()); + } + + bool ok = false; + if (correctFields == CODE_FIELDS && code.size() > 0) { + //the last 3 chars must be an encoded crc16 + if (code.size() > 3) { + uint32 size = code.size(); + uint32 gotcrc = decodeHashchar(code[size - 3]) | (decodeHashchar(code[size - 2]) << 6) | (decodeHashchar(code[size - 1]) << 12); + code.erase(size - 3); + uint32 crc = crc16(code); + ok = (crc == gotcrc); + } + if (ok) + message = _("All OK!"); + else + message = _("Invalid code"); + } + _connectWidget->setEnabled(ok); + _messageWidget->setLabel(message); + break; + } + case kOpenUrlCmd: { + if (!Networking::Browser::openUrl(getUrl())) { + MessageDialog alert(_("Failed to open URL!\nYou should navigate there manually then.")); + alert.runModal(); + } + break; + } + case kPasteCodeCmd: { + if (g_system->hasTextInClipboard()) { + Common::String message = g_system->getTextFromClipboard(); + for (uint32 i = 0; i < CODE_FIELDS; ++i) { + if (message.empty()) break; + Common::String subcode = ""; + for (uint32 j = 0; j < message.size(); ++j) { + if (message[j] == ' ') { + message.erase(0, j+1); + break; + } + subcode += message[j]; + if (j+1 == message.size()) { + message = ""; + break; + } + } + _codeWidget[i]->setEditString(subcode); + } + handleCommand(sender, kCodeBoxCmd, data); + draw(); + } + break; + } + case kConnectCmd: { + Common::String code; + for (uint32 i = 0; i < CODE_FIELDS; ++i) { + Common::String subcode = _codeWidget[i]->getEditString(); + if (subcode.size() == 0) + continue; + code += subcode; + code.deleteLastChar(); + } + code.erase(code.size() - 3); + CloudMan.connectStorage(_storageId, code); + setResult(1); + close(); + break; + } +#ifdef USE_SDL_NET + case kStorageCodePassedCmd: + CloudMan.connectStorage(_storageId, LocalServer.indexPageHandler().code()); + _close = true; + break; +#endif + case kStorageWizardContainerReflowCmd: + containerWidgetsReflow(); + break; + default: + Dialog::handleCommand(sender, cmd, data); + } +} + +void StorageWizardDialog::handleTickle() { + if (_close) { + setResult(1); + close(); + } + + Dialog::handleTickle(); +} + +void StorageWizardDialog::containerWidgetsReflow() { + // contents + if (_headlineWidget) _headlineWidget->setVisible(true); + if (_navigateLineWidget) _navigateLineWidget->setVisible(true); + if (_urlLineWidget) _urlLineWidget->setVisible(true); + if (_returnLine1) _returnLine1->setVisible(true); + if (_returnLine2) _returnLine2->setVisible(true); + + bool showFields = (!Cloud::CloudManager::couldUseLocalServer()); + for (uint32 i = 0; i < CODE_FIELDS; ++i) + _codeWidget[i]->setVisible(showFields); + _messageWidget->setVisible(showFields); + + // left column / first bottom row + if (_picture) { + _picture->setVisible(g_system->getOverlayWidth() > 320); + } + if (_openUrlWidget) _openUrlWidget->setVisible(true); + if (_pasteCodeWidget) { + bool visible = showFields && g_system->hasFeature(OSystem::kFeatureClipboardSupport); + _pasteCodeWidget->setVisible(visible); + } + + // bottom row + if (_cancelWidget) _cancelWidget->setVisible(true); + if (_connectWidget) { + _connectWidget->setVisible(showFields); + } +} + +Common::String StorageWizardDialog::getUrl() const { + Common::String url = "https://www.scummvm.org/c/"; + switch (_storageId) { + case Cloud::kStorageDropboxId: + url += "db"; + break; + case Cloud::kStorageOneDriveId: + url += "od"; + break; + case Cloud::kStorageGoogleDriveId: + url += "gd"; + break; + case Cloud::kStorageBoxId: + url += "bx"; + break; + } + + if (Cloud::CloudManager::couldUseLocalServer()) + url += "s"; + + return url; +} + +int StorageWizardDialog::decodeHashchar(char c) { + const char HASHCHARS[65] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ?!"; + for (uint32 i = 0; i < 64; ++i) + if (c == HASHCHARS[i]) + return i; + return -1; +} + +bool StorageWizardDialog::correctChecksum(Common::String s) { + if (s.size() == 0) + return false; //no last char + int providedChecksum = decodeHashchar(s.lastChar()); + int calculatedChecksum = 0x2A; //any initial value would do, but it must equal to the one used on the page where these checksums were generated + for (uint32 i = 0; i < s.size()-1; ++i) { + calculatedChecksum = calculatedChecksum ^ s[i]; + } + return providedChecksum == (calculatedChecksum % 64); +} + +uint32 StorageWizardDialog::crc16(Common::String s) { //"CRC16_CCITT_FALSE" + uint32 crc = 0xFFFF, x; + for (uint32 i = 0; i < s.size(); ++i) { + x = ((crc >> 8) ^ s[i]) & 0xFF; + x ^= x >> 4; + crc = ((crc << 8) ^ (x << 12) ^ (x << 5) ^ x) & 0xFFFF; + } + return crc; +} + +} // End of namespace GUI diff --git a/gui/storagewizarddialog.h b/gui/storagewizarddialog.h new file mode 100644 index 0000000000..61bc8ac873 --- /dev/null +++ b/gui/storagewizarddialog.h @@ -0,0 +1,107 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef GUI_STORAGEWIZARDDIALOG_H +#define GUI_STORAGEWIZARDDIALOG_H + +#include "gui/dialog.h" +#include "common/str.h" + +namespace GUI { + +class CommandSender; +class EditTextWidget; +class StaticTextWidget; +class ButtonWidget; +class GraphicsWidget; + +#ifdef USE_SDL_NET +enum StorageWizardDialogCommands { + kStorageCodePassedCmd = 'SWDC' +}; +#endif + +class StorageWizardDialog : public Dialog { + static const uint32 CODE_FIELDS = 8; + uint32 _storageId; + + StaticTextWidget *_headlineWidget; + StaticTextWidget *_navigateLineWidget; + StaticTextWidget *_urlLineWidget; + StaticTextWidget *_returnLine1; + StaticTextWidget *_returnLine2; + EditTextWidget *_codeWidget[CODE_FIELDS]; + StaticTextWidget *_messageWidget; + + GraphicsWidget *_picture; + ButtonWidget *_openUrlWidget; + ButtonWidget *_pasteCodeWidget; + + ButtonWidget *_cancelWidget; + ButtonWidget *_connectWidget; + + bool _close; +#ifdef USE_SDL_NET + bool _stopServerOnClose; +#endif + + /** Hides/shows widgets for Container to work with them correctly. */ + void containerWidgetsReflow(); + + /** Return short scummvm.org URL for user to navigate to. */ + Common::String getUrl() const; + + /** + * Return the value corresponding to the given character. + * + * There is a value corresponding to each of 64 selected + * printable characters (0-9, A-Z, a-z, ? and !). + * + * When given another character, -1 is returned. + */ + int decodeHashchar(char c); + + /** + * Return whether checksum is correct. + * + * The last character of the string is treated as + * the checksum of all the others (decoded with + * decodeHashchar()). + * + * Checksum = (c[0] ^ c[1] ^ ...) % 64 + */ + bool correctChecksum(Common::String s); + + /** The "CRC16_CCITT_FALSE" CRC-16 algorithm. */ + uint32 crc16(Common::String s); +public: + StorageWizardDialog(uint32 storageId); + + virtual void open(); + virtual void close(); + virtual void handleCommand(CommandSender *sender, uint32 cmd, uint32 data); + virtual void handleTickle(); +}; + +} // End of namespace GUI + +#endif diff --git a/gui/themes/default.inc b/gui/themes/default.inc index c0ea733de8..d46a603830 100644 --- a/gui/themes/default.inc +++ b/gui/themes/default.inc @@ -1092,6 +1092,192 @@ const char *defaultXML1 = "<?xml version = '1.0'?>" "/>" "</layout>" "</dialog>" +"<dialog name='GlobalOptions_Cloud' overlays='Dialog.GlobalOptions.TabWidget'>" +"<layout type='vertical' padding='0,0,0,0'>" +"<widget name='Container'/>" +"</layout>" +"</dialog>" +"<dialog name='GlobalOptions_Cloud_Container' overlays='GlobalOptions_Cloud.Container'>" +"<layout type='vertical' padding='16,16,16,16' spacing='8'>" +"<layout type='horizontal' padding='0,0,0,0' spacing='10' center='true'>" +"<widget name='StoragePopupDesc' " +"type='OptionsLabel' " +"/>" +"<widget name='StoragePopup' " +"type='PopUp' " +"/>" +"</layout>" +"<layout type='horizontal' padding='0,0,0,0' spacing='10' center='true'>" +"<widget name='StorageUsernameDesc' " +"type='OptionsLabel' " +"/>" +"<widget name='StorageUsernameLabel' " +"height='Globals.Line.Height' " +"/>" +"</layout>" +"<layout type='horizontal' padding='0,0,0,0' spacing='10' center='true'>" +"<widget name='StorageUsedSpaceDesc' " +"type='OptionsLabel' " +"/>" +"<widget name='StorageUsedSpaceLabel' " +"height='Globals.Line.Height' " +"/>" +"</layout>" +"<layout type='horizontal' padding='0,0,0,0' spacing='10' center='true'>" +"<widget name='StorageLastSyncDesc' " +"type='OptionsLabel' " +"/>" +"<widget name='StorageLastSyncLabel' " +"height='Globals.Line.Height' " +"/>" +"</layout>" +"<layout type='horizontal' padding='0,0,0,0' spacing='10' center='true'>" +"<widget name='ConnectButton' " +"type='Button' " +"/>" +"<widget name='RefreshButton' " +"type='Button' " +"/>" +"<widget name='DownloadButton' " +"type='Button' " +"/>" +"</layout>" +"<layout type='horizontal' padding='0,0,0,0' spacing='10' center='true'>" +"<widget name='RunServerButton' " +"type='Button' " +"/>" +"<widget name='ServerInfoLabel' " +"height='Globals.Line.Height' " +"/>" +"</layout>" +"<layout type='horizontal' padding='0,0,0,0' spacing='10' center='true'>" +"<widget name='ServerPortDesc' " +"type='OptionsLabel' " +"/>" +"<widget name='ServerPortEditText' " +"width='70' " +"height='Globals.Line.Height' " +"/>" +"<widget name='ServerPortClearButton' " +"height='Globals.Line.Height' " +"width='Globals.Line.Height' " +"/>" +"</layout>" +"</layout>" +"</dialog>" +"<dialog name='GlobalOptions_Cloud_DownloadDialog' overlays='Dialog.GlobalOptions'>" +"<layout type='vertical' padding='16,16,16,8' spacing='8'>" +"<widget name='RemoteDirectory' " +"height='Globals.Line.Height' " +"/>" +"<widget name='LocalDirectory' " +"height='Globals.Line.Height' " +"/>" +"<widget name='ProgressBar' " +"height='Globals.Button.Height' " +"/>" +"<space size='1'/>" +"<widget name='PercentText' " +"height='Globals.Line.Height' " +"textalign='center' " +"/>" +"<widget name='DownloadSize' " +"height='Globals.Line.Height' " +"/>" +"<widget name='DownloadSpeed' " +"height='Globals.Line.Height' " +"/>" +"<space/>" +"<layout type='horizontal' padding='0,0,0,0' spacing='10'>" +"<widget name='MainButton' " +"type='Button' " +"/>" +"<space/>" +"<widget name='CloseButton' " +"type='Button' " +"/>" +"</layout>" +"</layout>" +"</dialog>" +"<dialog name='GlobalOptions_Cloud_ConnectionWizard' overlays='Dialog.GlobalOptions'>" +"<layout type='vertical' padding='16,16,16,16' spacing='8'>" +"<layout type='horizontal' padding='0,0,0,0' spacing='10' center='true'>" +"<widget name='Picture' " +"type='OptionsLabel' " +"/>" +"<layout type='vertical' padding='0,0,0,0' spacing='6'>" +"<widget name='Headline' " +"height='Globals.Line.Height' " +"/>" +"<space size='4' />" +"<widget name='NavigateLine' " +"height='Globals.Line.Height' " +"/>" +"<widget name='URLLine' " +"height='Globals.Line.Height' " +"/>" +"<space size='4' />" +"<widget name='ReturnLine1' " +"height='Globals.Line.Height' " +"/>" +"<widget name='ReturnLine2' " +"height='Globals.Line.Height' " +"/>" +"<layout type='horizontal' padding='0,0,0,0' spacing='4' center='true'>" +"<widget name='CodeBox1' " +"width='70' " +"height='Globals.Line.Height' " +"/>" +"<widget name='CodeBox2' " +"width='70' " +"height='Globals.Line.Height' " +"/>" +"<widget name='CodeBox3' " +"width='70' " +"height='Globals.Line.Height' " +"/>" +"<widget name='CodeBox4' " +"width='70' " +"height='Globals.Line.Height' " +"/>" +"</layout>" +"<layout type='horizontal' padding='0,0,0,0' spacing='4' center='true'>" +"<widget name='CodeBox5' " +"width='70' " +"height='Globals.Line.Height' " +"/>" +"<widget name='CodeBox6' " +"width='70' " +"height='Globals.Line.Height' " +"/>" +"<widget name='CodeBox7' " +"width='70' " +"height='Globals.Line.Height' " +"/>" +"<widget name='CodeBox8' " +"width='70' " +"height='Globals.Line.Height' " +"/>" +"</layout>" +"<widget name='MessageLine' " +"height='Globals.Line.Height' " +"/>" +"<space size='6' />" +"</layout>" +"</layout>" +"<layout type='horizontal' padding='0,0,0,0' spacing='10' center='true'>" +"<widget name='CancelButton' " +"type='Button' " +"/>" +"<widget name='OpenUrlButton' " +"type='Button' " +"/>" +"<widget name='ConnectButton' " +"type='Button' " +"/>" +"</layout>" +"</layout>" +"</dialog>" "<dialog name='KeysDialog' overlays='Dialog.GlobalOptions' shading='dim'>" "<layout type='vertical' padding='8,8,8,8' center='true'>" "<widget name='Action' " @@ -1592,6 +1778,37 @@ const char *defaultXML1 = "<?xml version = '1.0'?>" "</layout>" "</layout>" "</dialog>" +"<dialog name='SaveLoadCloudSyncProgress' overlays='screen_center' inset='8' shading='dim'>" +"<layout type='vertical' padding='8,8,8,8' center='true'>" +"<widget name='TitleText' " +"width='496' " +"height='Globals.Line.Height' " +"textalign='center' " +"/>" +"<space size='1'/>" +"<widget name='ProgressBar' " +"width='496' " +"height='Globals.Button.Height' " +"/>" +"<space size='1'/>" +"<widget name='PercentText' " +"width='496' " +"height='Globals.Line.Height' " +"textalign='center' " +"/>" +"<space size='1'/>" +"<layout type='horizontal' padding='0,0,0,0' center='true' spacing='10'>" +"<widget name='Cancel' " +"width='150' " +"height='Globals.Button.Height' " +"/>" +"<widget name='Background' " +"width='150' " +"height='Globals.Button.Height' " +"/>" +"</layout>" +"</layout>" +"</dialog>" "<dialog name='SavenameDialog' overlays='screen_center'>" "<layout type='vertical' padding='8,8,8,8'>" "<widget name='DescriptionText' " @@ -2394,6 +2611,197 @@ const char *defaultXML1 = "<?xml version = '1.0'?>" "/>" "</layout>" "</dialog>" +"<dialog name='GlobalOptions_Cloud' overlays='Dialog.GlobalOptions.TabWidget'>" +"<layout type='vertical' padding='0,0,0,0'>" +"<widget name='Container'/>" +"</layout>" +"</dialog>" +"<dialog name='GlobalOptions_Cloud_Container' overlays='GlobalOptions_Cloud.Container'>" +"<layout type='vertical' padding='16,16,16,16' spacing='8'>" +"<layout type='horizontal' padding='0,0,0,0' spacing='6' center='true'>" +"<widget name='StoragePopupDesc' " +"width='80' " +"height='Globals.Line.Height' " +"textalign='right' " +"/>" +"<widget name='StoragePopup' " +"type='PopUp' " +"/>" +"</layout>" +"<layout type='horizontal' padding='0,0,0,0' spacing='6' center='true'>" +"<widget name='StorageUsernameDesc' " +"width='80' " +"height='Globals.Line.Height' " +"textalign='right' " +"/>" +"<widget name='StorageUsernameLabel' " +"height='Globals.Line.Height' " +"/>" +"</layout>" +"<layout type='horizontal' padding='0,0,0,0' spacing='6' center='true'>" +"<widget name='StorageUsedSpaceDesc' " +"width='80' " +"height='Globals.Line.Height' " +"textalign='right' " +"/>" +"<widget name='StorageUsedSpaceLabel' " +"height='Globals.Line.Height' " +"/>" +"</layout>" +"<layout type='horizontal' padding='0,0,0,0' spacing='6' center='true'>" +"<widget name='StorageLastSyncDesc' " +"width='80' " +"height='Globals.Line.Height' " +"textalign='right' " +"/>" +"<widget name='StorageLastSyncLabel' " +"height='Globals.Line.Height' " +"/>" +"</layout>" +"<layout type='horizontal' padding='0,0,0,0' spacing='6' center='true'>" +"<widget name='ConnectButton' " +"type='Button' " +"/>" +"<widget name='RefreshButton' " +"type='Button' " +"/>" +"<widget name='DownloadButton' " +"type='Button' " +"/>" +"</layout>" +"<layout type='horizontal' padding='0,0,0,0' spacing='6' center='true'>" +"<widget name='RunServerButton' " +"type='Button' " +"/>" +"<widget name='ServerInfoLabel' " +"height='Globals.Line.Height' " +"/>" +"</layout>" +"<layout type='horizontal' padding='0,0,0,0' spacing='6' center='true'>" +"<widget name='ServerPortDesc' " +"width='80' " +"height='Globals.Line.Height' " +"textalign='right' " +"/>" +"<widget name='ServerPortEditText' " +"width='60' " +"height='16' " +"/>" +"<widget name='ServerPortClearButton' " +"height='Globals.Line.Height' " +"width='Globals.Line.Height' " +"/>" +"</layout>" +"</layout>" +"</dialog>" +"<dialog name='GlobalOptions_Cloud_DownloadDialog' overlays='Dialog.GlobalOptions'>" +"<layout type='vertical' padding='8,8,8,4' spacing='8'>" +"<widget name='RemoteDirectory' " +"height='Globals.Line.Height' " +"/>" +"<widget name='LocalDirectory' " +"height='Globals.Line.Height' " +"/>" +"<widget name='ProgressBar' " +"height='Globals.Button.Height' " +"/>" +"<space size='1'/>" +"<widget name='PercentText' " +"height='Globals.Line.Height' " +"textalign='center' " +"/>" +"<widget name='DownloadSize' " +"height='Globals.Line.Height' " +"/>" +"<widget name='DownloadSpeed' " +"height='Globals.Line.Height' " +"/>" +"<space/>" +"<layout type='horizontal' padding='0,0,0,0' spacing='6'>" +"<widget name='MainButton' " +"type='Button' " +"/>" +"<space/>" +"<widget name='CloseButton' " +"type='Button' " +"/>" +"</layout>" +"</layout>" +"</dialog>" +"<dialog name='GlobalOptions_Cloud_ConnectionWizard' overlays='Dialog.GlobalOptions'>" +"<layout type='vertical' padding='16,16,16,16' spacing='8'>" +"<layout type='vertical' padding='0,0,0,0' spacing='4'>" +"<widget name='Headline' " +"height='Globals.Line.Height' " +"/>" +"<space size='2' />" +"<widget name='NavigateLine' " +"height='Globals.Line.Height' " +"/>" +"<widget name='URLLine' " +"height='Globals.Line.Height' " +"/>" +"<space size='2' />" +"<widget name='ReturnLine1' " +"height='Globals.Line.Height' " +"/>" +"<widget name='ReturnLine2' " +"height='Globals.Line.Height' " +"/>" +"<layout type='horizontal' padding='0,0,0,0' spacing='4' center='true'>" +"<widget name='CodeBox1' " +"width='60' " +"height='16' " +"/>" +"<widget name='CodeBox2' " +"width='60' " +"height='16' " +"/>" +"<widget name='CodeBox3' " +"width='60' " +"height='16' " +"/>" +"<widget name='CodeBox4' " +"width='60' " +"height='16' " +"/>" +"</layout>" +"<layout type='horizontal' padding='0,0,0,0' spacing='4' center='true'>" +"<widget name='CodeBox5' " +"width='60' " +"height='16' " +"/>" +"<widget name='CodeBox6' " +"width='60' " +"height='16' " +"/>" +"<widget name='CodeBox7' " +"width='60' " +"height='16' " +"/>" +"<widget name='CodeBox8' " +"width='60' " +"height='16' " +"/>" +"</layout>" +"<widget name='MessageLine' " +"height='Globals.Line.Height' " +"/>" +"<space size='4' />" +"</layout>" +"<layout type='horizontal' padding='0,0,0,0' spacing='6' center='true'>" +"<widget name='CancelButton' " +"type='Button' " +"/>" +"<widget name='OpenUrlButton' " +"type='Button' " +"/>" +"<widget name='ConnectButton' " +"type='Button' " +"/>" +"</layout>" +"</layout>" +"</dialog>" "<dialog name='KeysDialog' overlays='Dialog.GlobalOptions' shading='dim'>" "<layout type='vertical' padding='8,8,8,8' center='true'>" "<widget name='Action' " @@ -2888,6 +3296,37 @@ const char *defaultXML1 = "<?xml version = '1.0'?>" "</layout>" "</layout>" "</dialog>" +"<dialog name='SaveLoadCloudSyncProgress' overlays='screen_center' inset='8' shading='dim'>" +"<layout type='vertical' padding='8,8,8,8' center='true'>" +"<widget name='TitleText' " +"width='240' " +"height='Globals.Line.Height' " +"textalign='center' " +"/>" +"<space size='1'/>" +"<widget name='ProgressBar' " +"width='240' " +"height='Globals.Button.Height' " +"/>" +"<space size='1'/>" +"<widget name='PercentText' " +"width='240' " +"height='Globals.Line.Height' " +"textalign='center' " +"/>" +"<space size='1'/>" +"<layout type='horizontal' padding='0,0,0,0' center='true' spacing='10'>" +"<widget name='Cancel' " +"width='100' " +"height='Globals.Button.Height' " +"/>" +"<widget name='Background' " +"width='100' " +"height='Globals.Button.Height' " +"/>" +"</layout>" +"</layout>" +"</dialog>" "<dialog name='SavenameDialog' overlays='screen_center'>" "<layout type='vertical' padding='8,8,8,8'>" "<widget name='DescriptionText' " diff --git a/gui/themes/scummclassic.zip b/gui/themes/scummclassic.zip Binary files differindex 561f2a5dd3..e574fe5039 100644 --- a/gui/themes/scummclassic.zip +++ b/gui/themes/scummclassic.zip diff --git a/gui/themes/scummclassic/classic_gfx.stx b/gui/themes/scummclassic/classic_gfx.stx index 49ecea2e11..1311345621 100644 --- a/gui/themes/scummclassic/classic_gfx.stx +++ b/gui/themes/scummclassic/classic_gfx.stx @@ -192,7 +192,7 @@ orientation = 'top' /> </drawdata> - + <drawdata id = 'scrollbar_button_idle' cache = 'false' resolution = 'y<400'> <drawstep func = 'bevelsq' bevel = '2' @@ -226,7 +226,7 @@ orientation = 'top' /> </drawdata> - + <drawdata id = 'scrollbar_button_hover' cache = 'false' resolution = 'y<400'> <drawstep func = 'bevelsq' bevel = '2' @@ -314,7 +314,7 @@ bevel = '2' fill = 'none' /> - + <drawstep func = 'triangle' fg_color = 'green' fill = 'foreground' @@ -325,7 +325,7 @@ padding = '0, 0, 7, 0' orientation = 'bottom' /> - + <drawstep func = 'triangle' fg_color = 'green' fill = 'foreground' @@ -336,20 +336,20 @@ padding = '0, 0, 7, 0' orientation = 'top' /> - + <text font = 'text_default' text_color = 'color_normal' vertical_align = 'center' horizontal_align = 'left' /> </drawdata> - + <drawdata id = 'popup_idle' cache = 'false' resolution = 'y<400'> <drawstep func = 'bevelsq' bevel = '2' fill = 'none' /> - + <drawstep func = 'triangle' fg_color = 'green' fill = 'foreground' @@ -360,7 +360,7 @@ padding = '0, 0, 3, 0' orientation = 'bottom' /> - + <drawstep func = 'triangle' fg_color = 'green' fill = 'foreground' @@ -371,7 +371,7 @@ padding = '0, 0, 3, 0' orientation = 'top' /> - + <text font = 'text_default' text_color = 'color_normal' vertical_align = 'center' @@ -394,7 +394,7 @@ padding = '0, 0, 7, 0' orientation = 'bottom' /> - + <drawstep func = 'triangle' fg_color = 'green' fill = 'foreground' @@ -411,13 +411,13 @@ horizontal_align = 'left' /> </drawdata> - + <drawdata id = 'popup_disabled' cache = 'false' resolution = 'y<400'> <drawstep func = 'bevelsq' bevel = '2' fill = 'none' /> - + <drawstep func = 'triangle' fg_color = 'green' fill = 'foreground' @@ -428,7 +428,7 @@ padding = '0, 0, 3, 0' orientation = 'bottom' /> - + <drawstep func = 'triangle' fg_color = 'green' fill = 'foreground' @@ -439,7 +439,7 @@ padding = '0, 0, 3, 0' orientation = 'top' /> - + <text font = 'text_default' text_color = 'color_normal' vertical_align = 'center' @@ -462,7 +462,7 @@ padding = '0, 0, 7, 0' orientation = 'bottom' /> - + <drawstep func = 'triangle' fg_color = 'green' fill = 'foreground' @@ -479,13 +479,13 @@ horizontal_align = 'left' /> </drawdata> - + <drawdata id = 'popup_hover' cache = 'false' resolution = 'y<400'> <drawstep func = 'bevelsq' bevel = '2' fill = 'none' /> - + <drawstep func = 'triangle' fg_color = 'green' fill = 'foreground' @@ -496,7 +496,7 @@ padding = '0, 0, 3, 0' orientation = 'bottom' /> - + <drawstep func = 'triangle' fg_color = 'green' fill = 'foreground' @@ -507,7 +507,7 @@ padding = '0, 0, 3, 0' orientation = 'top' /> - + <text font = 'text_default' text_color = 'color_normal' vertical_align = 'center' @@ -552,7 +552,7 @@ fill = 'foreground' fg_color = 'green' /> - </drawdata> + </drawdata> <drawdata id = 'button_idle' cache = 'false'> <text font = 'text_button' diff --git a/gui/themes/scummclassic/classic_layout.stx b/gui/themes/scummclassic/classic_layout.stx index 5172326859..c67631ad22 100644 --- a/gui/themes/scummclassic/classic_layout.stx +++ b/gui/themes/scummclassic/classic_layout.stx @@ -524,6 +524,221 @@ </layout> </dialog> + <dialog name = 'GlobalOptions_Cloud' overlays = 'Dialog.GlobalOptions.TabWidget'> + <layout type = 'vertical' padding = '0, 0, 0, 0'> + <widget name = 'Container'/> + </layout> + </dialog> + + <dialog name = 'GlobalOptions_Cloud_Container' overlays = 'GlobalOptions_Cloud.Container'> + <layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '8'> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'> + <widget name = 'StoragePopupDesc' + type = 'OptionsLabel' + /> + <widget name = 'StoragePopup' + type = 'PopUp' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'> + <widget name = 'StorageUsernameDesc' + type = 'OptionsLabel' + /> + <widget name = 'StorageUsernameLabel' + height = 'Globals.Line.Height' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'> + <widget name = 'StorageUsedSpaceDesc' + type = 'OptionsLabel' + /> + <widget name = 'StorageUsedSpaceLabel' + height = 'Globals.Line.Height' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'> + <widget name = 'StorageLastSyncDesc' + type = 'OptionsLabel' + /> + <widget name = 'StorageLastSyncLabel' + height = 'Globals.Line.Height' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'> + <widget name = 'ConnectButton' + type = 'Button' + /> + <widget name = 'RefreshButton' + type = 'Button' + /> + <widget name = 'DownloadButton' + type = 'Button' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'> + <widget name = 'RunServerButton' + type = 'Button' + /> + <widget name = 'ServerInfoLabel' + height = 'Globals.Line.Height' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'> + <widget name = 'RootPathButton' + type = 'Button' + /> + <widget name = 'RootPath' + height = 'Globals.Line.Height' + /> + <widget name = 'RootPathClearButton' + height = 'Globals.Line.Height' + width = 'Globals.Line.Height' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'> + <widget name = 'ServerPortDesc' + type = 'OptionsLabel' + /> + <widget name = 'ServerPortEditText' + width = '70' + height = 'Globals.Line.Height' + /> + <widget name = 'ServerPortClearButton' + height = 'Globals.Line.Height' + width = 'Globals.Line.Height' + /> + </layout> + </layout> + </dialog> + + <dialog name = 'GlobalOptions_Cloud_DownloadDialog' overlays = 'Dialog.GlobalOptions'> + <layout type = 'vertical' padding = '16, 16, 16, 8' spacing = '8'> + <widget name = 'RemoteDirectory' + height = 'Globals.Line.Height' + /> + <widget name = 'LocalDirectory' + height = 'Globals.Line.Height' + /> + <widget name = 'ProgressBar' + height = 'Globals.Button.Height' + /> + <space size = '1'/> + <widget name = 'PercentText' + height = 'Globals.Line.Height' + textalign = 'center' + /> + <widget name = 'DownloadSize' + height = 'Globals.Line.Height' + /> + <widget name = 'DownloadSpeed' + height = 'Globals.Line.Height' + /> + <space/> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10'> + <widget name = 'MainButton' + type = 'Button' + /> + <space/> + <widget name = 'CloseButton' + type = 'Button' + /> + </layout> + </layout> + </dialog> + + <dialog name = 'GlobalOptions_Cloud_ConnectionWizard' overlays = 'Dialog.GlobalOptions'> + <layout type = 'vertical' padding = '0, 0, 0, 0'> + <widget name = 'Container'/> + </layout> + </dialog> + + <dialog name = 'GlobalOptions_Cloud_ConnectionWizard_Container' overlays = 'GlobalOptions_Cloud_ConnectionWizard.Container'> + <layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '0'> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'> + <layout type = 'vertical' padding = '0, 0, 0, 0' spacing = '6'> + <widget name = 'Picture' + width = '109' + height = '109' + /> + <widget name = 'OpenUrlButton' + type = 'Button' + /> + <widget name = 'PasteCodeButton' + type = 'Button' + /> + </layout> + <layout type = 'vertical' padding = '0, 0, 0, 0' spacing = '6'> + <widget name = 'Headline' + height = 'Globals.Line.Height' + /> + <space size = '4' /> + <widget name = 'NavigateLine' + height = 'Globals.Line.Height' + /> + <widget name = 'URLLine' + height = 'Globals.Line.Height' + /> + <space size = '4' /> + <widget name = 'ReturnLine1' + height = 'Globals.Line.Height' + /> + <widget name = 'ReturnLine2' + height = 'Globals.Line.Height' + /> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'> + <widget name = 'CodeBox1' + width = '70' + height = 'Globals.Line.Height' + /> + <widget name = 'CodeBox2' + width = '70' + height = 'Globals.Line.Height' + /> + <widget name = 'CodeBox3' + width = '70' + height = 'Globals.Line.Height' + /> + <widget name = 'CodeBox4' + width = '70' + height = 'Globals.Line.Height' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'> + <widget name = 'CodeBox5' + width = '70' + height = 'Globals.Line.Height' + /> + <widget name = 'CodeBox6' + width = '70' + height = 'Globals.Line.Height' + /> + <widget name = 'CodeBox7' + width = '70' + height = 'Globals.Line.Height' + /> + <widget name = 'CodeBox8' + width = '70' + height = 'Globals.Line.Height' + /> + </layout> + <widget name = 'MessageLine' + height = 'Globals.Line.Height' + /> + <space size = '6' /> + </layout> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'> + <widget name = 'CancelButton' + type = 'Button' + /> + <space /> + <widget name = 'ConnectButton' + type = 'Button' + /> + </layout> + </layout> + </dialog> + <dialog name='KeysDialog' overlays='Dialog.GlobalOptions' shading='dim'> <layout type='vertical' padding='8,8,8,8' center='true'> <widget name='Action' @@ -1042,6 +1257,38 @@ </layout> </dialog> + <dialog name = 'SaveLoadCloudSyncProgress' overlays = 'screen_center' inset = '8' shading = 'dim'> + <layout type = 'vertical' padding = '8, 8, 8, 8' center = 'true'> + <widget name = 'TitleText' + width = '496' + height = 'Globals.Line.Height' + textalign = 'center' + /> + <space size = '1'/> + <widget name = 'ProgressBar' + width = '496' + height = 'Globals.Button.Height' + /> + <space size = '1'/> + <widget name = 'PercentText' + width = '496' + height = 'Globals.Line.Height' + textalign = 'center' + /> + <space size = '1'/> + <layout type = 'horizontal' padding = '0, 0, 0, 0' center = 'true' spacing = '10'> + <widget name = 'Cancel' + width = '150' + height = 'Globals.Button.Height' + /> + <widget name = 'Background' + width = '150' + height = 'Globals.Button.Height' + /> + </layout> + </layout> + </dialog> + <dialog name = 'SavenameDialog' overlays = 'screen_center'> <layout type = 'vertical' padding = '8, 8, 8, 8'> <widget name = 'DescriptionText' diff --git a/gui/themes/scummclassic/classic_layout_lowres.stx b/gui/themes/scummclassic/classic_layout_lowres.stx index 0013b91ee2..e19256695c 100644 --- a/gui/themes/scummclassic/classic_layout_lowres.stx +++ b/gui/themes/scummclassic/classic_layout_lowres.stx @@ -529,6 +529,226 @@ </layout> </dialog> + <dialog name = 'GlobalOptions_Cloud' overlays = 'Dialog.GlobalOptions.TabWidget'> + <layout type = 'vertical' padding = '0, 0, 0, 0'> + <widget name = 'Container'/> + </layout> + </dialog> + + <dialog name = 'GlobalOptions_Cloud_Container' overlays = 'GlobalOptions_Cloud.Container'> + <layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '8'> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'> + <widget name = 'StoragePopupDesc' + width = '80' + height = 'Globals.Line.Height' + textalign = 'right' + /> + <widget name = 'StoragePopup' + type = 'PopUp' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'> + <widget name = 'StorageUsernameDesc' + width = '80' + height = 'Globals.Line.Height' + textalign = 'right' + /> + <widget name = 'StorageUsernameLabel' + height = 'Globals.Line.Height' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'> + <widget name = 'StorageUsedSpaceDesc' + width = '80' + height = 'Globals.Line.Height' + textalign = 'right' + /> + <widget name = 'StorageUsedSpaceLabel' + height = 'Globals.Line.Height' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'> + <widget name = 'StorageLastSyncDesc' + width = '80' + height = 'Globals.Line.Height' + textalign = 'right' + /> + <widget name = 'StorageLastSyncLabel' + height = 'Globals.Line.Height' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'> + <widget name = 'ConnectButton' + type = 'Button' + /> + <widget name = 'RefreshButton' + type = 'Button' + /> + <widget name = 'DownloadButton' + type = 'Button' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'> + <widget name = 'RunServerButton' + type = 'Button' + /> + <widget name = 'ServerInfoLabel' + height = 'Globals.Line.Height' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '16' center = 'true'> + <widget name = 'RootPathButton' + type = 'Button' + /> + <widget name = 'RootPathPath' + height = 'Globals.Line.Height' + /> + <widget name = 'RootPathClearButton' + height = 'Globals.Line.Height' + width = 'Globals.Line.Height' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'> + <widget name = 'ServerPortDesc' + width = '80' + height = 'Globals.Line.Height' + textalign = 'right' + /> + <widget name = 'ServerPortEditText' + width = '60' + height = '16' + /> + <widget name = 'ServerPortClearButton' + height = 'Globals.Line.Height' + width = 'Globals.Line.Height' + /> + </layout> + </layout> + </dialog> + + <dialog name = 'GlobalOptions_Cloud_DownloadDialog' overlays = 'Dialog.GlobalOptions'> + <layout type = 'vertical' padding = '8, 8, 8, 4' spacing = '8'> + <widget name = 'RemoteDirectory' + height = 'Globals.Line.Height' + /> + <widget name = 'LocalDirectory' + height = 'Globals.Line.Height' + /> + <widget name = 'ProgressBar' + height = 'Globals.Button.Height' + /> + <space size = '1'/> + <widget name = 'PercentText' + height = 'Globals.Line.Height' + textalign = 'center' + /> + <widget name = 'DownloadSize' + height = 'Globals.Line.Height' + /> + <widget name = 'DownloadSpeed' + height = 'Globals.Line.Height' + /> + <space/> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6'> + <widget name = 'MainButton' + type = 'Button' + /> + <space/> + <widget name = 'CloseButton' + type = 'Button' + /> + </layout> + </layout> + </dialog> + + <dialog name = 'GlobalOptions_Cloud_ConnectionWizard' overlays = 'Dialog.GlobalOptions'> + <layout type = 'vertical' padding = '0, 0, 0, 0'> + <widget name = 'Container'/> + </layout> + </dialog> + + <dialog name = 'GlobalOptions_Cloud_ConnectionWizard_Container' overlays = 'GlobalOptions_Cloud_ConnectionWizard.Container'> + <layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '8'> + <layout type = 'vertical' padding = '0, 0, 0, 0' spacing = '4'> + <widget name = 'Headline' + height = 'Globals.Line.Height' + /> + <space size = '2' /> + <widget name = 'NavigateLine' + height = 'Globals.Line.Height' + /> + <widget name = 'URLLine' + height = 'Globals.Line.Height' + /> + <space size = '2' /> + <widget name = 'ReturnLine1' + height = 'Globals.Line.Height' + /> + <widget name = 'ReturnLine2' + height = 'Globals.Line.Height' + /> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'> + <widget name = 'CodeBox1' + width = '60' + height = '16' + /> + <widget name = 'CodeBox2' + width = '60' + height = '16' + /> + <widget name = 'CodeBox3' + width = '60' + height = '16' + /> + <widget name = 'CodeBox4' + width = '60' + height = '16' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'> + <widget name = 'CodeBox5' + width = '60' + height = '16' + /> + <widget name = 'CodeBox6' + width = '60' + height = '16' + /> + <widget name = 'CodeBox7' + width = '60' + height = '16' + /> + <widget name = 'CodeBox8' + width = '60' + height = '16' + /> + </layout> + <widget name = 'MessageLine' + height = 'Globals.Line.Height' + /> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'> + <widget name = 'OpenUrlButton' + type = 'Button' + /> + <widget name = 'PasteCodeButton' + type = 'Button' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'> + <widget name = 'CancelButton' + type = 'Button' + /> + <space /> + <widget name = 'ConnectButton' + type = 'Button' + /> + </layout> + <space size = '6' /> + <widget name = 'Picture' width = '1' height = '1' /> + </layout> + </layout> + </dialog> + <dialog name='KeysDialog' overlays='Dialog.GlobalOptions' shading='dim'> <layout type='vertical' padding='8,8,8,8' center='true'> <widget name='Action' @@ -1040,6 +1260,38 @@ </layout> </dialog> + <dialog name = 'SaveLoadCloudSyncProgress' overlays = 'screen_center' inset = '8' shading = 'dim'> + <layout type = 'vertical' padding = '8, 8, 8, 8' center = 'true'> + <widget name = 'TitleText' + width = '240' + height = 'Globals.Line.Height' + textalign = 'center' + /> + <space size = '1'/> + <widget name = 'ProgressBar' + width = '240' + height = 'Globals.Button.Height' + /> + <space size = '1'/> + <widget name = 'PercentText' + width = '240' + height = 'Globals.Line.Height' + textalign = 'center' + /> + <space size = '1'/> + <layout type = 'horizontal' padding = '0, 0, 0, 0' center = 'true' spacing = '10'> + <widget name = 'Cancel' + width = '100' + height = 'Globals.Button.Height' + /> + <widget name = 'Background' + width = '100' + height = 'Globals.Button.Height' + /> + </layout> + </layout> + </dialog> + <dialog name = 'SavenameDialog' overlays = 'screen_center'> <layout type = 'vertical' padding = '8, 8, 8, 8'> <widget name = 'DescriptionText' diff --git a/gui/themes/scummmodern.zip b/gui/themes/scummmodern.zip Binary files differindex d80c481ffc..78b62d4286 100644 --- a/gui/themes/scummmodern.zip +++ b/gui/themes/scummmodern.zip diff --git a/gui/themes/scummmodern/box.bmp b/gui/themes/scummmodern/box.bmp Binary files differnew file mode 100644 index 0000000000..21fb650f02 --- /dev/null +++ b/gui/themes/scummmodern/box.bmp diff --git a/gui/themes/scummmodern/dropbox.bmp b/gui/themes/scummmodern/dropbox.bmp Binary files differnew file mode 100644 index 0000000000..4ed95f0009 --- /dev/null +++ b/gui/themes/scummmodern/dropbox.bmp diff --git a/gui/themes/scummmodern/googledrive.bmp b/gui/themes/scummmodern/googledrive.bmp Binary files differnew file mode 100644 index 0000000000..30377a5f74 --- /dev/null +++ b/gui/themes/scummmodern/googledrive.bmp diff --git a/gui/themes/scummmodern/onedrive.bmp b/gui/themes/scummmodern/onedrive.bmp Binary files differnew file mode 100644 index 0000000000..cd26d71d3c --- /dev/null +++ b/gui/themes/scummmodern/onedrive.bmp diff --git a/gui/themes/scummmodern/scummmodern_gfx.stx b/gui/themes/scummmodern/scummmodern_gfx.stx index 3a1ec5a5f0..e3d2152e4b 100644 --- a/gui/themes/scummmodern/scummmodern_gfx.stx +++ b/gui/themes/scummmodern/scummmodern_gfx.stx @@ -119,6 +119,10 @@ <bitmap filename = 'editbtn_small.bmp'/> <bitmap filename = 'switchbtn_small.bmp'/> <bitmap filename = 'fastreplay_small.bmp'/> + <bitmap filename = 'dropbox.bmp'/> + <bitmap filename = 'onedrive.bmp'/> + <bitmap filename = 'googledrive.bmp'/> + <bitmap filename = 'box.bmp'/> </bitmaps> <fonts> @@ -181,7 +185,7 @@ <text_color id = 'color_button_hover' color = 'white' /> - + <text_color id = 'color_alternative_inverted' color = 'white' /> @@ -327,7 +331,7 @@ orientation = 'top' /> </drawdata> - + <drawdata id = 'scrollbar_button_hover' cache = 'false' resolution = 'y>399'> <drawstep func = 'roundedsq' radius = '10' @@ -350,7 +354,7 @@ orientation = 'top' /> </drawdata> - + <drawdata id = 'scrollbar_button_hover' cache = 'false' resolution = 'y<400'> <drawstep func = 'roundedsq' radius = '10' @@ -476,7 +480,7 @@ bg_color = 'xtrabrightred' shadow = '1' /> - + <drawstep func = 'triangle' bg_color = 'shadowcolor' fill = 'background' @@ -487,7 +491,7 @@ padding = '0, 0, 6, 0' orientation = 'bottom' /> - + <drawstep func = 'triangle' bg_color = 'shadowcolor' fill = 'background' @@ -498,14 +502,14 @@ padding = '0, 0, 6, 0' orientation = 'top' /> - + <text font = 'text_default' text_color = 'color_normal' vertical_align = 'center' horizontal_align = 'left' /> </drawdata> - + <drawdata id = 'popup_idle' cache = 'false' resolution ='y<400'> <drawstep func = 'roundedsq' radius = '5' @@ -515,7 +519,7 @@ bg_color = 'xtrabrightred' shadow = '1' /> - + <drawstep func = 'triangle' bg_color = 'shadowcolor' fill = 'background' @@ -526,7 +530,7 @@ padding = '0, 0, 3, 0' orientation = 'bottom' /> - + <drawstep func = 'triangle' bg_color = 'shadowcolor' fill = 'background' @@ -537,7 +541,7 @@ padding = '0, 0, 3, 0' orientation = 'top' /> - + <text font = 'text_default' text_color = 'color_normal' vertical_align = 'center' @@ -566,7 +570,7 @@ padding = '0, 0, 6, 0' orientation = 'bottom' /> - + <drawstep func = 'triangle' bg_color = 'shadowcolor' fill = 'background' @@ -577,14 +581,14 @@ padding = '0, 0, 6, 0' orientation = 'top' /> - + <text font = 'text_default' text_color = 'color_normal_hover' vertical_align = 'center' horizontal_align = 'left' /> </drawdata> - + <drawdata id = 'popup_disabled' cache = 'false' resolution = 'y<400'> <drawstep func = 'roundedsq' radius = '5' @@ -594,7 +598,7 @@ bg_color = 'xtrabrightred' shadow = '2' /> - + <drawstep func = 'triangle' bg_color = 'shadowcolor' fill = 'background' @@ -605,7 +609,7 @@ padding = '0, 0, 3, 0' orientation = 'bottom' /> - + <drawstep func = 'triangle' bg_color = 'shadowcolor' fill = 'background' @@ -616,7 +620,7 @@ padding = '0, 0, 3, 0' orientation = 'top' /> - + <text font = 'text_default' text_color = 'color_normal' vertical_align = 'center' @@ -645,7 +649,7 @@ padding = '0, 0, 6, 0' orientation = 'bottom' /> - + <drawstep func = 'triangle' bg_color = 'shadowcolor' fill = 'background' @@ -656,14 +660,14 @@ padding = '0, 0, 6, 0' orientation = 'top' /> - + <text font = 'text_default' text_color = 'color_normal_hover' vertical_align = 'center' horizontal_align = 'left' /> </drawdata> - + <drawdata id = 'popup_hover' cache = 'false' resolution = 'y<400'> <drawstep func = 'roundedsq' radius = '5' @@ -673,7 +677,7 @@ bg_color = 'xtrabrightred' shadow = '1' /> - + <drawstep func = 'triangle' bg_color = 'shadowcolor' fill = 'background' @@ -684,7 +688,7 @@ padding = '0, 0, 3, 0' orientation = 'bottom' /> - + <drawstep func = 'triangle' bg_color = 'shadowcolor' fill = 'background' @@ -695,7 +699,7 @@ padding = '0, 0, 3, 0' orientation = 'top' /> - + <text font = 'text_default' text_color = 'color_normal' vertical_align = 'center' @@ -777,7 +781,7 @@ bevel = '1' bevel_color = 'black' /> - </drawdata> + </drawdata> <!-- Idle button --> <drawdata id = 'button_idle' cache = 'false'> diff --git a/gui/themes/scummmodern/scummmodern_layout.stx b/gui/themes/scummmodern/scummmodern_layout.stx index 026fa7bc64..bb182c9dbb 100644 --- a/gui/themes/scummmodern/scummmodern_layout.stx +++ b/gui/themes/scummmodern/scummmodern_layout.stx @@ -538,6 +538,221 @@ </layout> </dialog> + <dialog name = 'GlobalOptions_Cloud' overlays = 'Dialog.GlobalOptions.TabWidget'> + <layout type = 'vertical' padding = '0, 0, 0, 0'> + <widget name = 'Container'/> + </layout> + </dialog> + + <dialog name = 'GlobalOptions_Cloud_Container' overlays = 'GlobalOptions_Cloud.Container'> + <layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '8'> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'> + <widget name = 'StoragePopupDesc' + type = 'OptionsLabel' + /> + <widget name = 'StoragePopup' + type = 'PopUp' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'> + <widget name = 'StorageUsernameDesc' + type = 'OptionsLabel' + /> + <widget name = 'StorageUsernameLabel' + height = 'Globals.Line.Height' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'> + <widget name = 'StorageUsedSpaceDesc' + type = 'OptionsLabel' + /> + <widget name = 'StorageUsedSpaceLabel' + height = 'Globals.Line.Height' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'> + <widget name = 'StorageLastSyncDesc' + type = 'OptionsLabel' + /> + <widget name = 'StorageLastSyncLabel' + height = 'Globals.Line.Height' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'> + <widget name = 'ConnectButton' + type = 'Button' + /> + <widget name = 'RefreshButton' + type = 'Button' + /> + <widget name = 'DownloadButton' + type = 'Button' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'> + <widget name = 'RunServerButton' + type = 'Button' + /> + <widget name = 'ServerInfoLabel' + height = 'Globals.Line.Height' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'> + <widget name = 'RootPathButton' + type = 'Button' + /> + <widget name = 'RootPath' + height = 'Globals.Line.Height' + /> + <widget name = 'RootPathClearButton' + height = 'Globals.Line.Height' + width = 'Globals.Line.Height' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'> + <widget name = 'ServerPortDesc' + type = 'OptionsLabel' + /> + <widget name = 'ServerPortEditText' + width = '70' + height = 'Globals.Line.Height' + /> + <widget name = 'ServerPortClearButton' + height = 'Globals.Line.Height' + width = 'Globals.Line.Height' + /> + </layout> + </layout> + </dialog> + + <dialog name = 'GlobalOptions_Cloud_DownloadDialog' overlays = 'Dialog.GlobalOptions'> + <layout type = 'vertical' padding = '16, 16, 16, 8' spacing = '8'> + <widget name = 'RemoteDirectory' + height = 'Globals.Line.Height' + /> + <widget name = 'LocalDirectory' + height = 'Globals.Line.Height' + /> + <widget name = 'ProgressBar' + height = 'Globals.Button.Height' + /> + <space size = '1'/> + <widget name = 'PercentText' + height = 'Globals.Line.Height' + textalign = 'center' + /> + <widget name = 'DownloadSize' + height = 'Globals.Line.Height' + /> + <widget name = 'DownloadSpeed' + height = 'Globals.Line.Height' + /> + <space/> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10'> + <widget name = 'MainButton' + type = 'Button' + /> + <space/> + <widget name = 'CloseButton' + type = 'Button' + /> + </layout> + </layout> + </dialog> + + <dialog name = 'GlobalOptions_Cloud_ConnectionWizard' overlays = 'Dialog.GlobalOptions'> + <layout type = 'vertical' padding = '0, 0, 0, 0'> + <widget name = 'Container'/> + </layout> + </dialog> + + <dialog name = 'GlobalOptions_Cloud_ConnectionWizard_Container' overlays = 'GlobalOptions_Cloud_ConnectionWizard.Container'> + <layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '0'> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'> + <layout type = 'vertical' padding = '0, 0, 0, 0' spacing = '6'> + <widget name = 'Picture' + width = '109' + height = '109' + /> + <widget name = 'OpenUrlButton' + type = 'Button' + /> + <widget name = 'PasteCodeButton' + type = 'Button' + /> + </layout> + <layout type = 'vertical' padding = '0, 0, 0, 0' spacing = '6'> + <widget name = 'Headline' + height = 'Globals.Line.Height' + /> + <space size = '4' /> + <widget name = 'NavigateLine' + height = 'Globals.Line.Height' + /> + <widget name = 'URLLine' + height = 'Globals.Line.Height' + /> + <space size = '4' /> + <widget name = 'ReturnLine1' + height = 'Globals.Line.Height' + /> + <widget name = 'ReturnLine2' + height = 'Globals.Line.Height' + /> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'> + <widget name = 'CodeBox1' + width = '70' + height = 'Globals.Line.Height' + /> + <widget name = 'CodeBox2' + width = '70' + height = 'Globals.Line.Height' + /> + <widget name = 'CodeBox3' + width = '70' + height = 'Globals.Line.Height' + /> + <widget name = 'CodeBox4' + width = '70' + height = 'Globals.Line.Height' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'> + <widget name = 'CodeBox5' + width = '70' + height = 'Globals.Line.Height' + /> + <widget name = 'CodeBox6' + width = '70' + height = 'Globals.Line.Height' + /> + <widget name = 'CodeBox7' + width = '70' + height = 'Globals.Line.Height' + /> + <widget name = 'CodeBox8' + width = '70' + height = 'Globals.Line.Height' + /> + </layout> + <widget name = 'MessageLine' + height = 'Globals.Line.Height' + /> + <space size = '6' /> + </layout> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' center = 'true'> + <widget name = 'CancelButton' + type = 'Button' + /> + <space /> + <widget name = 'ConnectButton' + type = 'Button' + /> + </layout> + </layout> + </dialog> + <dialog name='KeysDialog' overlays='Dialog.GlobalOptions' shading='dim'> <layout type='vertical' padding='8,8,8,8' center='true'> <widget name='Action' @@ -1056,6 +1271,38 @@ </layout> </dialog> + <dialog name = 'SaveLoadCloudSyncProgress' overlays = 'screen_center' inset = '8' shading = 'dim'> + <layout type = 'vertical' padding = '8, 8, 8, 8' center = 'true'> + <widget name = 'TitleText' + width = '496' + height = 'Globals.Line.Height' + textalign = 'center' + /> + <space size = '1'/> + <widget name = 'ProgressBar' + width = '496' + height = 'Globals.Button.Height' + /> + <space size = '1'/> + <widget name = 'PercentText' + width = '496' + height = 'Globals.Line.Height' + textalign = 'center' + /> + <space size = '1'/> + <layout type = 'horizontal' padding = '0, 0, 0, 0' center = 'true' spacing = '10'> + <widget name = 'Cancel' + width = '150' + height = 'Globals.Button.Height' + /> + <widget name = 'Background' + width = '150' + height = 'Globals.Button.Height' + /> + </layout> + </layout> + </dialog> + <dialog name = 'SavenameDialog' overlays = 'screen_center'> <layout type = 'vertical' padding = '8, 8, 8, 8'> <widget name = 'DescriptionText' diff --git a/gui/themes/scummmodern/scummmodern_layout_lowres.stx b/gui/themes/scummmodern/scummmodern_layout_lowres.stx index 169e61a9bb..3416fbeb5e 100644 --- a/gui/themes/scummmodern/scummmodern_layout_lowres.stx +++ b/gui/themes/scummmodern/scummmodern_layout_lowres.stx @@ -527,6 +527,226 @@ </layout> </dialog> + <dialog name = 'GlobalOptions_Cloud' overlays = 'Dialog.GlobalOptions.TabWidget'> + <layout type = 'vertical' padding = '0, 0, 0, 0'> + <widget name = 'Container'/> + </layout> + </dialog> + + <dialog name = 'GlobalOptions_Cloud_Container' overlays = 'GlobalOptions_Cloud.Container'> + <layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '8'> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'> + <widget name = 'StoragePopupDesc' + width = '80' + height = 'Globals.Line.Height' + textalign = 'right' + /> + <widget name = 'StoragePopup' + type = 'PopUp' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'> + <widget name = 'StorageUsernameDesc' + width = '80' + height = 'Globals.Line.Height' + textalign = 'right' + /> + <widget name = 'StorageUsernameLabel' + height = 'Globals.Line.Height' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'> + <widget name = 'StorageUsedSpaceDesc' + width = '80' + height = 'Globals.Line.Height' + textalign = 'right' + /> + <widget name = 'StorageUsedSpaceLabel' + height = 'Globals.Line.Height' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'> + <widget name = 'StorageLastSyncDesc' + width = '80' + height = 'Globals.Line.Height' + textalign = 'right' + /> + <widget name = 'StorageLastSyncLabel' + height = 'Globals.Line.Height' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'> + <widget name = 'ConnectButton' + type = 'Button' + /> + <widget name = 'RefreshButton' + type = 'Button' + /> + <widget name = 'DownloadButton' + type = 'Button' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'> + <widget name = 'RunServerButton' + type = 'Button' + /> + <widget name = 'ServerInfoLabel' + height = 'Globals.Line.Height' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '16' center = 'true'> + <widget name = 'RootPathButton' + type = 'Button' + /> + <widget name = 'RootPathPath' + height = 'Globals.Line.Height' + /> + <widget name = 'RootPathClearButton' + height = 'Globals.Line.Height' + width = 'Globals.Line.Height' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6' center = 'true'> + <widget name = 'ServerPortDesc' + width = '80' + height = 'Globals.Line.Height' + textalign = 'right' + /> + <widget name = 'ServerPortEditText' + width = '60' + height = '16' + /> + <widget name = 'ServerPortClearButton' + height = 'Globals.Line.Height' + width = 'Globals.Line.Height' + /> + </layout> + </layout> + </dialog> + + <dialog name = 'GlobalOptions_Cloud_DownloadDialog' overlays = 'Dialog.GlobalOptions'> + <layout type = 'vertical' padding = '8, 8, 8, 4' spacing = '8'> + <widget name = 'RemoteDirectory' + height = 'Globals.Line.Height' + /> + <widget name = 'LocalDirectory' + height = 'Globals.Line.Height' + /> + <widget name = 'ProgressBar' + height = 'Globals.Button.Height' + /> + <space size = '1'/> + <widget name = 'PercentText' + height = 'Globals.Line.Height' + textalign = 'center' + /> + <widget name = 'DownloadSize' + height = 'Globals.Line.Height' + /> + <widget name = 'DownloadSpeed' + height = 'Globals.Line.Height' + /> + <space/> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '6'> + <widget name = 'MainButton' + type = 'Button' + /> + <space/> + <widget name = 'CloseButton' + type = 'Button' + /> + </layout> + </layout> + </dialog> + + <dialog name = 'GlobalOptions_Cloud_ConnectionWizard' overlays = 'Dialog.GlobalOptions'> + <layout type = 'vertical' padding = '0, 0, 0, 0'> + <widget name = 'Container'/> + </layout> + </dialog> + + <dialog name = 'GlobalOptions_Cloud_ConnectionWizard_Container' overlays = 'GlobalOptions_Cloud_ConnectionWizard.Container'> + <layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '8'> + <layout type = 'vertical' padding = '0, 0, 0, 0' spacing = '4'> + <widget name = 'Headline' + height = 'Globals.Line.Height' + /> + <space size = '2' /> + <widget name = 'NavigateLine' + height = 'Globals.Line.Height' + /> + <widget name = 'URLLine' + height = 'Globals.Line.Height' + /> + <space size = '2' /> + <widget name = 'ReturnLine1' + height = 'Globals.Line.Height' + /> + <widget name = 'ReturnLine2' + height = 'Globals.Line.Height' + /> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'> + <widget name = 'CodeBox1' + width = '60' + height = '16' + /> + <widget name = 'CodeBox2' + width = '60' + height = '16' + /> + <widget name = 'CodeBox3' + width = '60' + height = '16' + /> + <widget name = 'CodeBox4' + width = '60' + height = '16' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'> + <widget name = 'CodeBox5' + width = '60' + height = '16' + /> + <widget name = 'CodeBox6' + width = '60' + height = '16' + /> + <widget name = 'CodeBox7' + width = '60' + height = '16' + /> + <widget name = 'CodeBox8' + width = '60' + height = '16' + /> + </layout> + <widget name = 'MessageLine' + height = 'Globals.Line.Height' + /> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'> + <widget name = 'OpenUrlButton' + type = 'Button' + /> + <widget name = 'PasteCodeButton' + type = 'Button' + /> + </layout> + <layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '4' center = 'true'> + <widget name = 'CancelButton' + type = 'Button' + /> + <space /> + <widget name = 'ConnectButton' + type = 'Button' + /> + </layout> + <space size = '6' /> + <widget name = 'Picture' width = '1' height = '1' /> + </layout> + </layout> + </dialog> + <dialog name='KeysDialog' overlays='Dialog.GlobalOptions' shading='dim'> <layout type='vertical' padding='8,8,8,8' center='true'> <widget name='Action' @@ -1038,6 +1258,38 @@ </layout> </dialog> + <dialog name = 'SaveLoadCloudSyncProgress' overlays = 'screen_center' inset = '8' shading = 'dim'> + <layout type = 'vertical' padding = '8, 8, 8, 8' center = 'true'> + <widget name = 'TitleText' + width = '240' + height = 'Globals.Line.Height' + textalign = 'center' + /> + <space size = '1'/> + <widget name = 'ProgressBar' + width = '240' + height = 'Globals.Button.Height' + /> + <space size = '1'/> + <widget name = 'PercentText' + width = '240' + height = 'Globals.Line.Height' + textalign = 'center' + /> + <space size = '1'/> + <layout type = 'horizontal' padding = '0, 0, 0, 0' center = 'true' spacing = '10'> + <widget name = 'Cancel' + width = '100' + height = 'Globals.Button.Height' + /> + <widget name = 'Background' + width = '100' + height = 'Globals.Button.Height' + /> + </layout> + </layout> + </dialog> + <dialog name = 'SavenameDialog' overlays = 'screen_center'> <layout type = 'vertical' padding = '8, 8, 8, 8'> <widget name = 'DescriptionText' diff --git a/gui/themes/scummtheme.py b/gui/themes/scummtheme.py index d5fa4dfca7..365787275b 100755 --- a/gui/themes/scummtheme.py +++ b/gui/themes/scummtheme.py @@ -5,7 +5,7 @@ import re import os import zipfile -THEME_FILE_EXTENSIONS = ('.stx', '.bmp', '.fcc', '.ttf') +THEME_FILE_EXTENSIONS = ('.stx', '.bmp', '.fcc', '.ttf', '.png') def buildTheme(themeName): if not os.path.isdir(themeName) or not os.path.isfile(os.path.join(themeName, "THEMERC")): diff --git a/gui/widget.cpp b/gui/widget.cpp index f2a29c3100..f6e6d09a8a 100644 --- a/gui/widget.cpp +++ b/gui/widget.cpp @@ -398,25 +398,28 @@ void ButtonWidget::setUnpressedState() { PicButtonWidget::PicButtonWidget(GuiObject *boss, int x, int y, int w, int h, const char *tooltip, uint32 cmd, uint8 hotkey) : ButtonWidget(boss, x, y, w, h, "", tooltip, cmd, hotkey), - _gfx(), _alpha(256), _transparency(false) { + _alpha(255), _transparency(false), _showButton(true) { setFlags(WIDGET_ENABLED/* | WIDGET_BORDER*/ | WIDGET_CLEARBG); _type = kButtonWidget; + _mode = ThemeEngine::kAutoScaleNone; } PicButtonWidget::PicButtonWidget(GuiObject *boss, const Common::String &name, const char *tooltip, uint32 cmd, uint8 hotkey) : ButtonWidget(boss, name, "", tooltip, cmd, hotkey), - _gfx(), _alpha(256), _transparency(false) { + _alpha(255), _transparency(false), _showButton(true), _isAlpha(false) { setFlags(WIDGET_ENABLED/* | WIDGET_BORDER*/ | WIDGET_CLEARBG); _type = kButtonWidget; + _mode = ThemeEngine::kAutoScaleNone; } PicButtonWidget::~PicButtonWidget() { - _gfx.free(); + for (int i = 0; i < kPicButtonStateMax + 1; i++) + _gfx[i].free(); } -void PicButtonWidget::setGfx(const Graphics::Surface *gfx) { - _gfx.free(); +void PicButtonWidget::setGfx(const Graphics::Surface *gfx, int statenum) { + _gfx[statenum].free(); if (!gfx || !gfx->getPixels()) return; @@ -432,10 +435,27 @@ void PicButtonWidget::setGfx(const Graphics::Surface *gfx) { return; } - _gfx.copyFrom(*gfx); + _gfx[statenum].copyFrom(*gfx); +} + +void PicButtonWidget::setAGfx(const Graphics::TransparentSurface *gfx, int statenum, ThemeEngine::AutoScaleMode mode) { + _agfx[statenum].free(); + + if (!gfx || !gfx->getPixels()) + return; + + if (gfx->format.bytesPerPixel == 1) { + warning("PicButtonWidget::setGfx got paletted surface passed"); + return; + } + + _agfx[statenum].copyFrom(*gfx); + + _isAlpha = true; + _mode = mode; } -void PicButtonWidget::setGfx(int w, int h, int r, int g, int b) { +void PicButtonWidget::setGfx(int w, int h, int r, int g, int b, int statenum) { if (w == -1) w = _w; if (h == -1) @@ -443,26 +463,69 @@ void PicButtonWidget::setGfx(int w, int h, int r, int g, int b) { const Graphics::PixelFormat &requiredFormat = g_gui.theme()->getPixelFormat(); - _gfx.free(); - _gfx.create(w, h, requiredFormat); - _gfx.fillRect(Common::Rect(0, 0, w, h), _gfx.format.RGBToColor(r, g, b)); + _gfx[statenum].free(); + _gfx[statenum].create(w, h, requiredFormat); + _gfx[statenum].fillRect(Common::Rect(0, 0, w, h), _gfx[statenum].format.RGBToColor(r, g, b)); } void PicButtonWidget::drawWidget() { - g_gui.theme()->drawButtonClip(Common::Rect(_x, _y, _x + _w, _y + _h), getBossClipRect(), "", _state, getFlags()); + if (_showButton) + g_gui.theme()->drawButtonClip(Common::Rect(_x, _y, _x + _w, _y + _h), getBossClipRect(), "", _state, getFlags()); + + if (!_isAlpha) { + Graphics::Surface *gfx; + + if (_state == ThemeEngine::kStateHighlight) + gfx = &_gfx[kPicButtonHighlight]; + else if (_state == ThemeEngine::kStateDisabled) + gfx = &_gfx[kPicButtonStateDisabled]; + else if (_state == ThemeEngine::kStatePressed) + gfx = &_gfx[kPicButtonStatePressed]; + else + gfx = &_gfx[kPicButtonStateEnabled]; - if (_gfx.getPixels()) { + if (!gfx->getPixels()) + gfx = &_gfx[kPicButtonStateEnabled]; + + if (gfx->getPixels()) { // Check whether the set up surface needs to be converted to the GUI // color format. - const Graphics::PixelFormat &requiredFormat = g_gui.theme()->getPixelFormat(); - if (_gfx.format != requiredFormat) { - _gfx.convertToInPlace(requiredFormat); + const Graphics::PixelFormat &requiredFormat = g_gui.theme()->getPixelFormat(); + if (gfx->format != requiredFormat) { + gfx->convertToInPlace(requiredFormat); + } + + const int x = _x + (_w - gfx->w) / 2; + const int y = _y + (_h - gfx->h) / 2; + + g_gui.theme()->drawSurfaceClip(Common::Rect(x, y, x + gfx->w, y + gfx->h), getBossClipRect(), *gfx, _state, _alpha, _transparency); } + } else { + Graphics::TransparentSurface *gfx; + + if (_state == ThemeEngine::kStateHighlight) + gfx = &_agfx[kPicButtonHighlight]; + else if (_state == ThemeEngine::kStateDisabled) + gfx = &_agfx[kPicButtonStateDisabled]; + else if (_state == ThemeEngine::kStatePressed) + gfx = &_agfx[kPicButtonStatePressed]; + else + gfx = &_agfx[kPicButtonStateEnabled]; - const int x = _x + (_w - _gfx.w) / 2; - const int y = _y + (_h - _gfx.h) / 2; + if (!gfx->getPixels()) + gfx = &_agfx[kPicButtonStateEnabled]; - g_gui.theme()->drawSurfaceClip(Common::Rect(x, y, x + _gfx.w, y + _gfx.h), getBossClipRect(), _gfx, _state, _alpha, _transparency); + if (gfx->getPixels()) { + if (_mode == GUI::ThemeEngine::kAutoScaleNone) { + const int x = _x + (_w - gfx->w) / 2; + const int y = _y + (_h - gfx->h) / 2; + + g_gui.theme()->drawASurface(Common::Rect(x, y, x + gfx->w, y + gfx->h), *gfx, _mode, _alpha); + + } else { + g_gui.theme()->drawASurface(Common::Rect(_x, _y, _x + _w, _y + _h), *gfx, _mode, _alpha); + } + } } } @@ -652,13 +715,13 @@ int SliderWidget::posToValue(int pos) { #pragma mark - GraphicsWidget::GraphicsWidget(GuiObject *boss, int x, int y, int w, int h, const char *tooltip) - : Widget(boss, x, y, w, h, tooltip), _gfx(), _alpha(256), _transparency(false) { + : Widget(boss, x, y, w, h, tooltip), _gfx(), _alpha(255), _transparency(false) { setFlags(WIDGET_ENABLED | WIDGET_CLEARBG); _type = kGraphicsWidget; } GraphicsWidget::GraphicsWidget(GuiObject *boss, const Common::String &name, const char *tooltip) - : Widget(boss, name, tooltip), _gfx(), _alpha(256), _transparency(false) { + : Widget(boss, name, tooltip), _gfx(), _alpha(255), _transparency(false) { setFlags(WIDGET_ENABLED | WIDGET_CLEARBG); _type = kGraphicsWidget; } @@ -686,6 +749,26 @@ void GraphicsWidget::setGfx(const Graphics::Surface *gfx) { _gfx.copyFrom(*gfx); } +void GraphicsWidget::setAGfx(const Graphics::TransparentSurface *gfx, ThemeEngine::AutoScaleMode mode) { + _agfx.free(); + + if (!gfx || !gfx->getPixels()) + return; + + if (gfx->format.bytesPerPixel == 1) { + warning("GraphicsWidget::setGfx got paletted surface passed"); + return; + } + + if ((gfx->w > _w || gfx->h > _h) && mode == ThemeEngine::kAutoScaleNone) { + warning("GraphicsWidget has size %dx%d, but a surface with %dx%d is to be set", _w, _h, gfx->w, gfx->h); + return; + } + + _agfx.copyFrom(*gfx); + _mode = mode; +} + void GraphicsWidget::setGfx(int w, int h, int r, int g, int b) { if (w == -1) w = _w; @@ -712,6 +795,23 @@ void GraphicsWidget::drawWidget() { const int y = _y + (_h - _gfx.h) / 2; g_gui.theme()->drawSurfaceClip(Common::Rect(x, y, x + _gfx.w, y + _gfx.h), getBossClipRect(), _gfx, _state, _alpha, _transparency); + } else if (_agfx.getPixels()) { + // Check whether the set up surface needs to be converted to the GUI + // color format. + const Graphics::PixelFormat &requiredFormat = g_gui.theme()->getPixelFormat(); + if (_agfx.format != requiredFormat) { + _agfx.convertToInPlace(requiredFormat); + } + + if (_mode == GUI::ThemeEngine::kAutoScaleNone) { + const int x = _x + (_w - _agfx.w) / 2; + const int y = _y + (_h - _agfx.h) / 2; + + g_gui.theme()->drawASurface(Common::Rect(x, y, x + _agfx.w, y + _agfx.h), _agfx, _mode, _alpha); + + } else { + g_gui.theme()->drawASurface(Common::Rect(_x, _y, _x + _w, _y + _h), _agfx, _mode, _alpha); + } } } diff --git a/gui/widget.h b/gui/widget.h index 0f4b300233..e9343f264c 100644 --- a/gui/widget.h +++ b/gui/widget.h @@ -80,6 +80,15 @@ enum { kPressedButtonTime = 200 }; +enum { + kPicButtonStateEnabled = 0, + kPicButtonHighlight = 1, + kPicButtonStateDisabled = 2, + kPicButtonStatePressed = 3, + + kPicButtonStateMax = 3 +}; + /* Widget */ class Widget : public GuiObject { friend class Dialog; @@ -221,18 +230,24 @@ public: PicButtonWidget(GuiObject *boss, const Common::String &name, const char *tooltip = 0, uint32 cmd = 0, uint8 hotkey = 0); ~PicButtonWidget(); - void setGfx(const Graphics::Surface *gfx); - void setGfx(int w, int h, int r, int g, int b); + void setGfx(const Graphics::Surface *gfx, int statenum = kPicButtonStateEnabled); + void setAGfx(const Graphics::TransparentSurface *gfx, int statenum = kPicButtonStateEnabled, ThemeEngine::AutoScaleMode mode = ThemeEngine::kAutoScaleNone); + void setGfx(int w, int h, int r, int g, int b, int statenum = kPicButtonStateEnabled); void useAlpha(int alpha) { _alpha = alpha; } void useThemeTransparency(bool enable) { _transparency = enable; } + void setButtonDisplay(bool enable) {_showButton = enable; } protected: void drawWidget(); - Graphics::Surface _gfx; + Graphics::Surface _gfx[kPicButtonStateMax + 1]; + Graphics::TransparentSurface _agfx[kPicButtonStateMax + 1]; int _alpha; bool _transparency; + bool _showButton; + bool _isAlpha; + ThemeEngine::AutoScaleMode _mode; }; /* CheckboxWidget */ @@ -351,6 +366,7 @@ public: void setGfx(const Graphics::Surface *gfx); void setGfx(int w, int h, int r, int g, int b); + void setAGfx(const Graphics::TransparentSurface *gfx, ThemeEngine::AutoScaleMode mode = ThemeEngine::kAutoScaleNone); void useAlpha(int alpha) { _alpha = alpha; } void useThemeTransparency(bool enable) { _transparency = enable; } @@ -359,8 +375,10 @@ protected: void drawWidget(); Graphics::Surface _gfx; + Graphics::TransparentSurface _agfx; int _alpha; bool _transparency; + ThemeEngine::AutoScaleMode _mode; }; /* ContainerWidget */ diff --git a/gui/widgets/editable.cpp b/gui/widgets/editable.cpp index 4f7e584c14..02defe9a56 100644 --- a/gui/widgets/editable.cpp +++ b/gui/widgets/editable.cpp @@ -185,6 +185,21 @@ bool EditableWidget::handleKeyDown(Common::KeyState state) { forcecaret = true; break; + case Common::KEYCODE_v: + if (g_system->hasFeature(OSystem::kFeatureClipboardSupport) && state.flags & Common::KBD_CTRL) { + if (g_system->hasTextInClipboard()) { + String text = g_system->getTextFromClipboard(); + for (uint32 i = 0; i < text.size(); ++i) { + if (tryInsertChar(text[i], _caretPos)) + ++_caretPos; + } + dirty = true; + } + } else { + defaultKeyDownHandler(state, dirty, forcecaret, handled); + } + break; + #ifdef MACOSX // Let ctrl-a / ctrl-e move the caret to the start / end of the line. // diff --git a/gui/widgets/scrollcontainer.cpp b/gui/widgets/scrollcontainer.cpp index 1b38478c11..9a7792730d 100644 --- a/gui/widgets/scrollcontainer.cpp +++ b/gui/widgets/scrollcontainer.cpp @@ -28,13 +28,13 @@ namespace GUI { -ScrollContainerWidget::ScrollContainerWidget(GuiObject *boss, int x, int y, int w, int h) - : Widget(boss, x, y, w, h) { +ScrollContainerWidget::ScrollContainerWidget(GuiObject *boss, int x, int y, int w, int h, uint32 reflowCmd) + : Widget(boss, x, y, w, h), CommandSender(nullptr), _reflowCmd(reflowCmd) { init(); } -ScrollContainerWidget::ScrollContainerWidget(GuiObject *boss, const Common::String &name) - : Widget(boss, name) { +ScrollContainerWidget::ScrollContainerWidget(GuiObject *boss, const Common::String &name, uint32 reflowCmd) + : Widget(boss, name), CommandSender(nullptr), _reflowCmd(reflowCmd) { init(); } @@ -52,14 +52,14 @@ void ScrollContainerWidget::init() { void ScrollContainerWidget::recalc() { int scrollbarWidth = g_gui.xmlEval()->getVar("Globals.Scrollbar.Width", 0); _limitH = _h; - + //calculate virtual height const int spacing = g_gui.xmlEval()->getVar("Global.Font.Height", 16); //on the bottom int h = 0; int min = spacing, max = 0; Widget *ptr = _firstWidget; while (ptr) { - if (ptr != _verticalScroll) { + if (ptr != _verticalScroll && ptr->isVisible()) { int y = ptr->getAbsY() - getChildY(); min = MIN(min, y - spacing); max = MAX(max, y + ptr->getHeight() + spacing); @@ -68,6 +68,8 @@ void ScrollContainerWidget::recalc() { } h = max - min; + if (h <= _limitH) _scrolledY = 0; + _verticalScroll->_numEntries = h; _verticalScroll->_currentPos = _scrolledY; _verticalScroll->_entriesPerPage = _limitH; @@ -115,7 +117,10 @@ void ScrollContainerWidget::reflowLayout() { ptr->reflowLayout(); ptr = ptr->next(); } - + + //hide and move widgets, if needed + sendCommand(_reflowCmd, 0); + //recalculate height recalc(); @@ -124,7 +129,7 @@ void ScrollContainerWidget::reflowLayout() { while (ptr) { int y = ptr->getAbsY() - getChildY(); int h = ptr->getHeight(); - bool visible = true; + bool visible = ptr->isVisible(); if (y + h - _scrolledY < 0) visible = false; if (y - _scrolledY > _limitH) visible = false; ptr->setVisible(visible); @@ -132,6 +137,7 @@ void ScrollContainerWidget::reflowLayout() { } _verticalScroll->setVisible(_verticalScroll->_numEntries > _limitH); //show when there is something to scroll + _verticalScroll->recalc(); } void ScrollContainerWidget::drawWidget() { diff --git a/gui/widgets/scrollcontainer.h b/gui/widgets/scrollcontainer.h index 692c7e3507..c2d47370ee 100644 --- a/gui/widgets/scrollcontainer.h +++ b/gui/widgets/scrollcontainer.h @@ -29,16 +29,17 @@ namespace GUI { -class ScrollContainerWidget: public Widget { +class ScrollContainerWidget: public Widget, public CommandSender { ScrollBarWidget *_verticalScroll; int16 _scrolledX, _scrolledY; uint16 _limitH; + uint32 _reflowCmd; void recalc(); public: - ScrollContainerWidget(GuiObject *boss, int x, int y, int w, int h); - ScrollContainerWidget(GuiObject *boss, const Common::String &name); + ScrollContainerWidget(GuiObject *boss, int x, int y, int w, int h, uint32 reflowCmd = 0); + ScrollContainerWidget(GuiObject *boss, const Common::String &name, uint32 reflowCmd = 0); ~ScrollContainerWidget(); void init(); |