From: Hannah von Reth Date: Wed, 17 Feb 2021 14:41:32 +0000 (+0100) Subject: Add socket command to upload a selection of files based on a regex X-Git-Tag: archive/raspbian/3.16.7-1_deb13u1+rpi1~1^2~12^2~18^2~278^2~2 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=4b0122093a97f5f1a68b0c407b79621a97b14f02;p=nextcloud-desktop.git Add socket command to upload a selection of files based on a regex (cherry picked from commit 0ded3a56a9f3470a951b18eaa9d3c1b5e8db1135) --- diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 83ee27706..748526310 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -87,7 +87,6 @@ set(client_SRCS sharemanager.cpp shareusergroupwidget.cpp sharee.cpp - socketapi.cpp sslbutton.cpp sslerrordialog.cpp syncrunfilelog.cpp @@ -338,6 +337,8 @@ target_link_libraries(nextcloudCore Qt5::QuickControls2 ) +add_subdirectory(socketapi) + if(Qt5WebEngine_FOUND AND Qt5WebEngineWidgets_FOUND) target_link_libraries(nextcloudCore PUBLIC Qt5::WebEngineWidgets) endif() diff --git a/src/gui/application.cpp b/src/gui/application.cpp index 97ecedba9..64b2da8fa 100644 --- a/src/gui/application.cpp +++ b/src/gui/application.cpp @@ -27,7 +27,7 @@ #include "folderman.h" #include "logger.h" #include "configfile.h" -#include "socketapi.h" +#include "socketapi/socketapi.h" #include "sslerrordialog.h" #include "theme.h" #include "clientproxy.h" diff --git a/src/gui/folder.cpp b/src/gui/folder.cpp index cbb192b01..0cb4f472b 100644 --- a/src/gui/folder.cpp +++ b/src/gui/folder.cpp @@ -28,7 +28,7 @@ #include "clientproxy.h" #include "syncengine.h" #include "syncrunfilelog.h" -#include "socketapi.h" +#include "socketapi/socketapi.h" #include "theme.h" #include "filesystem.h" #include "localdiscoverytracker.h" diff --git a/src/gui/folderman.cpp b/src/gui/folderman.cpp index 9b0f297e9..5a3881480 100644 --- a/src/gui/folderman.cpp +++ b/src/gui/folderman.cpp @@ -17,7 +17,7 @@ #include "folder.h" #include "syncresult.h" #include "theme.h" -#include "socketapi.h" +#include "socketapi/socketapi.h" #include "account.h" #include "accountstate.h" #include "accountmanager.h" diff --git a/src/gui/socketapi.cpp b/src/gui/socketapi.cpp deleted file mode 100644 index bd0dc40b7..000000000 --- a/src/gui/socketapi.cpp +++ /dev/null @@ -1,1392 +0,0 @@ -/* - * Copyright (C) by Dominik Schmidt - * Copyright (C) by Klaas Freitag - * Copyright (C) by Roeland Jago Douma - * - * 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. - */ - -#include "socketapi.h" -#include "socketapi_p.h" - -#include "conflictdialog.h" -#include "conflictsolver.h" -#include "config.h" -#include "configfile.h" -#include "folderman.h" -#include "folder.h" -#include "theme.h" -#include "common/syncjournalfilerecord.h" -#include "syncengine.h" -#include "syncfileitem.h" -#include "filesystem.h" -#include "version.h" -#include "account.h" -#include "accountstate.h" -#include "account.h" -#include "capabilities.h" -#include "common/asserts.h" -#include "guiutility.h" -#ifndef OWNCLOUD_TEST -#include "sharemanager.h" -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - - -#include -#include -#include -#include - -#include -#include - -#include -#include - -#ifdef Q_OS_MAC -#include -#endif - - -// This is the version that is returned when the client asks for the VERSION. -// The first number should be changed if there is an incompatible change that breaks old clients. -// The second number should be changed when there are new features. -#define MIRALL_SOCKET_API_VERSION "1.1" - -namespace { - -const QLatin1Char RecordSeparator() -{ - return QLatin1Char('\x1e'); -} - -QStringList split(const QString &data) -{ - // TODO: string ref? - return data.split(RecordSeparator()); -} - -#if GUI_TESTING - -using namespace OCC; - -QList allObjects(const QList &widgets) { - QList objects; - std::copy(widgets.constBegin(), widgets.constEnd(), std::back_inserter(objects)); - - objects << qApp; - - return objects; -} - -QObject *findWidget(const QString &queryString, const QList &widgets = QApplication::allWidgets()) -{ - auto objects = allObjects(widgets); - - QList::const_iterator foundWidget; - - if (queryString.contains('>')) { - qCDebug(lcSocketApi) << "queryString contains >"; - - auto subQueries = queryString.split('>', QString::SkipEmptyParts); - Q_ASSERT(subQueries.count() == 2); - - auto parentQueryString = subQueries[0].trimmed(); - qCDebug(lcSocketApi) << "Find parent: " << parentQueryString; - auto parent = findWidget(parentQueryString); - - if(!parent) { - return nullptr; - } - - auto childQueryString = subQueries[1].trimmed(); - auto child = findWidget(childQueryString, parent->findChildren()); - qCDebug(lcSocketApi) << "found child: " << !!child; - return child; - - } else if(queryString.startsWith('#')) { - auto objectName = queryString.mid(1); - qCDebug(lcSocketApi) << "find objectName: " << objectName; - foundWidget = std::find_if(objects.constBegin(), objects.constEnd(), [&](QObject *widget) { - return widget->objectName() == objectName; - }); - } else { - QList matches; - std::copy_if(objects.constBegin(), objects.constEnd(), std::back_inserter(matches), [&](QObject* widget) { - return widget->inherits(queryString.toLatin1()); - }); - - std::for_each(matches.constBegin(), matches.constEnd(), [](QObject* w) { - if(!w) return; - qCDebug(lcSocketApi) << "WIDGET: " << w->objectName() << w->metaObject()->className(); - }); - - if(matches.empty()) { - return nullptr; - } - return matches[0]; - } - - if (foundWidget == objects.constEnd()) { - return nullptr; - } - - return *foundWidget; -} -#endif - -static inline QString removeTrailingSlash(QString path) -{ - Q_ASSERT(path.endsWith(QLatin1Char('/'))); - path.truncate(path.length() - 1); - return path; -} - -static QString buildMessage(const QString &verb, const QString &path, const QString &status = QString()) -{ - QString msg(verb); - - if (!status.isEmpty()) { - msg.append(QLatin1Char(':')); - msg.append(status); - } - if (!path.isEmpty()) { - msg.append(QLatin1Char(':')); - QFileInfo fi(path); - msg.append(QDir::toNativeSeparators(fi.absoluteFilePath())); - } - return msg; -} -} - -namespace OCC { - -Q_LOGGING_CATEGORY(lcSocketApi, "nextcloud.gui.socketapi", QtInfoMsg) -Q_LOGGING_CATEGORY(lcPublicLink, "nextcloud.gui.socketapi.publiclink", QtInfoMsg) - - -void SocketListener::sendMessage(const QString &message, bool doWait) const -{ - if (!socket) { - qCWarning(lcSocketApi) << "Not sending message to dead socket:" << message; - return; - } - - qCDebug(lcSocketApi) << "Sending SocketAPI message -->" << message << "to" << socket; - QString localMessage = message; - if (!localMessage.endsWith(QLatin1Char('\n'))) { - localMessage.append(QLatin1Char('\n')); - } - - QByteArray bytesToSend = localMessage.toUtf8(); - qint64 sent = socket->write(bytesToSend); - if (doWait) { - socket->waitForBytesWritten(1000); - } - if (sent != bytesToSend.length()) { - qCWarning(lcSocketApi) << "Could not send all data on socket for " << localMessage; - } -} - -struct ListenerHasSocketPred -{ - QIODevice *socket; - ListenerHasSocketPred(QIODevice *socket) - : socket(socket) - { - } - bool operator()(const SocketListener &listener) const { return listener.socket == socket; } -}; - -SocketApi::SocketApi(QObject *parent) - : QObject(parent) -{ - QString socketPath; - - qRegisterMetaType("SocketListener*"); - qRegisterMetaType>("QSharedPointer"); - - if (Utility::isWindows()) { - socketPath = QLatin1String(R"(\\.\pipe\)") - + QLatin1String(APPLICATION_EXECUTABLE) - + QLatin1String("-") - + QString::fromLocal8Bit(qgetenv("USERNAME")); - // TODO: once the windows extension supports multiple - // client connections, switch back to the theme name - // See issue #2388 - // + Theme::instance()->appName(); - } else if (Utility::isMac()) { - // This must match the code signing Team setting of the extension - // Example for developer builds (with ad-hoc signing identity): "" "com.owncloud.desktopclient" ".socketApi" - // Example for official signed packages: "9B5WD74GWJ." "com.owncloud.desktopclient" ".socketApi" - socketPath = SOCKETAPI_TEAM_IDENTIFIER_PREFIX APPLICATION_REV_DOMAIN ".socketApi"; -#ifdef Q_OS_MAC - CFURLRef url = (CFURLRef)CFAutorelease((CFURLRef)CFBundleCopyBundleURL(CFBundleGetMainBundle())); - QString bundlePath = QUrl::fromCFURL(url).path(); - - auto _system = [](const QString &cmd, const QStringList &args){ - QProcess process; - process.setProcessChannelMode(QProcess::MergedChannels); - process.start(cmd, args); - if (!process.waitForFinished()) - { - qCWarning(lcSocketApi) << "Failed to load shell extension:" << cmd << args.join(" ") << process.errorString(); - } else { - qCInfo(lcSocketApi) << (process.exitCode() != 0 ? "Failed to load" : "Loaded") << "shell extension:" << cmd << args.join(" ") << process.readAll(); - } - }; - // Add it again. This was needed for Mojave to trigger a load. - _system(QStringLiteral("pluginkit"), {QStringLiteral("-a"),QStringLiteral("%1Contents/PlugIns/FinderSyncExt.appex/").arg(bundlePath)}); - // Tell Finder to use the Extension (checking it from System Preferences -> Extensions) - _system(QStringLiteral("pluginkit"), {QStringLiteral("-e"), QStringLiteral("use"), QStringLiteral("-i"), QStringLiteral(APPLICATION_REV_DOMAIN ".FinderSyncExt")}); - -#endif - } else if (Utility::isLinux() || Utility::isBSD()) { - QString runtimeDir; - runtimeDir = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation); - socketPath = runtimeDir + "/" + Theme::instance()->appName() + "/socket"; - } else { - qCWarning(lcSocketApi) << "An unexpected system detected, this probably won't work."; - } - - SocketApiServer::removeServer(socketPath); - QFileInfo info(socketPath); - if (!info.dir().exists()) { - bool result = info.dir().mkpath("."); - qCDebug(lcSocketApi) << "creating" << info.dir().path() << result; - if (result) { - QFile::setPermissions(socketPath, - QFile::Permissions(QFile::ReadOwner + QFile::WriteOwner + QFile::ExeOwner)); - } - } - if (!_localServer.listen(socketPath)) { - qCWarning(lcSocketApi) << "can't start server" << socketPath; - } else { - qCInfo(lcSocketApi) << "server started, listening at " << socketPath; - } - - connect(&_localServer, &SocketApiServer::newConnection, this, &SocketApi::slotNewConnection); - - // folder watcher - connect(FolderMan::instance(), &FolderMan::folderSyncStateChange, this, &SocketApi::slotUpdateFolderView); -} - -SocketApi::~SocketApi() -{ - qCDebug(lcSocketApi) << "dtor"; - _localServer.close(); - // All remaining sockets will be destroyed with _localServer, their parent - ASSERT(_listeners.isEmpty() || _listeners.first().socket->parent() == &_localServer); - _listeners.clear(); -} - -void SocketApi::slotNewConnection() -{ - // Note that on macOS this is not actually a line-based QIODevice, it's a SocketApiSocket which is our - // custom message based macOS IPC. - QIODevice *socket = _localServer.nextPendingConnection(); - - if (!socket) { - return; - } - qCInfo(lcSocketApi) << "New connection" << socket; - connect(socket, &QIODevice::readyRead, this, &SocketApi::slotReadSocket); - connect(socket, SIGNAL(disconnected()), this, SLOT(onLostConnection())); - connect(socket, &QObject::destroyed, this, &SocketApi::slotSocketDestroyed); - ASSERT(socket->readAll().isEmpty()); - - _listeners.append(SocketListener(socket)); - SocketListener &listener = _listeners.last(); - - foreach (Folder *f, FolderMan::instance()->map()) { - if (f->canSync()) { - QString message = buildRegisterPathMessage(removeTrailingSlash(f->path())); - qCInfo(lcSocketApi) << "Trying to send SocketAPI Register Path Message -->" << message << "to" << listener.socket; - listener.sendMessage(message); - } - } -} - -void SocketApi::onLostConnection() -{ - qCInfo(lcSocketApi) << "Lost connection " << sender(); - sender()->deleteLater(); - - auto socket = qobject_cast(sender()); - ASSERT(socket); - _listeners.erase(std::remove_if(_listeners.begin(), _listeners.end(), ListenerHasSocketPred(socket)), _listeners.end()); -} - -void SocketApi::slotSocketDestroyed(QObject *obj) -{ - auto *socket = static_cast(obj); - _listeners.erase(std::remove_if(_listeners.begin(), _listeners.end(), ListenerHasSocketPred(socket)), _listeners.end()); -} - -void SocketApi::slotReadSocket() -{ - auto *socket = qobject_cast(sender()); - ASSERT(socket); - - // Find the SocketListener - // - // It's possible for the disconnected() signal to be triggered before - // the readyRead() signals are received - in that case there won't be a - // valid listener. We execute the handler anyway, but it will work with - // a SocketListener that doesn't send any messages. - static auto noListener = SocketListener(nullptr); - SocketListener *listener = &noListener; - auto listenerIt = std::find_if(_listeners.begin(), _listeners.end(), ListenerHasSocketPred(socket)); - if (listenerIt != _listeners.end()) { - listener = &*listenerIt; - } - - while (socket->canReadLine()) { - // Make sure to normalize the input from the socket to - // make sure that the path will match, especially on OS X. - const QString line = QString::fromUtf8(socket->readLine().trimmed()).normalized(QString::NormalizationForm_C); - qCInfo(lcSocketApi) << "Received SocketAPI message <--" << line << "from" << socket; - const QByteArray command = line.mid(0, line.indexOf(QLatin1Char(':'))).toUtf8(); - const QByteArray functionWithArguments = "command_" + command + (command.startsWith("ASYNC_") ? "(QSharedPointer)" : "(QString,SocketListener*)"); - const int indexOfMethod = staticMetaObject.indexOfMethod(functionWithArguments); - - const auto argument = line.midRef(command.length() + 1); - if (command.startsWith("ASYNC_")) { - - auto arguments = argument.split('|'); - if (arguments.size() != 2) { - listener->sendMessage(QStringLiteral("argument count is wrong")); - return; - } - - auto json = QJsonDocument::fromJson(arguments[1].toUtf8()).object(); - - auto jobId = arguments[0]; - - auto socketApiJob = QSharedPointer( - new SocketApiJob(jobId.toString(), listener, json), &QObject::deleteLater); - if (indexOfMethod != -1) { - staticMetaObject.method(indexOfMethod) - .invoke(this, Qt::QueuedConnection, - Q_ARG(QSharedPointer, socketApiJob)); - } else { - qCWarning(lcSocketApi) << "The command is not supported by this version of the client:" << command - << "with argument:" << argument; - socketApiJob->reject(QStringLiteral("command not found")); - } - } else { - if (indexOfMethod != -1) { - // to ensure that listener is still valid we need to call it with Qt::DirectConnection - ASSERT(thread() == QThread::currentThread()) - staticMetaObject.method(indexOfMethod) - .invoke(this, Qt::DirectConnection, Q_ARG(QString, argument.toString()), - Q_ARG(SocketListener *, listener)); - } else { - qCWarning(lcSocketApi) << "The command is not supported by this version of the client:" << command << "with argument:" << argument; - } - } - } -} - -void SocketApi::slotRegisterPath(const QString &alias) -{ - // Make sure not to register twice to each connected client - if (_registeredAliases.contains(alias)) - return; - - Folder *f = FolderMan::instance()->folder(alias); - if (f) { - QString message = buildRegisterPathMessage(removeTrailingSlash(f->path())); - foreach (auto &listener, _listeners) { - qCInfo(lcSocketApi) << "Trying to send SocketAPI Register Path Message -->" << message << "to" << listener.socket; - listener.sendMessage(message); - } - } - - _registeredAliases.insert(alias); -} - -void SocketApi::slotUnregisterPath(const QString &alias) -{ - if (!_registeredAliases.contains(alias)) - return; - - Folder *f = FolderMan::instance()->folder(alias); - if (f) - broadcastMessage(buildMessage(QLatin1String("UNREGISTER_PATH"), removeTrailingSlash(f->path()), QString()), true); - - _registeredAliases.remove(alias); -} - -void SocketApi::slotUpdateFolderView(Folder *f) -{ - if (_listeners.isEmpty()) { - return; - } - - if (f) { - // do only send UPDATE_VIEW for a couple of status - if (f->syncResult().status() == SyncResult::SyncPrepare - || f->syncResult().status() == SyncResult::Success - || f->syncResult().status() == SyncResult::Paused - || f->syncResult().status() == SyncResult::Problem - || f->syncResult().status() == SyncResult::Error - || f->syncResult().status() == SyncResult::SetupError) { - QString rootPath = removeTrailingSlash(f->path()); - broadcastStatusPushMessage(rootPath, f->syncEngine().syncFileStatusTracker().fileStatus("")); - - broadcastMessage(buildMessage(QLatin1String("UPDATE_VIEW"), rootPath)); - } else { - qCDebug(lcSocketApi) << "Not sending UPDATE_VIEW for" << f->alias() << "because status() is" << f->syncResult().status(); - } - } -} - -void SocketApi::broadcastMessage(const QString &msg, bool doWait) -{ - foreach (auto &listener, _listeners) { - listener.sendMessage(msg, doWait); - } -} - -void SocketApi::processShareRequest(const QString &localFile, SocketListener *listener, ShareDialogStartPage startPage) -{ - auto theme = Theme::instance(); - - auto fileData = FileData::get(localFile); - auto shareFolder = fileData.folder; - if (!shareFolder) { - const QString message = QLatin1String("SHARE:NOP:") + QDir::toNativeSeparators(localFile); - // files that are not within a sync folder are not synced. - listener->sendMessage(message); - } else if (!shareFolder->accountState()->isConnected()) { - const QString message = QLatin1String("SHARE:NOTCONNECTED:") + QDir::toNativeSeparators(localFile); - // if the folder isn't connected, don't open the share dialog - listener->sendMessage(message); - } else if (!theme->linkSharing() && (!theme->userGroupSharing() || shareFolder->accountState()->account()->serverVersionInt() < Account::makeServerVersion(8, 2, 0))) { - const QString message = QLatin1String("SHARE:NOP:") + QDir::toNativeSeparators(localFile); - listener->sendMessage(message); - } else { - // If the file doesn't have a journal record, it might not be uploaded yet - if (!fileData.journalRecord().isValid()) { - const QString message = QLatin1String("SHARE:NOTSYNCED:") + QDir::toNativeSeparators(localFile); - listener->sendMessage(message); - return; - } - - auto &remotePath = fileData.serverRelativePath; - - // Can't share root folder - if (remotePath == "/") { - const QString message = QLatin1String("SHARE:CANNOTSHAREROOT:") + QDir::toNativeSeparators(localFile); - listener->sendMessage(message); - return; - } - - const QString message = QLatin1String("SHARE:OK:") + QDir::toNativeSeparators(localFile); - listener->sendMessage(message); - - emit shareCommandReceived(remotePath, fileData.localPath, startPage); - } -} - -void SocketApi::broadcastStatusPushMessage(const QString &systemPath, SyncFileStatus fileStatus) -{ - QString msg = buildMessage(QLatin1String("STATUS"), systemPath, fileStatus.toSocketAPIString()); - Q_ASSERT(!systemPath.endsWith('/')); - uint directoryHash = qHash(systemPath.left(systemPath.lastIndexOf('/'))); - foreach (auto &listener, _listeners) { - listener.sendMessageIfDirectoryMonitored(msg, directoryHash); - } -} - -void SocketApi::command_RETRIEVE_FOLDER_STATUS(const QString &argument, SocketListener *listener) -{ - // This command is the same as RETRIEVE_FILE_STATUS - command_RETRIEVE_FILE_STATUS(argument, listener); -} - -void SocketApi::command_RETRIEVE_FILE_STATUS(const QString &argument, SocketListener *listener) -{ - QString statusString; - - auto fileData = FileData::get(argument); - if (!fileData.folder) { - // this can happen in offline mode e.g.: nothing to worry about - statusString = QLatin1String("NOP"); - } else { - // The user probably visited this directory in the file shell. - // Let the listener know that it should now send status pushes for sibblings of this file. - QString directory = fileData.localPath.left(fileData.localPath.lastIndexOf('/')); - listener->registerMonitoredDirectory(qHash(directory)); - - SyncFileStatus fileStatus = fileData.syncFileStatus(); - statusString = fileStatus.toSocketAPIString(); - } - - const QString message = QLatin1String("STATUS:") % statusString % QLatin1Char(':') % QDir::toNativeSeparators(argument); - listener->sendMessage(message); -} - -void SocketApi::command_SHARE(const QString &localFile, SocketListener *listener) -{ - processShareRequest(localFile, listener, ShareDialogStartPage::UsersAndGroups); -} - -void SocketApi::command_MANAGE_PUBLIC_LINKS(const QString &localFile, SocketListener *listener) -{ - processShareRequest(localFile, listener, ShareDialogStartPage::PublicLinks); -} - -void SocketApi::command_VERSION(const QString &, SocketListener *listener) -{ - listener->sendMessage(QLatin1String("VERSION:" MIRALL_VERSION_STRING ":" MIRALL_SOCKET_API_VERSION)); -} - -void SocketApi::command_SHARE_MENU_TITLE(const QString &, SocketListener *listener) -{ - //listener->sendMessage(QLatin1String("SHARE_MENU_TITLE:") + tr("Share with %1", "parameter is Nextcloud").arg(Theme::instance()->appNameGUI())); - listener->sendMessage(QLatin1String("SHARE_MENU_TITLE:") + Theme::instance()->appNameGUI()); -} - -void SocketApi::command_EDIT(const QString &localFile, SocketListener *listener) -{ - Q_UNUSED(listener) - auto fileData = FileData::get(localFile); - if (!fileData.folder) { - qCWarning(lcSocketApi) << "Unknown path" << localFile; - return; - } - - auto record = fileData.journalRecord(); - if (!record.isValid()) - return; - - DirectEditor* editor = getDirectEditorForLocalFile(fileData.localPath); - if (!editor) - return; - - auto *job = new JsonApiJob(fileData.folder->accountState()->account(), QLatin1String("ocs/v2.php/apps/files/api/v1/directEditing/open"), this); - - QUrlQuery params; - params.addQueryItem("path", fileData.serverRelativePath); - params.addQueryItem("editorId", editor->id()); - job->addQueryParams(params); - job->usePOST(); - - QObject::connect(job, &JsonApiJob::jsonReceived, [](const QJsonDocument &json){ - auto data = json.object().value("ocs").toObject().value("data").toObject(); - auto url = QUrl(data.value("url").toString()); - - if(!url.isEmpty()) - Utility::openBrowser(url); - }); - job->start(); -} - -// don't pull the share manager into socketapi unittests -#ifndef OWNCLOUD_TEST - -class GetOrCreatePublicLinkShare : public QObject -{ - Q_OBJECT -public: - GetOrCreatePublicLinkShare(const AccountPtr &account, const QString &localFile, - std::function targetFun, QObject *parent) - : QObject(parent) - , _shareManager(account) - , _localFile(localFile) - , _targetFun(targetFun) - { - connect(&_shareManager, &ShareManager::sharesFetched, - this, &GetOrCreatePublicLinkShare::sharesFetched); - connect(&_shareManager, &ShareManager::linkShareCreated, - this, &GetOrCreatePublicLinkShare::linkShareCreated); - connect(&_shareManager, &ShareManager::serverError, - this, &GetOrCreatePublicLinkShare::serverError); - connect(&_shareManager, &ShareManager::linkShareRequiresPassword, - this, &GetOrCreatePublicLinkShare::passwordRequired); - } - - void run() - { - qCDebug(lcPublicLink) << "Fetching shares"; - _shareManager.fetchShares(_localFile); - } - -private slots: - void sharesFetched(const QList> &shares) - { - auto shareName = SocketApi::tr("Context menu share"); - // If there already is a context menu share, reuse it - for (const auto &share : shares) { - const auto linkShare = qSharedPointerDynamicCast(share); - if (!linkShare) - continue; - - if (linkShare->getName() == shareName) { - qCDebug(lcPublicLink) << "Found existing share, reusing"; - return success(linkShare->getLink().toString()); - } - } - - // otherwise create a new one - qCDebug(lcPublicLink) << "Creating new share"; - _shareManager.createLinkShare(_localFile, shareName, QString()); - } - - void linkShareCreated(const QSharedPointer &share) - { - qCDebug(lcPublicLink) << "New share created"; - success(share->getLink().toString()); - } - - void passwordRequired() { - bool ok = false; - QString password = QInputDialog::getText(nullptr, - tr("Password for share required"), - tr("Please enter a password for your link share:"), - QLineEdit::Normal, - QString(), - &ok); - - if (!ok) { - // The dialog was canceled so no need to do anything - return; - } - - // Try to create the link share again with the newly entered password - _shareManager.createLinkShare(_localFile, QString(), password); - } - - void serverError(int code, const QString &message) - { - qCWarning(lcPublicLink) << "Share fetch/create error" << code << message; - QMessageBox::warning( - nullptr, - tr("Sharing error"), - tr("Could not retrieve or create the public link share. Error:\n\n%1").arg(message), - QMessageBox::Ok, - QMessageBox::NoButton); - deleteLater(); - } - -private: - void success(const QString &link) - { - _targetFun(link); - deleteLater(); - } - - ShareManager _shareManager; - QString _localFile; - std::function _targetFun; -}; - -#else - -class GetOrCreatePublicLinkShare : public QObject -{ - Q_OBJECT -public: - GetOrCreatePublicLinkShare(const AccountPtr &, const QString &, - std::function, QObject *) - { - } - - void run() - { - } -}; - -#endif - -void SocketApi::command_COPY_PUBLIC_LINK(const QString &localFile, SocketListener *) -{ - auto fileData = FileData::get(localFile); - if (!fileData.folder) - return; - - AccountPtr account = fileData.folder->accountState()->account(); - auto job = new GetOrCreatePublicLinkShare(account, fileData.serverRelativePath, [](const QString &url) { copyUrlToClipboard(url); }, this); - job->run(); -} - -// Windows Shell / Explorer pinning fallbacks, see issue: https://github.com/nextcloud/desktop/issues/1599 -#ifdef Q_OS_WIN -void SocketApi::command_COPYASPATH(const QString &localFile, SocketListener *) -{ - QApplication::clipboard()->setText(localFile); -} - -void SocketApi::command_OPENNEWWINDOW(const QString &localFile, SocketListener *) -{ - QDesktopServices::openUrl(QUrl::fromLocalFile(localFile)); -} - -void SocketApi::command_OPEN(const QString &localFile, SocketListener *socketListener) -{ - command_OPENNEWWINDOW(localFile, socketListener); -} -#endif - -// Fetches the private link url asynchronously and then calls the target slot -void SocketApi::fetchPrivateLinkUrlHelper(const QString &localFile, const std::function &targetFun) -{ - auto fileData = FileData::get(localFile); - if (!fileData.folder) { - qCWarning(lcSocketApi) << "Unknown path" << localFile; - return; - } - - auto record = fileData.journalRecord(); - if (!record.isValid()) - return; - - fetchPrivateLinkUrl( - fileData.folder->accountState()->account(), - fileData.serverRelativePath, - record.numericFileId(), - this, - targetFun); -} - -void SocketApi::command_COPY_PRIVATE_LINK(const QString &localFile, SocketListener *) -{ - fetchPrivateLinkUrlHelper(localFile, &SocketApi::copyUrlToClipboard); -} - -void SocketApi::command_EMAIL_PRIVATE_LINK(const QString &localFile, SocketListener *) -{ - fetchPrivateLinkUrlHelper(localFile, &SocketApi::emailPrivateLink); -} - -void SocketApi::command_OPEN_PRIVATE_LINK(const QString &localFile, SocketListener *) -{ - fetchPrivateLinkUrlHelper(localFile, &SocketApi::openPrivateLink); -} - -void SocketApi::command_MAKE_AVAILABLE_LOCALLY(const QString &filesArg, SocketListener *) -{ - const QStringList files = split(filesArg); - - for (const auto &file : files) { - auto data = FileData::get(file); - if (!data.folder) - continue; - - // Update the pin state on all items - if (!data.folder->vfs().setPinState(data.folderRelativePath, PinState::AlwaysLocal)) { - qCWarning(lcSocketApi) << "Could not set pin state of" << data.folderRelativePath << "to always local"; - } - - // Trigger sync - data.folder->schedulePathForLocalDiscovery(data.folderRelativePath); - data.folder->scheduleThisFolderSoon(); - } -} - -/* Go over all the files and replace them by a virtual file */ -void SocketApi::command_MAKE_ONLINE_ONLY(const QString &filesArg, SocketListener *) -{ - const QStringList files = split(filesArg); - - for (const auto &file : files) { - auto data = FileData::get(file); - if (!data.folder) - continue; - - // Update the pin state on all items - if (!data.folder->vfs().setPinState(data.folderRelativePath, PinState::OnlineOnly)) { - qCWarning(lcSocketApi) << "Could not set pin state of" << data.folderRelativePath << "to online only"; - } - - // Trigger sync - data.folder->schedulePathForLocalDiscovery(data.folderRelativePath); - data.folder->scheduleThisFolderSoon(); - } -} - -void SocketApi::copyUrlToClipboard(const QString &link) -{ - QApplication::clipboard()->setText(link); -} - -void SocketApi::command_RESOLVE_CONFLICT(const QString &localFile, SocketListener *) -{ - const auto fileData = FileData::get(localFile); - if (!fileData.folder || !Utility::isConflictFile(fileData.folderRelativePath)) - return; // should not have shown menu item - - const auto conflictedRelativePath = fileData.folderRelativePath; - const auto baseRelativePath = fileData.folder->journalDb()->conflictFileBaseName(fileData.folderRelativePath.toUtf8()); - - const auto dir = QDir(fileData.folder->path()); - const auto conflictedPath = dir.filePath(conflictedRelativePath); - const auto basePath = dir.filePath(baseRelativePath); - - const auto baseName = QFileInfo(basePath).fileName(); - -#ifndef OWNCLOUD_TEST - ConflictDialog dialog; - dialog.setBaseFilename(baseName); - dialog.setLocalVersionFilename(conflictedPath); - dialog.setRemoteVersionFilename(basePath); - if (dialog.exec() == ConflictDialog::Accepted) { - fileData.folder->scheduleThisFolderSoon(); - } -#endif -} - -void SocketApi::command_DELETE_ITEM(const QString &localFile, SocketListener *) -{ - ConflictSolver solver; - solver.setLocalVersionFilename(localFile); - solver.exec(ConflictSolver::KeepRemoteVersion); -} - -void SocketApi::command_MOVE_ITEM(const QString &localFile, SocketListener *) -{ - const auto fileData = FileData::get(localFile); - const auto parentDir = fileData.parentFolder(); - if (!fileData.folder) - return; // should not have shown menu item - - QString defaultDirAndName = fileData.folderRelativePath; - - // If it's a conflict, we want to save it under the base name by default - if (Utility::isConflictFile(defaultDirAndName)) { - defaultDirAndName = fileData.folder->journalDb()->conflictFileBaseName(fileData.folderRelativePath.toUtf8()); - } - - // If the parent doesn't accept new files, go to the root of the sync folder - QFileInfo fileInfo(localFile); - const auto parentRecord = parentDir.journalRecord(); - if ((fileInfo.isFile() && !parentRecord._remotePerm.hasPermission(RemotePermissions::CanAddFile)) - || (fileInfo.isDir() && !parentRecord._remotePerm.hasPermission(RemotePermissions::CanAddSubDirectories))) { - defaultDirAndName = QFileInfo(defaultDirAndName).fileName(); - } - - // Add back the folder path - defaultDirAndName = QDir(fileData.folder->path()).filePath(defaultDirAndName); - - const auto target = QFileDialog::getSaveFileName( - nullptr, - tr("Select new location …"), - defaultDirAndName, - QString(), nullptr, QFileDialog::HideNameFilterDetails); - if (target.isEmpty()) - return; - - ConflictSolver solver; - solver.setLocalVersionFilename(localFile); - solver.setRemoteVersionFilename(target); -} - -void SocketApi::emailPrivateLink(const QString &link) -{ - Utility::openEmailComposer( - tr("I shared something with you"), - link, - nullptr); -} - -void OCC::SocketApi::openPrivateLink(const QString &link) -{ - Utility::openBrowser(link); -} - -void SocketApi::command_GET_STRINGS(const QString &argument, SocketListener *listener) -{ - static std::array, 5> strings { { - { "SHARE_MENU_TITLE", tr("Share options") }, - { "CONTEXT_MENU_TITLE", Theme::instance()->appNameGUI() }, - { "COPY_PRIVATE_LINK_MENU_TITLE", tr("Copy private link to clipboard") }, - { "EMAIL_PRIVATE_LINK_MENU_TITLE", tr("Send private link by email …") }, - { "CONTEXT_MENU_ICON", APPLICATION_ICON_NAME}, - } }; - listener->sendMessage(QString("GET_STRINGS:BEGIN")); - for (const auto& key_value : strings) { - if (argument.isEmpty() || argument == QLatin1String(key_value.first)) { - listener->sendMessage(QString("STRING:%1:%2").arg(key_value.first, key_value.second)); - } - } - listener->sendMessage(QString("GET_STRINGS:END")); -} - -void SocketApi::sendSharingContextMenuOptions(const FileData &fileData, SocketListener *listener, bool enabled) -{ - auto record = fileData.journalRecord(); - bool isOnTheServer = record.isValid(); - auto flagString = isOnTheServer && enabled ? QLatin1String("::") : QLatin1String(":d:"); - - auto capabilities = fileData.folder->accountState()->account()->capabilities(); - auto theme = Theme::instance(); - if (!capabilities.shareAPI() || !(theme->userGroupSharing() || (theme->linkSharing() && capabilities.sharePublicLink()))) - return; - - // If sharing is globally disabled, do not show any sharing entries. - // If there is no permission to share for this file, add a disabled entry saying so - if (isOnTheServer && !record._remotePerm.isNull() && !record._remotePerm.hasPermission(RemotePermissions::CanReshare)) { - listener->sendMessage(QLatin1String("MENU_ITEM:DISABLED:d:") + (!record.isDirectory() - ? tr("Resharing this file is not allowed") : tr("Resharing this folder is not allowed"))); - } else { - listener->sendMessage(QLatin1String("MENU_ITEM:SHARE") + flagString + tr("Share options")); - - // Do we have public links? - bool publicLinksEnabled = theme->linkSharing() && capabilities.sharePublicLink(); - - // Is is possible to create a public link without user choices? - bool canCreateDefaultPublicLink = publicLinksEnabled - && !capabilities.sharePublicLinkEnforceExpireDate() - && !capabilities.sharePublicLinkAskOptionalPassword() - && !capabilities.sharePublicLinkEnforcePassword(); - - if (canCreateDefaultPublicLink) { - listener->sendMessage(QLatin1String("MENU_ITEM:COPY_PUBLIC_LINK") + flagString + tr("Copy public link")); - } else if (publicLinksEnabled) { - listener->sendMessage(QLatin1String("MENU_ITEM:MANAGE_PUBLIC_LINKS") + flagString + tr("Copy public link")); - } - } - - listener->sendMessage(QLatin1String("MENU_ITEM:COPY_PRIVATE_LINK") + flagString + tr("Copy internal link")); - - // Disabled: only providing email option for private links would look odd, - // and the copy option is more general. - //listener->sendMessage(QLatin1String("MENU_ITEM:EMAIL_PRIVATE_LINK") + flagString + tr("Send private link by email …")); -} - -SocketApi::FileData SocketApi::FileData::get(const QString &localFile) -{ - FileData data; - - data.localPath = QDir::cleanPath(localFile); - if (data.localPath.endsWith(QLatin1Char('/'))) - data.localPath.chop(1); - - data.folder = FolderMan::instance()->folderForPath(data.localPath); - if (!data.folder) - return data; - - data.folderRelativePath = data.localPath.mid(data.folder->cleanPath().length() + 1); - data.serverRelativePath = QDir(data.folder->remotePath()).filePath(data.folderRelativePath); - QString virtualFileExt = QStringLiteral(APPLICATION_DOTVIRTUALFILE_SUFFIX); - if (data.serverRelativePath.endsWith(virtualFileExt)) { - data.serverRelativePath.chop(virtualFileExt.size()); - } - return data; -} - -QString SocketApi::FileData::folderRelativePathNoVfsSuffix() const -{ - auto result = folderRelativePath; - QString virtualFileExt = QStringLiteral(APPLICATION_DOTVIRTUALFILE_SUFFIX); - if (result.endsWith(virtualFileExt)) { - result.chop(virtualFileExt.size()); - } - return result; -} - -SyncFileStatus SocketApi::FileData::syncFileStatus() const -{ - if (!folder) - return SyncFileStatus::StatusNone; - return folder->syncEngine().syncFileStatusTracker().fileStatus(folderRelativePath); -} - -SyncJournalFileRecord SocketApi::FileData::journalRecord() const -{ - SyncJournalFileRecord record; - if (!folder) - return record; - folder->journalDb()->getFileRecord(folderRelativePath, &record); - return record; -} - -SocketApi::FileData SocketApi::FileData::parentFolder() const -{ - return FileData::get(QFileInfo(localPath).dir().path().toUtf8()); -} - -void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListener *listener) -{ - listener->sendMessage(QString("GET_MENU_ITEMS:BEGIN")); - const QStringList files = split(argument); - - // Find the common sync folder. - // syncFolder will be null if files are in different folders. - Folder *syncFolder = nullptr; - for (const auto &file : files) { - auto folder = FolderMan::instance()->folderForPath(file); - if (folder != syncFolder) { - if (!syncFolder) { - syncFolder = folder; - } else { - syncFolder = nullptr; - break; - } - } - } - - // Sharing actions show for single files only - if (syncFolder && files.size() == 1 && syncFolder->accountState()->isConnected()) { - QString systemPath = QDir::cleanPath(argument); - if (systemPath.endsWith(QLatin1Char('/'))) { - systemPath.truncate(systemPath.length() - 1); - } - - FileData fileData = FileData::get(argument); - const auto record = fileData.journalRecord(); - const bool isOnTheServer = record.isValid(); - const auto isE2eEncryptedPath = fileData.journalRecord()._isE2eEncrypted || !fileData.journalRecord()._e2eMangledName.isEmpty(); - auto flagString = isOnTheServer && !isE2eEncryptedPath ? QLatin1String("::") : QLatin1String(":d:"); - - DirectEditor* editor = getDirectEditorForLocalFile(fileData.localPath); - if (editor) { - //listener->sendMessage(QLatin1String("MENU_ITEM:EDIT") + flagString + tr("Edit via ") + editor->name()); - listener->sendMessage(QLatin1String("MENU_ITEM:EDIT") + flagString + tr("Edit")); - } else { - listener->sendMessage(QLatin1String("MENU_ITEM:OPEN_PRIVATE_LINK") + flagString + tr("Open in browser")); - } - - sendSharingContextMenuOptions(fileData, listener, !isE2eEncryptedPath); - - // Conflict files get conflict resolution actions - bool isConflict = Utility::isConflictFile(fileData.folderRelativePath); - if (isConflict || !isOnTheServer) { - // Check whether this new file is in a read-only directory - QFileInfo fileInfo(fileData.localPath); - const auto parentDir = fileData.parentFolder(); - const auto parentRecord = parentDir.journalRecord(); - const bool canAddToDir = - !parentRecord.isValid() // We're likely at the root of the sync folder, got to assume we can add there - || (fileInfo.isFile() && parentRecord._remotePerm.hasPermission(RemotePermissions::CanAddFile)) - || (fileInfo.isDir() && parentRecord._remotePerm.hasPermission(RemotePermissions::CanAddSubDirectories)); - const bool canChangeFile = - !isOnTheServer - || (record._remotePerm.hasPermission(RemotePermissions::CanDelete) - && record._remotePerm.hasPermission(RemotePermissions::CanMove) - && record._remotePerm.hasPermission(RemotePermissions::CanRename)); - - if (isConflict && canChangeFile) { - if (canAddToDir) { - listener->sendMessage(QLatin1String("MENU_ITEM:RESOLVE_CONFLICT::") + tr("Resolve conflict …")); - } else { - if (isOnTheServer) { - // Uploaded conflict file in read-only directory - listener->sendMessage(QLatin1String("MENU_ITEM:MOVE_ITEM::") + tr("Move and rename …")); - } else { - // Local-only conflict file in a read-only dir - listener->sendMessage(QLatin1String("MENU_ITEM:MOVE_ITEM::") + tr("Move, rename and upload …")); - } - listener->sendMessage(QLatin1String("MENU_ITEM:DELETE_ITEM::") + tr("Delete local changes")); - } - } - - // File in a read-only directory? - if (!isConflict && !isOnTheServer && !canAddToDir) { - listener->sendMessage(QLatin1String("MENU_ITEM:MOVE_ITEM::") + tr("Move and upload …")); - listener->sendMessage(QLatin1String("MENU_ITEM:DELETE_ITEM::") + tr("Delete")); - } - } - } - - // File availability actions - if (syncFolder - && syncFolder->virtualFilesEnabled() - && syncFolder->vfs().socketApiPinStateActionsShown()) { - ENFORCE(!files.isEmpty()); - - // Determine the combined availability status of the files - auto combined = Optional(); - auto merge = [](VfsItemAvailability lhs, VfsItemAvailability rhs) { - if (lhs == rhs) - return lhs; - if (int(lhs) > int(rhs)) - std::swap(lhs, rhs); // reduce cases ensuring lhs < rhs - if (lhs == VfsItemAvailability::AlwaysLocal && rhs == VfsItemAvailability::AllHydrated) - return VfsItemAvailability::AllHydrated; - if (lhs == VfsItemAvailability::AllDehydrated && rhs == VfsItemAvailability::OnlineOnly) - return VfsItemAvailability::AllDehydrated; - return VfsItemAvailability::Mixed; - }; - for (const auto &file : files) { - auto fileData = FileData::get(file); - auto availability = syncFolder->vfs().availability(fileData.folderRelativePath); - if (!availability) { - if (availability.error() == Vfs::AvailabilityError::DbError) - availability = VfsItemAvailability::Mixed; - if (availability.error() == Vfs::AvailabilityError::NoSuchItem) - continue; - } - if (!combined) { - combined = *availability; - } else { - combined = merge(*combined, *availability); - } - } - - // TODO: Should be a submenu, should use icons - auto makePinContextMenu = [&](bool makeAvailableLocally, bool freeSpace) { - listener->sendMessage(QLatin1String("MENU_ITEM:CURRENT_PIN:d:") - + Utility::vfsCurrentAvailabilityText(*combined)); - listener->sendMessage(QLatin1String("MENU_ITEM:MAKE_AVAILABLE_LOCALLY:") - + (makeAvailableLocally ? QLatin1String(":") : QLatin1String("d:")) - + Utility::vfsPinActionText()); - listener->sendMessage(QLatin1String("MENU_ITEM:MAKE_ONLINE_ONLY:") - + (freeSpace ? QLatin1String(":") : QLatin1String("d:")) - + Utility::vfsFreeSpaceActionText()); - }; - - if (combined) { - switch (*combined) { - case VfsItemAvailability::AlwaysLocal: - makePinContextMenu(false, true); - break; - case VfsItemAvailability::AllHydrated: - case VfsItemAvailability::Mixed: - makePinContextMenu(true, true); - break; - case VfsItemAvailability::AllDehydrated: - case VfsItemAvailability::OnlineOnly: - makePinContextMenu(true, false); - break; - } - } - } - - listener->sendMessage(QString("GET_MENU_ITEMS:END")); -} - -DirectEditor* SocketApi::getDirectEditorForLocalFile(const QString &localFile) -{ - FileData fileData = FileData::get(localFile); - auto capabilities = fileData.folder->accountState()->account()->capabilities(); - - if (fileData.folder && fileData.folder->accountState()->isConnected()) { - const auto record = fileData.journalRecord(); - const auto mimeMatchMode = record.isVirtualFile() ? QMimeDatabase::MatchExtension : QMimeDatabase::MatchDefault; - - QMimeDatabase db; - QMimeType type = db.mimeTypeForFile(localFile, mimeMatchMode); - - DirectEditor* editor = capabilities.getDirectEditorForMimetype(type); - if (!editor) { - editor = capabilities.getDirectEditorForOptionalMimetype(type); - } - return editor; - } - - return nullptr; -} - -#if GUI_TESTING -void SocketApi::command_ASYNC_LIST_WIDGETS(const QSharedPointer &job) -{ - QString response; - for (auto &widget : allObjects(QApplication::allWidgets())) { - auto objectName = widget->objectName(); - if (!objectName.isEmpty()) { - response += objectName + ":" + widget->property("text").toString() + ", "; - } - } - job->resolve(response); -} - -void SocketApi::command_ASYNC_INVOKE_WIDGET_METHOD(const QSharedPointer &job) -{ - auto &arguments = job->arguments(); - - auto widget = findWidget(arguments["objectName"].toString()); - if (!widget) { - job->reject(QLatin1String("widget not found")); - return; - } - - QMetaObject::invokeMethod(widget, arguments["method"].toString().toUtf8().constData()); - job->resolve(); -} - -void SocketApi::command_ASYNC_GET_WIDGET_PROPERTY(const QSharedPointer &job) -{ - QString widgetName = job->arguments()[QLatin1String("objectName")].toString(); - auto widget = findWidget(widgetName); - if (!widget) { - QString message = QString(QLatin1String("Widget not found: 2: %1")).arg(widgetName); - job->reject(message); - return; - } - - auto propertyName = job->arguments()[QLatin1String("property")].toString(); - - auto segments = propertyName.split('.'); - - QObject* currentObject = widget; - QString value; - for(int i = 0;iproperty(segment.toUtf8().constData()); - - if(var.canConvert()) { - var.convert(QMetaType::QString); - value = var.value(); - break; - } - - auto tmpObject = var.value(); - if(tmpObject) { - currentObject = tmpObject; - } else { - QString message = QString(QLatin1String("Widget not found: 3: %1")).arg(widgetName); - job->reject(message); - return; - } - } - - job->resolve(value); -} - -void SocketApi::command_ASYNC_SET_WIDGET_PROPERTY(const QSharedPointer &job) -{ - auto &arguments = job->arguments(); - QString widgetName = arguments["objectName"].toString(); - auto widget = findWidget(widgetName); - if (!widget) { - QString message = QString(QLatin1String("Widget not found: 4: %1")).arg(widgetName); - job->reject(message); - return; - } - widget->setProperty(arguments["property"].toString().toUtf8().constData(), - arguments["value"]); - - job->resolve(); -} - -void SocketApi::command_ASYNC_WAIT_FOR_WIDGET_SIGNAL(const QSharedPointer &job) -{ - auto &arguments = job->arguments(); - QString widgetName = arguments["objectName"].toString(); - auto widget = findWidget(arguments["objectName"].toString()); - if (!widget) { - QString message = QString(QLatin1String("Widget not found: 5: %1")).arg(widgetName); - job->reject(message); - return; - } - - ListenerClosure *closure = new ListenerClosure([job]() { job->resolve("signal emitted"); }); - - auto signalSignature = arguments["signalSignature"].toString(); - signalSignature.prepend("2"); - auto utf8 = signalSignature.toUtf8(); - auto signalSignatureFinal = utf8.constData(); - connect(widget, signalSignatureFinal, closure, SLOT(closureSlot()), Qt::QueuedConnection); -} - -void SocketApi::command_ASYNC_TRIGGER_MENU_ACTION(const QSharedPointer &job) -{ - auto &arguments = job->arguments(); - - auto objectName = arguments["objectName"].toString(); - auto widget = findWidget(objectName); - if (!widget) { - QString message = QString(QLatin1String("Object not found: 1: %1")).arg(objectName); - job->reject(message); - return; - } - - auto children = widget->findChildren(); - for (auto childWidget : children) { - // foo is the popupwidget! - auto actions = childWidget->actions(); - for (auto action : actions) { - if (action->objectName() == arguments["actionName"].toString()) { - action->trigger(); - - job->resolve("action found"); - return; - } - } - } - - QString message = QString(QLatin1String("Action not found: 1: %1")).arg(arguments["actionName"].toString()); - job->reject(message); -} - -void SocketApi::command_ASYNC_ASSERT_ICON_IS_EQUAL(const QSharedPointer &job) -{ - auto widget = findWidget(job->arguments()[QLatin1String("queryString")].toString()); - if (!widget) { - QString message = QString(QLatin1String("Object not found: 6: %1")).arg(job->arguments()["queryString"].toString()); - job->reject(message); - return; - } - - auto propertyName = job->arguments()[QLatin1String("propertyPath")].toString(); - - auto segments = propertyName.split('.'); - - QObject* currentObject = widget; - QIcon value; - for(int i = 0;iproperty(segment.toUtf8().constData()); - - if(var.canConvert()) { - var.convert(QMetaType::QIcon); - value = var.value(); - break; - } - - auto tmpObject = var.value(); - if(tmpObject) { - currentObject = tmpObject; - } else { - job->reject(QString(QLatin1String("Icon not found: %1")).arg(propertyName)); - } - } - - auto iconName = job->arguments()[QLatin1String("iconName")].toString(); - if (value.name() == iconName) { - job->resolve(); - } else { - job->reject("iconName " + iconName + " does not match: " + value.name()); - } - -} -#endif - -QString SocketApi::buildRegisterPathMessage(const QString &path) -{ - QFileInfo fi(path); - QString message = QLatin1String("REGISTER_PATH:"); - message.append(QDir::toNativeSeparators(fi.absoluteFilePath())); - return message; -} - -} // namespace OCC - -#include "socketapi.moc" diff --git a/src/gui/socketapi.h b/src/gui/socketapi.h deleted file mode 100644 index 1c2ef49f7..000000000 --- a/src/gui/socketapi.h +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright (C) by Dominik Schmidt - * - * 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. - */ - -#ifndef SOCKETAPI_H -#define SOCKETAPI_H - -#include "syncfileitem.h" -#include "common/syncfilestatus.h" -#include "sharedialog.h" // for the ShareDialogStartPage -#include "common/syncjournalfilerecord.h" - -#include "config.h" - -#if defined(Q_OS_MAC) -#include "socketapisocket_mac.h" -#else -#include -using SocketApiServer = QLocalServer; -#endif - -class QUrl; -class QLocalSocket; -class QStringList; - -namespace OCC { - -class SyncFileStatus; -class Folder; -class SocketListener; -class DirectEditor; -class SocketApiJob; - -Q_DECLARE_LOGGING_CATEGORY(lcSocketApi) - -/** - * @brief The SocketApi class - * @ingroup gui - */ -class SocketApi : public QObject -{ - Q_OBJECT - -public: - explicit SocketApi(QObject *parent = nullptr); - virtual ~SocketApi(); - -public slots: - void slotUpdateFolderView(Folder *f); - void slotUnregisterPath(const QString &alias); - void slotRegisterPath(const QString &alias); - void broadcastStatusPushMessage(const QString &systemPath, SyncFileStatus fileStatus); - -signals: - void shareCommandReceived(const QString &sharePath, const QString &localPath, ShareDialogStartPage startPage); - -private slots: - void slotNewConnection(); - void onLostConnection(); - void slotSocketDestroyed(QObject *obj); - void slotReadSocket(); - - static void copyUrlToClipboard(const QString &link); - static void emailPrivateLink(const QString &link); - static void openPrivateLink(const QString &link); - -private: - // Helper structure for getting information on a file - // based on its local path - used for nearly all remote - // actions. - struct FileData - { - static FileData get(const QString &localFile); - SyncFileStatus syncFileStatus() const; - SyncJournalFileRecord journalRecord() const; - FileData parentFolder() const; - - // Relative path of the file locally, without any vfs suffix - QString folderRelativePathNoVfsSuffix() const; - - Folder *folder; - // Absolute path of the file locally. (May be a virtual file) - QString localPath; - // Relative path of the file locally, as in the DB. (May be a virtual file) - QString folderRelativePath; - // Path of the file on the server (In case of virtual file, it points to the actual file) - QString serverRelativePath; - }; - - void broadcastMessage(const QString &msg, bool doWait = false); - - // opens share dialog, sends reply - void processShareRequest(const QString &localFile, SocketListener *listener, ShareDialogStartPage startPage); - - Q_INVOKABLE void command_RETRIEVE_FOLDER_STATUS(const QString &argument, SocketListener *listener); - Q_INVOKABLE void command_RETRIEVE_FILE_STATUS(const QString &argument, SocketListener *listener); - - Q_INVOKABLE void command_VERSION(const QString &argument, SocketListener *listener); - - Q_INVOKABLE void command_SHARE_MENU_TITLE(const QString &argument, SocketListener *listener); - - // The context menu actions - Q_INVOKABLE void command_SHARE(const QString &localFile, SocketListener *listener); - Q_INVOKABLE void command_MANAGE_PUBLIC_LINKS(const QString &localFile, SocketListener *listener); - Q_INVOKABLE void command_COPY_PUBLIC_LINK(const QString &localFile, SocketListener *listener); - Q_INVOKABLE void command_COPY_PRIVATE_LINK(const QString &localFile, SocketListener *listener); - Q_INVOKABLE void command_EMAIL_PRIVATE_LINK(const QString &localFile, SocketListener *listener); - Q_INVOKABLE void command_OPEN_PRIVATE_LINK(const QString &localFile, SocketListener *listener); - Q_INVOKABLE void command_MAKE_AVAILABLE_LOCALLY(const QString &filesArg, SocketListener *listener); - Q_INVOKABLE void command_MAKE_ONLINE_ONLY(const QString &filesArg, SocketListener *listener); - Q_INVOKABLE void command_RESOLVE_CONFLICT(const QString &localFile, SocketListener *listener); - Q_INVOKABLE void command_DELETE_ITEM(const QString &localFile, SocketListener *listener); - Q_INVOKABLE void command_MOVE_ITEM(const QString &localFile, SocketListener *listener); - - // Windows Shell / Explorer pinning fallbacks, see issue: https://github.com/nextcloud/desktop/issues/1599 -#ifdef Q_OS_WIN - Q_INVOKABLE void command_COPYASPATH(const QString &localFile, SocketListener *listener); - Q_INVOKABLE void command_OPENNEWWINDOW(const QString &localFile, SocketListener *listener); - Q_INVOKABLE void command_OPEN(const QString &localFile, SocketListener *listener); -#endif - - // Fetch the private link and call targetFun - void fetchPrivateLinkUrlHelper(const QString &localFile, const std::function &targetFun); - - /** Sends translated/branded strings that may be useful to the integration */ - Q_INVOKABLE void command_GET_STRINGS(const QString &argument, SocketListener *listener); - - // Sends the context menu options relating to sharing to listener - void sendSharingContextMenuOptions(const FileData &fileData, SocketListener *listener, bool enabled); - - /** Send the list of menu item. (added in version 1.1) - * argument is a list of files for which the menu should be shown, separated by '\x1e' - * Reply with GET_MENU_ITEMS:BEGIN - * followed by several MENU_ITEM:[Action]:[flag]:[Text] - * If flag contains 'd', the menu should be disabled - * and ends with GET_MENU_ITEMS:END - */ - Q_INVOKABLE void command_GET_MENU_ITEMS(const QString &argument, SocketListener *listener); - - /// Direct Editing - Q_INVOKABLE void command_EDIT(const QString &localFile, SocketListener *listener); - DirectEditor* getDirectEditorForLocalFile(const QString &localFile); - -#if GUI_TESTING - Q_INVOKABLE void command_ASYNC_ASSERT_ICON_IS_EQUAL(const QSharedPointer &job); - Q_INVOKABLE void command_ASYNC_LIST_WIDGETS(const QSharedPointer &job); - Q_INVOKABLE void command_ASYNC_INVOKE_WIDGET_METHOD(const QSharedPointer &job); - Q_INVOKABLE void command_ASYNC_GET_WIDGET_PROPERTY(const QSharedPointer &job); - Q_INVOKABLE void command_ASYNC_SET_WIDGET_PROPERTY(const QSharedPointer &job); - Q_INVOKABLE void command_ASYNC_WAIT_FOR_WIDGET_SIGNAL(const QSharedPointer &job); - Q_INVOKABLE void command_ASYNC_TRIGGER_MENU_ACTION(const QSharedPointer &job); -#endif - - QString buildRegisterPathMessage(const QString &path); - - QSet _registeredAliases; - QList _listeners; - SocketApiServer _localServer; -}; -} - -#endif // SOCKETAPI_H diff --git a/src/gui/socketapi/CMakeLists.txt b/src/gui/socketapi/CMakeLists.txt new file mode 100644 index 000000000..9290269ee --- /dev/null +++ b/src/gui/socketapi/CMakeLists.txt @@ -0,0 +1,8 @@ +target_sources(nextcloudCore PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/socketapi.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/socketuploadjob.cpp +) + +if( APPLE ) + target_sources(nextcloudCore PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/socketapisocket_mac.mm) +endif() diff --git a/src/gui/socketapi/socketapi.cpp b/src/gui/socketapi/socketapi.cpp new file mode 100644 index 000000000..4335c4e10 --- /dev/null +++ b/src/gui/socketapi/socketapi.cpp @@ -0,0 +1,1434 @@ +/* + * Copyright (C) by Dominik Schmidt + * Copyright (C) by Klaas Freitag + * Copyright (C) by Roeland Jago Douma + * + * 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. + */ + +#include "socketapi.h" +#include "socketapi_p.h" +#include "socketapi/socketuploadjob.h" + +#include "conflictdialog.h" +#include "conflictsolver.h" + +#include "config.h" +#include "configfile.h" +#include "folderman.h" +#include "folder.h" +#include "theme.h" +#include "common/syncjournalfilerecord.h" +#include "syncengine.h" +#include "syncfileitem.h" +#include "filesystem.h" +#include "version.h" +#include "account.h" +#include "accountstate.h" +#include "account.h" +#include "accountmanager.h" +#include "capabilities.h" +#include "common/asserts.h" +#include "guiutility.h" +#ifndef OWNCLOUD_TEST +#include "sharemanager.h" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +#ifdef Q_OS_MAC +#include +#endif + + +// This is the version that is returned when the client asks for the VERSION. +// The first number should be changed if there is an incompatible change that breaks old clients. +// The second number should be changed when there are new features. +#define MIRALL_SOCKET_API_VERSION "1.1" + +namespace { + +const QLatin1Char RecordSeparator() +{ + return QLatin1Char('\x1e'); +} + +QStringList split(const QString &data) +{ + // TODO: string ref? + return data.split(RecordSeparator()); +} + +#if GUI_TESTING + +using namespace OCC; + +QList allObjects(const QList &widgets) +{ + QList objects; + std::copy(widgets.constBegin(), widgets.constEnd(), std::back_inserter(objects)); + + objects << qApp; + + return objects; +} + +QObject *findWidget(const QString &queryString, const QList &widgets = QApplication::allWidgets()) +{ + auto objects = allObjects(widgets); + + QList::const_iterator foundWidget; + + if (queryString.contains('>')) { + qCDebug(lcSocketApi) << "queryString contains >"; + + auto subQueries = queryString.split('>', QString::SkipEmptyParts); + Q_ASSERT(subQueries.count() == 2); + + auto parentQueryString = subQueries[0].trimmed(); + qCDebug(lcSocketApi) << "Find parent: " << parentQueryString; + auto parent = findWidget(parentQueryString); + + if (!parent) { + return nullptr; + } + + auto childQueryString = subQueries[1].trimmed(); + auto child = findWidget(childQueryString, parent->findChildren()); + qCDebug(lcSocketApi) << "found child: " << !!child; + return child; + + } else if (queryString.startsWith('#')) { + auto objectName = queryString.mid(1); + qCDebug(lcSocketApi) << "find objectName: " << objectName; + foundWidget = std::find_if(objects.constBegin(), objects.constEnd(), [&](QObject *widget) { + return widget->objectName() == objectName; + }); + } else { + QList matches; + std::copy_if(objects.constBegin(), objects.constEnd(), std::back_inserter(matches), [&](QObject *widget) { + return widget->inherits(queryString.toLatin1()); + }); + + std::for_each(matches.constBegin(), matches.constEnd(), [](QObject *w) { + if (!w) + return; + qCDebug(lcSocketApi) << "WIDGET: " << w->objectName() << w->metaObject()->className(); + }); + + if (matches.empty()) { + return nullptr; + } + return matches[0]; + } + + if (foundWidget == objects.constEnd()) { + return nullptr; + } + + return *foundWidget; +} +#endif + +static inline QString removeTrailingSlash(QString path) +{ + Q_ASSERT(path.endsWith(QLatin1Char('/'))); + path.truncate(path.length() - 1); + return path; +} + +static QString buildMessage(const QString &verb, const QString &path, const QString &status = QString()) +{ + QString msg(verb); + + if (!status.isEmpty()) { + msg.append(QLatin1Char(':')); + msg.append(status); + } + if (!path.isEmpty()) { + msg.append(QLatin1Char(':')); + QFileInfo fi(path); + msg.append(QDir::toNativeSeparators(fi.absoluteFilePath())); + } + return msg; +} +} + +namespace OCC { + +Q_LOGGING_CATEGORY(lcSocketApi, "nextcloud.gui.socketapi", QtInfoMsg) +Q_LOGGING_CATEGORY(lcPublicLink, "nextcloud.gui.socketapi.publiclink", QtInfoMsg) + + +void SocketListener::sendMessage(const QString &function, const QJsonObject &obj, bool doWait) const +{ + sendMessage(function + QLatin1Char(':') + QJsonDocument(obj).toJson(QJsonDocument::Compact), doWait); +} + +void SocketListener::sendMessage(const QString &message, bool doWait) const +{ + if (!socket) { + qCWarning(lcSocketApi) << "Not sending message to dead socket:" << message; + return; + } + + qCDebug(lcSocketApi) << "Sending SocketAPI message -->" << message << "to" << socket; + QString localMessage = message; + if (!localMessage.endsWith(QLatin1Char('\n'))) { + localMessage.append(QLatin1Char('\n')); + } + + QByteArray bytesToSend = localMessage.toUtf8(); + qint64 sent = socket->write(bytesToSend); + if (doWait) { + socket->waitForBytesWritten(1000); + } + if (sent != bytesToSend.length()) { + qCWarning(lcSocketApi) << "Could not send all data on socket for " << localMessage; + } +} + +struct ListenerHasSocketPred +{ + QIODevice *socket; + ListenerHasSocketPred(QIODevice *socket) + : socket(socket) + { + } + bool operator()(const SocketListener &listener) const { return listener.socket == socket; } +}; + +SocketApi::SocketApi(QObject *parent) + : QObject(parent) +{ + QString socketPath; + + qRegisterMetaType("SocketListener*"); + qRegisterMetaType>("QSharedPointer"); + + if (Utility::isWindows()) { + socketPath = QLatin1String(R"(\\.\pipe\)") + + QLatin1String(APPLICATION_EXECUTABLE) + + QLatin1String("-") + + QString::fromLocal8Bit(qgetenv("USERNAME")); + // TODO: once the windows extension supports multiple + // client connections, switch back to the theme name + // See issue #2388 + // + Theme::instance()->appName(); + } else if (Utility::isMac()) { + // This must match the code signing Team setting of the extension + // Example for developer builds (with ad-hoc signing identity): "" "com.owncloud.desktopclient" ".socketApi" + // Example for official signed packages: "9B5WD74GWJ." "com.owncloud.desktopclient" ".socketApi" + socketPath = SOCKETAPI_TEAM_IDENTIFIER_PREFIX APPLICATION_REV_DOMAIN ".socketApi"; +#ifdef Q_OS_MAC + CFURLRef url = (CFURLRef)CFAutorelease((CFURLRef)CFBundleCopyBundleURL(CFBundleGetMainBundle())); + QString bundlePath = QUrl::fromCFURL(url).path(); + + auto _system = [](const QString &cmd, const QStringList &args) { + QProcess process; + process.setProcessChannelMode(QProcess::MergedChannels); + process.start(cmd, args); + if (!process.waitForFinished()) { + qCWarning(lcSocketApi) << "Failed to load shell extension:" << cmd << args.join(" ") << process.errorString(); + } else { + qCInfo(lcSocketApi) << (process.exitCode() != 0 ? "Failed to load" : "Loaded") << "shell extension:" << cmd << args.join(" ") << process.readAll(); + } + }; + // Add it again. This was needed for Mojave to trigger a load. + _system(QStringLiteral("pluginkit"), { QStringLiteral("-a"), QStringLiteral("%1Contents/PlugIns/FinderSyncExt.appex/").arg(bundlePath) }); + // Tell Finder to use the Extension (checking it from System Preferences -> Extensions) + _system(QStringLiteral("pluginkit"), { QStringLiteral("-e"), QStringLiteral("use"), QStringLiteral("-i"), QStringLiteral(APPLICATION_REV_DOMAIN ".FinderSyncExt") }); + +#endif + } else if (Utility::isLinux() || Utility::isBSD()) { + QString runtimeDir; + runtimeDir = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation); + socketPath = runtimeDir + "/" + Theme::instance()->appName() + "/socket"; + } else { + qCWarning(lcSocketApi) << "An unexpected system detected, this probably won't work."; + } + + SocketApiServer::removeServer(socketPath); + QFileInfo info(socketPath); + if (!info.dir().exists()) { + bool result = info.dir().mkpath("."); + qCDebug(lcSocketApi) << "creating" << info.dir().path() << result; + if (result) { + QFile::setPermissions(socketPath, + QFile::Permissions(QFile::ReadOwner + QFile::WriteOwner + QFile::ExeOwner)); + } + } + if (!_localServer.listen(socketPath)) { + qCWarning(lcSocketApi) << "can't start server" << socketPath; + } else { + qCInfo(lcSocketApi) << "server started, listening at " << socketPath; + } + + connect(&_localServer, &SocketApiServer::newConnection, this, &SocketApi::slotNewConnection); + + // folder watcher + connect(FolderMan::instance(), &FolderMan::folderSyncStateChange, this, &SocketApi::slotUpdateFolderView); +} + +SocketApi::~SocketApi() +{ + qCDebug(lcSocketApi) << "dtor"; + _localServer.close(); + // All remaining sockets will be destroyed with _localServer, their parent + ASSERT(_listeners.isEmpty() || _listeners.first().socket->parent() == &_localServer); + _listeners.clear(); +} + +void SocketApi::slotNewConnection() +{ + // Note that on macOS this is not actually a line-based QIODevice, it's a SocketApiSocket which is our + // custom message based macOS IPC. + QIODevice *socket = _localServer.nextPendingConnection(); + + if (!socket) { + return; + } + qCInfo(lcSocketApi) << "New connection" << socket; + connect(socket, &QIODevice::readyRead, this, &SocketApi::slotReadSocket); + connect(socket, SIGNAL(disconnected()), this, SLOT(onLostConnection())); + connect(socket, &QObject::destroyed, this, &SocketApi::slotSocketDestroyed); + ASSERT(socket->readAll().isEmpty()); + + _listeners.append(SocketListener(socket)); + SocketListener &listener = _listeners.last(); + + foreach (Folder *f, FolderMan::instance()->map()) { + if (f->canSync()) { + QString message = buildRegisterPathMessage(removeTrailingSlash(f->path())); + qCInfo(lcSocketApi) << "Trying to send SocketAPI Register Path Message -->" << message << "to" << listener.socket; + listener.sendMessage(message); + } + } +} + +void SocketApi::onLostConnection() +{ + qCInfo(lcSocketApi) << "Lost connection " << sender(); + sender()->deleteLater(); + + auto socket = qobject_cast(sender()); + ASSERT(socket); + _listeners.erase(std::remove_if(_listeners.begin(), _listeners.end(), ListenerHasSocketPred(socket)), _listeners.end()); +} + +void SocketApi::slotSocketDestroyed(QObject *obj) +{ + auto *socket = static_cast(obj); + _listeners.erase(std::remove_if(_listeners.begin(), _listeners.end(), ListenerHasSocketPred(socket)), _listeners.end()); +} + +void SocketApi::slotReadSocket() +{ + auto *socket = qobject_cast(sender()); + ASSERT(socket); + + // Find the SocketListener + // + // It's possible for the disconnected() signal to be triggered before + // the readyRead() signals are received - in that case there won't be a + // valid listener. We execute the handler anyway, but it will work with + // a SocketListener that doesn't send any messages. + static auto noListener = SocketListener(nullptr); + SocketListener *listener = &noListener; + auto listenerIt = std::find_if(_listeners.begin(), _listeners.end(), ListenerHasSocketPred(socket)); + if (listenerIt != _listeners.end()) { + listener = &*listenerIt; + } + + while (socket->canReadLine()) { + // Make sure to normalize the input from the socket to + // make sure that the path will match, especially on OS X. + const QString line = QString::fromUtf8(socket->readLine().trimmed()).normalized(QString::NormalizationForm_C); + qCInfo(lcSocketApi) << "Received SocketAPI message <--" << line << "from" << socket; + const QByteArray command = line.midRef(0, line.indexOf(QLatin1Char(':'))).toUtf8().toUpper().replace("/", "_"); + const QByteArray functionWithArguments = "command_" + command + (command.startsWith("ASYNC_") ? "(QSharedPointer)" : "(QString,SocketListener*)"); + const int indexOfMethod = staticMetaObject.indexOfMethod(functionWithArguments); + + const auto argument = line.midRef(command.length() + 1); + if (command.startsWith("ASYNC_")) { + auto arguments = argument.split('|'); + if (arguments.size() != 2) { + listener->sendMessage(QStringLiteral("argument count is wrong")); + return; + } + + auto json = QJsonDocument::fromJson(arguments[1].toUtf8()).object(); + + auto jobId = arguments[0]; + + auto socketApiJob = QSharedPointer( + new SocketApiJob(jobId.toString(), listener, json), &QObject::deleteLater); + if (indexOfMethod != -1) { + staticMetaObject.method(indexOfMethod) + .invoke(this, Qt::QueuedConnection, + Q_ARG(QSharedPointer, socketApiJob)); + } else { + qCWarning(lcSocketApi) << "The command is not supported by this version of the client:" << command + << "with argument:" << argument; + socketApiJob->reject(QStringLiteral("command not found")); + } + } else { + if (indexOfMethod != -1) { + // to ensure that listener is still valid we need to call it with Qt::DirectConnection + ASSERT(thread() == QThread::currentThread()) + staticMetaObject.method(indexOfMethod) + .invoke(this, Qt::DirectConnection, Q_ARG(QString, argument.toString()), + Q_ARG(SocketListener *, listener)); + } else { + qCWarning(lcSocketApi) << "The command is not supported by this version of the client:" << command << "with argument:" << argument; + } + } + } +} + +void SocketApi::slotRegisterPath(const QString &alias) +{ + // Make sure not to register twice to each connected client + if (_registeredAliases.contains(alias)) + return; + + Folder *f = FolderMan::instance()->folder(alias); + if (f) { + QString message = buildRegisterPathMessage(removeTrailingSlash(f->path())); + foreach (auto &listener, _listeners) { + qCInfo(lcSocketApi) << "Trying to send SocketAPI Register Path Message -->" << message << "to" << listener.socket; + listener.sendMessage(message); + } + } + + _registeredAliases.insert(alias); +} + +void SocketApi::slotUnregisterPath(const QString &alias) +{ + if (!_registeredAliases.contains(alias)) + return; + + Folder *f = FolderMan::instance()->folder(alias); + if (f) + broadcastMessage(buildMessage(QLatin1String("UNREGISTER_PATH"), removeTrailingSlash(f->path()), QString()), true); + + _registeredAliases.remove(alias); +} + +void SocketApi::slotUpdateFolderView(Folder *f) +{ + if (_listeners.isEmpty()) { + return; + } + + if (f) { + // do only send UPDATE_VIEW for a couple of status + if (f->syncResult().status() == SyncResult::SyncPrepare + || f->syncResult().status() == SyncResult::Success + || f->syncResult().status() == SyncResult::Paused + || f->syncResult().status() == SyncResult::Problem + || f->syncResult().status() == SyncResult::Error + || f->syncResult().status() == SyncResult::SetupError) { + QString rootPath = removeTrailingSlash(f->path()); + broadcastStatusPushMessage(rootPath, f->syncEngine().syncFileStatusTracker().fileStatus("")); + + broadcastMessage(buildMessage(QLatin1String("UPDATE_VIEW"), rootPath)); + } else { + qCDebug(lcSocketApi) << "Not sending UPDATE_VIEW for" << f->alias() << "because status() is" << f->syncResult().status(); + } + } +} + +void SocketApi::broadcastMessage(const QString &msg, bool doWait) +{ + foreach (auto &listener, _listeners) { + listener.sendMessage(msg, doWait); + } +} + +void SocketApi::processShareRequest(const QString &localFile, SocketListener *listener, ShareDialogStartPage startPage) +{ + auto theme = Theme::instance(); + + auto fileData = FileData::get(localFile); + auto shareFolder = fileData.folder; + if (!shareFolder) { + const QString message = QLatin1String("SHARE:NOP:") + QDir::toNativeSeparators(localFile); + // files that are not within a sync folder are not synced. + listener->sendMessage(message); + } else if (!shareFolder->accountState()->isConnected()) { + const QString message = QLatin1String("SHARE:NOTCONNECTED:") + QDir::toNativeSeparators(localFile); + // if the folder isn't connected, don't open the share dialog + listener->sendMessage(message); + } else if (!theme->linkSharing() && (!theme->userGroupSharing() || shareFolder->accountState()->account()->serverVersionInt() < Account::makeServerVersion(8, 2, 0))) { + const QString message = QLatin1String("SHARE:NOP:") + QDir::toNativeSeparators(localFile); + listener->sendMessage(message); + } else { + // If the file doesn't have a journal record, it might not be uploaded yet + if (!fileData.journalRecord().isValid()) { + const QString message = QLatin1String("SHARE:NOTSYNCED:") + QDir::toNativeSeparators(localFile); + listener->sendMessage(message); + return; + } + + auto &remotePath = fileData.serverRelativePath; + + // Can't share root folder + if (remotePath == "/") { + const QString message = QLatin1String("SHARE:CANNOTSHAREROOT:") + QDir::toNativeSeparators(localFile); + listener->sendMessage(message); + return; + } + + const QString message = QLatin1String("SHARE:OK:") + QDir::toNativeSeparators(localFile); + listener->sendMessage(message); + + emit shareCommandReceived(remotePath, fileData.localPath, startPage); + } +} + +void SocketApi::broadcastStatusPushMessage(const QString &systemPath, SyncFileStatus fileStatus) +{ + QString msg = buildMessage(QLatin1String("STATUS"), systemPath, fileStatus.toSocketAPIString()); + Q_ASSERT(!systemPath.endsWith('/')); + uint directoryHash = qHash(systemPath.left(systemPath.lastIndexOf('/'))); + foreach (auto &listener, _listeners) { + listener.sendMessageIfDirectoryMonitored(msg, directoryHash); + } +} + +void SocketApi::command_RETRIEVE_FOLDER_STATUS(const QString &argument, SocketListener *listener) +{ + // This command is the same as RETRIEVE_FILE_STATUS + command_RETRIEVE_FILE_STATUS(argument, listener); +} + +void SocketApi::command_RETRIEVE_FILE_STATUS(const QString &argument, SocketListener *listener) +{ + QString statusString; + + auto fileData = FileData::get(argument); + if (!fileData.folder) { + // this can happen in offline mode e.g.: nothing to worry about + statusString = QLatin1String("NOP"); + } else { + // The user probably visited this directory in the file shell. + // Let the listener know that it should now send status pushes for sibblings of this file. + QString directory = fileData.localPath.left(fileData.localPath.lastIndexOf('/')); + listener->registerMonitoredDirectory(qHash(directory)); + + SyncFileStatus fileStatus = fileData.syncFileStatus(); + statusString = fileStatus.toSocketAPIString(); + } + + const QString message = QLatin1String("STATUS:") % statusString % QLatin1Char(':') % QDir::toNativeSeparators(argument); + listener->sendMessage(message); +} + +void SocketApi::command_SHARE(const QString &localFile, SocketListener *listener) +{ + processShareRequest(localFile, listener, ShareDialogStartPage::UsersAndGroups); +} + +void SocketApi::command_MANAGE_PUBLIC_LINKS(const QString &localFile, SocketListener *listener) +{ + processShareRequest(localFile, listener, ShareDialogStartPage::PublicLinks); +} + +void SocketApi::command_VERSION(const QString &, SocketListener *listener) +{ + listener->sendMessage(QLatin1String("VERSION:" MIRALL_VERSION_STRING ":" MIRALL_SOCKET_API_VERSION)); +} + +void SocketApi::command_SHARE_MENU_TITLE(const QString &, SocketListener *listener) +{ + //listener->sendMessage(QLatin1String("SHARE_MENU_TITLE:") + tr("Share with %1", "parameter is Nextcloud").arg(Theme::instance()->appNameGUI())); + listener->sendMessage(QLatin1String("SHARE_MENU_TITLE:") + Theme::instance()->appNameGUI()); +} + +void SocketApi::command_EDIT(const QString &localFile, SocketListener *listener) +{ + Q_UNUSED(listener) + auto fileData = FileData::get(localFile); + if (!fileData.folder) { + qCWarning(lcSocketApi) << "Unknown path" << localFile; + return; + } + + auto record = fileData.journalRecord(); + if (!record.isValid()) + return; + + DirectEditor* editor = getDirectEditorForLocalFile(fileData.localPath); + if (!editor) + return; + + auto *job = new JsonApiJob(fileData.folder->accountState()->account(), QLatin1String("ocs/v2.php/apps/files/api/v1/directEditing/open"), this); + + QUrlQuery params; + params.addQueryItem("path", fileData.serverRelativePath); + params.addQueryItem("editorId", editor->id()); + job->addQueryParams(params); + job->usePOST(); + + QObject::connect(job, &JsonApiJob::jsonReceived, [](const QJsonDocument &json){ + auto data = json.object().value("ocs").toObject().value("data").toObject(); + auto url = QUrl(data.value("url").toString()); + + if(!url.isEmpty()) + Utility::openBrowser(url); + }); + job->start(); +} + +// don't pull the share manager into socketapi unittests +#ifndef OWNCLOUD_TEST + +class GetOrCreatePublicLinkShare : public QObject +{ + Q_OBJECT +public: + GetOrCreatePublicLinkShare(const AccountPtr &account, const QString &localFile, + QObject *parent) + : QObject(parent) + , _account(account) + , _shareManager(account) + , _localFile(localFile) + { + connect(&_shareManager, &ShareManager::sharesFetched, + this, &GetOrCreatePublicLinkShare::sharesFetched); + connect(&_shareManager, &ShareManager::linkShareCreated, + this, &GetOrCreatePublicLinkShare::linkShareCreated); + connect(&_shareManager, &ShareManager::linkShareRequiresPassword, + this, &GetOrCreatePublicLinkShare::linkShareRequiresPassword); + connect(&_shareManager, &ShareManager::serverError, + this, &GetOrCreatePublicLinkShare::serverError); + } + + void run() + { + qCDebug(lcPublicLink) << "Fetching shares"; + _shareManager.fetchShares(_localFile); + } + +private slots: + void sharesFetched(const QList> &shares) + { + auto shareName = SocketApi::tr("Context menu share"); + + // If there already is a context menu share, reuse it + for (const auto &share : shares) { + const auto linkShare = qSharedPointerDynamicCast(share); + if (!linkShare) + continue; + + if (linkShare->getName() == shareName) { + qCDebug(lcPublicLink) << "Found existing share, reusing"; + return success(linkShare->getLink().toString()); + } + } + + // otherwise create a new one + qCDebug(lcPublicLink) << "Creating new share"; + _shareManager.createLinkShare(_localFile, shareName, QString()); + } + + void linkShareCreated(const QSharedPointer &share) + { + qCDebug(lcPublicLink) << "New share created"; + success(share->getLink().toString()); + } + + void passwordRequired() { + bool ok = false; + QString password = QInputDialog::getText(nullptr, + tr("Password for share required"), + tr("Please enter a password for your link share:"), + QLineEdit::Normal, + QString(), + &ok); + + if (!ok) { + // The dialog was canceled so no need to do anything + return; + } + + // Try to create the link share again with the newly entered password + _shareManager.createLinkShare(_localFile, QString(), password); + } + + void linkShareRequiresPassword(const QString &message) + { + qCInfo(lcPublicLink) << "Could not create link share:" << message; + emit error(message); + deleteLater(); + } + + void serverError(int code, const QString &message) + { + qCWarning(lcPublicLink) << "Share fetch/create error" << code << message; + QMessageBox::warning( + nullptr, + tr("Sharing error"), + tr("Could not retrieve or create the public link share. Error:\n\n%1").arg(message), + QMessageBox::Ok, + QMessageBox::NoButton); + emit error(message); + deleteLater(); + } + +signals: + void done(const QString &link); + void error(const QString &message); + +private: + void success(const QString &link) + { + emit done(link); + deleteLater(); + } + + AccountPtr _account; + ShareManager _shareManager; + QString _localFile; +}; + +#else + +class GetOrCreatePublicLinkShare : public QObject +{ + Q_OBJECT +public: + GetOrCreatePublicLinkShare(const AccountPtr &, const QString &, + std::function, QObject *) + { + } + + void run() + { + } +}; + +#endif + +void SocketApi::command_COPY_PUBLIC_LINK(const QString &localFile, SocketListener *) +{ + auto fileData = FileData::get(localFile); + if (!fileData.folder) + return; + + AccountPtr account = fileData.folder->accountState()->account(); + auto job = new GetOrCreatePublicLinkShare(account, fileData.serverRelativePath, this); + connect(job, &GetOrCreatePublicLinkShare::done, this, + [](const QString &url) { copyUrlToClipboard(url); }); + connect(job, &GetOrCreatePublicLinkShare::error, this, + [=]() { emit shareCommandReceived(fileData.serverRelativePath, fileData.localPath, ShareDialogStartPage::PublicLinks); }); + job->run(); +} + +// Windows Shell / Explorer pinning fallbacks, see issue: https://github.com/nextcloud/desktop/issues/1599 +#ifdef Q_OS_WIN +void SocketApi::command_COPYASPATH(const QString &localFile, SocketListener *) +{ + QApplication::clipboard()->setText(localFile); +} + +void SocketApi::command_OPENNEWWINDOW(const QString &localFile, SocketListener *) +{ + QDesktopServices::openUrl(QUrl::fromLocalFile(localFile)); +} + +void SocketApi::command_OPEN(const QString &localFile, SocketListener *socketListener) +{ + command_OPENNEWWINDOW(localFile, socketListener); +} +#endif + +// Fetches the private link url asynchronously and then calls the target slot +void SocketApi::fetchPrivateLinkUrlHelper(const QString &localFile, const std::function &targetFun) +{ + auto fileData = FileData::get(localFile); + if (!fileData.folder) { + qCWarning(lcSocketApi) << "Unknown path" << localFile; + return; + } + + auto record = fileData.journalRecord(); + if (!record.isValid()) + return; + + fetchPrivateLinkUrl( + fileData.folder->accountState()->account(), + fileData.serverRelativePath, + record.numericFileId(), + this, + targetFun); +} + +void SocketApi::command_COPY_PRIVATE_LINK(const QString &localFile, SocketListener *) +{ + fetchPrivateLinkUrlHelper(localFile, &SocketApi::copyUrlToClipboard); +} + +void SocketApi::command_EMAIL_PRIVATE_LINK(const QString &localFile, SocketListener *) +{ + fetchPrivateLinkUrlHelper(localFile, &SocketApi::emailPrivateLink); +} + +void SocketApi::command_OPEN_PRIVATE_LINK(const QString &localFile, SocketListener *) +{ + fetchPrivateLinkUrlHelper(localFile, &SocketApi::openPrivateLink); +} + +void SocketApi::command_MAKE_AVAILABLE_LOCALLY(const QString &filesArg, SocketListener *) +{ + const QStringList files = split(filesArg); + + for (const auto &file : files) { + auto data = FileData::get(file); + if (!data.folder) + continue; + + // Update the pin state on all items + if (!data.folder->vfs().setPinState(data.folderRelativePath, PinState::AlwaysLocal)) { + qCWarning(lcSocketApi) << "Could not set pin state of" << data.folderRelativePath << "to always local"; + } + + // Trigger sync + data.folder->schedulePathForLocalDiscovery(data.folderRelativePath); + data.folder->scheduleThisFolderSoon(); + } +} + +/* Go over all the files and replace them by a virtual file */ +void SocketApi::command_MAKE_ONLINE_ONLY(const QString &filesArg, SocketListener *) +{ + const QStringList files = split(filesArg); + + for (const auto &file : files) { + auto data = FileData::get(file); + if (!data.folder) + continue; + + // Update the pin state on all items + if (!data.folder->vfs().setPinState(data.folderRelativePath, PinState::OnlineOnly)) { + qCWarning(lcSocketApi) << "Could not set pin state of" << data.folderRelativePath << "to online only"; + } + + // Trigger sync + data.folder->schedulePathForLocalDiscovery(data.folderRelativePath); + data.folder->scheduleThisFolderSoon(); + } +} + +void SocketApi::copyUrlToClipboard(const QString &link) +{ + QApplication::clipboard()->setText(link); +} + +void SocketApi::command_RESOLVE_CONFLICT(const QString &localFile, SocketListener *) +{ + const auto fileData = FileData::get(localFile); + if (!fileData.folder || !Utility::isConflictFile(fileData.folderRelativePath)) + return; // should not have shown menu item + + const auto conflictedRelativePath = fileData.folderRelativePath; + const auto baseRelativePath = fileData.folder->journalDb()->conflictFileBaseName(fileData.folderRelativePath.toUtf8()); + + const auto dir = QDir(fileData.folder->path()); + const auto conflictedPath = dir.filePath(conflictedRelativePath); + const auto basePath = dir.filePath(baseRelativePath); + + const auto baseName = QFileInfo(basePath).fileName(); + +#ifndef OWNCLOUD_TEST + ConflictDialog dialog; + dialog.setBaseFilename(baseName); + dialog.setLocalVersionFilename(conflictedPath); + dialog.setRemoteVersionFilename(basePath); + if (dialog.exec() == ConflictDialog::Accepted) { + fileData.folder->scheduleThisFolderSoon(); + } +#endif +} + +void SocketApi::command_DELETE_ITEM(const QString &localFile, SocketListener *) +{ + ConflictSolver solver; + solver.setLocalVersionFilename(localFile); + solver.exec(ConflictSolver::KeepRemoteVersion); +} + +void SocketApi::command_MOVE_ITEM(const QString &localFile, SocketListener *) +{ + const auto fileData = FileData::get(localFile); + const auto parentDir = fileData.parentFolder(); + if (!fileData.folder) + return; // should not have shown menu item + + QString defaultDirAndName = fileData.folderRelativePath; + + // If it's a conflict, we want to save it under the base name by default + if (Utility::isConflictFile(defaultDirAndName)) { + defaultDirAndName = fileData.folder->journalDb()->conflictFileBaseName(fileData.folderRelativePath.toUtf8()); + } + + // If the parent doesn't accept new files, go to the root of the sync folder + QFileInfo fileInfo(localFile); + const auto parentRecord = parentDir.journalRecord(); + if ((fileInfo.isFile() && !parentRecord._remotePerm.hasPermission(RemotePermissions::CanAddFile)) + || (fileInfo.isDir() && !parentRecord._remotePerm.hasPermission(RemotePermissions::CanAddSubDirectories))) { + defaultDirAndName = QFileInfo(defaultDirAndName).fileName(); + } + + // Add back the folder path + defaultDirAndName = QDir(fileData.folder->path()).filePath(defaultDirAndName); + + const auto target = QFileDialog::getSaveFileName( + nullptr, + tr("Select new location …"), + defaultDirAndName, + QString(), nullptr, QFileDialog::HideNameFilterDetails); + if (target.isEmpty()) + return; + + ConflictSolver solver; + solver.setLocalVersionFilename(localFile); + solver.setRemoteVersionFilename(target); +} + +void SocketApi::command_V2_LIST_ACCOUNTS(const QString &, SocketListener *listener) const +{ + QJsonArray out; + for (auto acc : AccountManager::instance()->accounts()) { + // TODO: Use uuid once https://github.com/owncloud/client/pull/8397 is merged + out << QJsonObject({ { "name", acc->account()->displayName() }, { "id", acc->account()->id() } }); + } + listener->sendMessage(QStringLiteral("V2/ACCOUNTS"), { { "accounts", out } }); +} + +void SocketApi::command_V2_UPLOAD_FILES_FROM(const QString &argument, SocketListener *listener) const +{ + auto job = new SocketUploadJob(listener, argument); + job->start(); +} + +void SocketApi::emailPrivateLink(const QString &link) +{ + Utility::openEmailComposer( + tr("I shared something with you"), + link, + nullptr); +} + +void OCC::SocketApi::openPrivateLink(const QString &link) +{ + Utility::openBrowser(link); +} + +void SocketApi::command_GET_STRINGS(const QString &argument, SocketListener *listener) +{ + static std::array, 5> strings { { + { "SHARE_MENU_TITLE", tr("Share options") }, + { "CONTEXT_MENU_TITLE", Theme::instance()->appNameGUI() }, + { "COPY_PRIVATE_LINK_MENU_TITLE", tr("Copy private link to clipboard") }, + { "EMAIL_PRIVATE_LINK_MENU_TITLE", tr("Send private link by email …") }, + { "CONTEXT_MENU_ICON", APPLICATION_ICON_NAME}, + } }; + listener->sendMessage(QString("GET_STRINGS:BEGIN")); + for (const auto& key_value : strings) { + if (argument.isEmpty() || argument == QLatin1String(key_value.first)) { + listener->sendMessage(QString("STRING:%1:%2").arg(key_value.first, key_value.second)); + } + } + listener->sendMessage(QString("GET_STRINGS:END")); +} + +void SocketApi::sendSharingContextMenuOptions(const FileData &fileData, SocketListener *listener, bool enabled) +{ + auto record = fileData.journalRecord(); + bool isOnTheServer = record.isValid(); + auto flagString = isOnTheServer && enabled ? QLatin1String("::") : QLatin1String(":d:"); + + auto capabilities = fileData.folder->accountState()->account()->capabilities(); + auto theme = Theme::instance(); + if (!capabilities.shareAPI() || !(theme->userGroupSharing() || (theme->linkSharing() && capabilities.sharePublicLink()))) + return; + + // If sharing is globally disabled, do not show any sharing entries. + // If there is no permission to share for this file, add a disabled entry saying so + if (isOnTheServer && !record._remotePerm.isNull() && !record._remotePerm.hasPermission(RemotePermissions::CanReshare)) { + listener->sendMessage(QLatin1String("MENU_ITEM:DISABLED:d:") + (!record.isDirectory() ? tr("Resharing this file is not allowed") : tr("Resharing this folder is not allowed"))); + } else { + listener->sendMessage(QLatin1String("MENU_ITEM:SHARE") + flagString + tr("Share options")); + + // Do we have public links? + bool publicLinksEnabled = theme->linkSharing() && capabilities.sharePublicLink(); + + // Is is possible to create a public link without user choices? + bool canCreateDefaultPublicLink = publicLinksEnabled + && !capabilities.sharePublicLinkEnforceExpireDate() + && !capabilities.sharePublicLinkAskOptionalPassword() + && !capabilities.sharePublicLinkEnforcePassword(); + + if (canCreateDefaultPublicLink) { + listener->sendMessage(QLatin1String("MENU_ITEM:COPY_PUBLIC_LINK") + flagString + tr("Copy public link")); + } else if (publicLinksEnabled) { + listener->sendMessage(QLatin1String("MENU_ITEM:MANAGE_PUBLIC_LINKS") + flagString + tr("Copy public link")); + } + } + + listener->sendMessage(QLatin1String("MENU_ITEM:COPY_PRIVATE_LINK") + flagString + tr("Copy internal link")); + + // Disabled: only providing email option for private links would look odd, + // and the copy option is more general. + //listener->sendMessage(QLatin1String("MENU_ITEM:EMAIL_PRIVATE_LINK") + flagString + tr("Send private link by email …")); +} + +SocketApi::FileData SocketApi::FileData::get(const QString &localFile) +{ + FileData data; + + data.localPath = QDir::cleanPath(localFile); + if (data.localPath.endsWith(QLatin1Char('/'))) + data.localPath.chop(1); + + data.folder = FolderMan::instance()->folderForPath(data.localPath); + if (!data.folder) + return data; + + data.folderRelativePath = data.localPath.mid(data.folder->cleanPath().length() + 1); + data.serverRelativePath = QDir(data.folder->remotePath()).filePath(data.folderRelativePath); + QString virtualFileExt = QStringLiteral(APPLICATION_DOTVIRTUALFILE_SUFFIX); + if (data.serverRelativePath.endsWith(virtualFileExt)) { + data.serverRelativePath.chop(virtualFileExt.size()); + } + return data; +} + +QString SocketApi::FileData::folderRelativePathNoVfsSuffix() const +{ + auto result = folderRelativePath; + QString virtualFileExt = QStringLiteral(APPLICATION_DOTVIRTUALFILE_SUFFIX); + if (result.endsWith(virtualFileExt)) { + result.chop(virtualFileExt.size()); + } + return result; +} + +SyncFileStatus SocketApi::FileData::syncFileStatus() const +{ + if (!folder) + return SyncFileStatus::StatusNone; + return folder->syncEngine().syncFileStatusTracker().fileStatus(folderRelativePath); +} + +SyncJournalFileRecord SocketApi::FileData::journalRecord() const +{ + SyncJournalFileRecord record; + if (!folder) + return record; + folder->journalDb()->getFileRecord(folderRelativePath, &record); + return record; +} + +SocketApi::FileData SocketApi::FileData::parentFolder() const +{ + return FileData::get(QFileInfo(localPath).dir().path().toUtf8()); +} + +void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListener *listener) +{ + listener->sendMessage(QString("GET_MENU_ITEMS:BEGIN")); + const QStringList files = split(argument); + + // Find the common sync folder. + // syncFolder will be null if files are in different folders. + Folder *syncFolder = nullptr; + for (const auto &file : files) { + auto folder = FolderMan::instance()->folderForPath(file); + if (folder != syncFolder) { + if (!syncFolder) { + syncFolder = folder; + } else { + syncFolder = nullptr; + break; + } + } + } + + // Sharing actions show for single files only + if (syncFolder && files.size() == 1 && syncFolder->accountState()->isConnected()) { + QString systemPath = QDir::cleanPath(argument); + if (systemPath.endsWith(QLatin1Char('/'))) { + systemPath.truncate(systemPath.length() - 1); + } + + FileData fileData = FileData::get(argument); + const auto record = fileData.journalRecord(); + const bool isOnTheServer = record.isValid(); + const auto isE2eEncryptedPath = fileData.journalRecord()._isE2eEncrypted || !fileData.journalRecord()._e2eMangledName.isEmpty(); + auto flagString = isOnTheServer && !isE2eEncryptedPath ? QLatin1String("::") : QLatin1String(":d:"); + + DirectEditor* editor = getDirectEditorForLocalFile(fileData.localPath); + if (editor) { + //listener->sendMessage(QLatin1String("MENU_ITEM:EDIT") + flagString + tr("Edit via ") + editor->name()); + listener->sendMessage(QLatin1String("MENU_ITEM:EDIT") + flagString + tr("Edit")); + } else { + listener->sendMessage(QLatin1String("MENU_ITEM:OPEN_PRIVATE_LINK") + flagString + tr("Open in browser")); + } + + sendSharingContextMenuOptions(fileData, listener, !isE2eEncryptedPath); + + // Conflict files get conflict resolution actions + bool isConflict = Utility::isConflictFile(fileData.folderRelativePath); + if (isConflict || !isOnTheServer) { + // Check whether this new file is in a read-only directory + QFileInfo fileInfo(fileData.localPath); + const auto parentDir = fileData.parentFolder(); + const auto parentRecord = parentDir.journalRecord(); + const bool canAddToDir = + !parentRecord.isValid() // We're likely at the root of the sync folder, got to assume we can add there + || (fileInfo.isFile() && parentRecord._remotePerm.hasPermission(RemotePermissions::CanAddFile)) + || (fileInfo.isDir() && parentRecord._remotePerm.hasPermission(RemotePermissions::CanAddSubDirectories)); + const bool canChangeFile = + !isOnTheServer + || (record._remotePerm.hasPermission(RemotePermissions::CanDelete) + && record._remotePerm.hasPermission(RemotePermissions::CanMove) + && record._remotePerm.hasPermission(RemotePermissions::CanRename)); + + if (isConflict && canChangeFile) { + if (canAddToDir) { + listener->sendMessage(QLatin1String("MENU_ITEM:RESOLVE_CONFLICT::") + tr("Resolve conflict …")); + } else { + if (isOnTheServer) { + // Uploaded conflict file in read-only directory + listener->sendMessage(QLatin1String("MENU_ITEM:MOVE_ITEM::") + tr("Move and rename …")); + } else { + // Local-only conflict file in a read-only dir + listener->sendMessage(QLatin1String("MENU_ITEM:MOVE_ITEM::") + tr("Move, rename and upload …")); + } + listener->sendMessage(QLatin1String("MENU_ITEM:DELETE_ITEM::") + tr("Delete local changes")); + } + } + + // File in a read-only directory? + if (!isConflict && !isOnTheServer && !canAddToDir) { + listener->sendMessage(QLatin1String("MENU_ITEM:MOVE_ITEM::") + tr("Move and upload …")); + listener->sendMessage(QLatin1String("MENU_ITEM:DELETE_ITEM::") + tr("Delete")); + } + } + } + + // File availability actions + if (syncFolder + && syncFolder->virtualFilesEnabled() + && syncFolder->vfs().socketApiPinStateActionsShown()) { + ENFORCE(!files.isEmpty()); + + // Determine the combined availability status of the files + auto combined = Optional(); + auto merge = [](VfsItemAvailability lhs, VfsItemAvailability rhs) { + if (lhs == rhs) + return lhs; + if (int(lhs) > int(rhs)) + std::swap(lhs, rhs); // reduce cases ensuring lhs < rhs + if (lhs == VfsItemAvailability::AlwaysLocal && rhs == VfsItemAvailability::AllHydrated) + return VfsItemAvailability::AllHydrated; + if (lhs == VfsItemAvailability::AllDehydrated && rhs == VfsItemAvailability::OnlineOnly) + return VfsItemAvailability::AllDehydrated; + return VfsItemAvailability::Mixed; + }; + for (const auto &file : files) { + auto fileData = FileData::get(file); + auto availability = syncFolder->vfs().availability(fileData.folderRelativePath); + if (!availability) { + if (availability.error() == Vfs::AvailabilityError::DbError) + availability = VfsItemAvailability::Mixed; + if (availability.error() == Vfs::AvailabilityError::NoSuchItem) + continue; + } + if (!combined) { + combined = *availability; + } else { + combined = merge(*combined, *availability); + } + } + + // TODO: Should be a submenu, should use icons + auto makePinContextMenu = [&](bool makeAvailableLocally, bool freeSpace) { + listener->sendMessage(QLatin1String("MENU_ITEM:CURRENT_PIN:d:") + + Utility::vfsCurrentAvailabilityText(*combined)); + listener->sendMessage(QLatin1String("MENU_ITEM:MAKE_AVAILABLE_LOCALLY:") + + (makeAvailableLocally ? QLatin1String(":") : QLatin1String("d:")) + + Utility::vfsPinActionText()); + listener->sendMessage(QLatin1String("MENU_ITEM:MAKE_ONLINE_ONLY:") + + (freeSpace ? QLatin1String(":") : QLatin1String("d:")) + + Utility::vfsFreeSpaceActionText()); + }; + + if (combined) { + switch (*combined) { + case VfsItemAvailability::AlwaysLocal: + makePinContextMenu(false, true); + break; + case VfsItemAvailability::AllHydrated: + case VfsItemAvailability::Mixed: + makePinContextMenu(true, true); + break; + case VfsItemAvailability::AllDehydrated: + case VfsItemAvailability::OnlineOnly: + makePinContextMenu(true, false); + break; + } + } + } + + listener->sendMessage(QString("GET_MENU_ITEMS:END")); +} + +DirectEditor* SocketApi::getDirectEditorForLocalFile(const QString &localFile) +{ + FileData fileData = FileData::get(localFile); + auto capabilities = fileData.folder->accountState()->account()->capabilities(); + + if (fileData.folder && fileData.folder->accountState()->isConnected()) { + const auto record = fileData.journalRecord(); + const auto mimeMatchMode = record.isVirtualFile() ? QMimeDatabase::MatchExtension : QMimeDatabase::MatchDefault; + + QMimeDatabase db; + QMimeType type = db.mimeTypeForFile(localFile, mimeMatchMode); + + DirectEditor* editor = capabilities.getDirectEditorForMimetype(type); + if (!editor) { + editor = capabilities.getDirectEditorForOptionalMimetype(type); + } + return editor; + } + + return nullptr; +} + +#if GUI_TESTING +void SocketApi::command_ASYNC_LIST_WIDGETS(const QSharedPointer &job) +{ + QString response; + for (auto &widget : allObjects(QApplication::allWidgets())) { + auto objectName = widget->objectName(); + if (!objectName.isEmpty()) { + response += objectName + ":" + widget->property("text").toString() + ", "; + } + } + job->resolve(response); +} + +void SocketApi::command_ASYNC_INVOKE_WIDGET_METHOD(const QSharedPointer &job) +{ + auto &arguments = job->arguments(); + + auto widget = findWidget(arguments["objectName"].toString()); + if (!widget) { + job->reject(QLatin1String("widget not found")); + return; + } + + QMetaObject::invokeMethod(widget, arguments["method"].toString().toUtf8().constData()); + job->resolve(); +} + +void SocketApi::command_ASYNC_GET_WIDGET_PROPERTY(const QSharedPointer &job) +{ + QString widgetName = job->arguments()[QLatin1String("objectName")].toString(); + auto widget = findWidget(widgetName); + if (!widget) { + QString message = QString(QLatin1String("Widget not found: 2: %1")).arg(widgetName); + job->reject(message); + return; + } + + auto propertyName = job->arguments()[QLatin1String("property")].toString(); + + auto segments = propertyName.split('.'); + + QObject *currentObject = widget; + QString value; + for (int i = 0; i < segments.count(); i++) { + auto segment = segments.at(i); + auto var = currentObject->property(segment.toUtf8().constData()); + + if (var.canConvert()) { + var.convert(QMetaType::QString); + value = var.value(); + break; + } + + auto tmpObject = var.value(); + if (tmpObject) { + currentObject = tmpObject; + } else { + QString message = QString(QLatin1String("Widget not found: 3: %1")).arg(widgetName); + job->reject(message); + return; + } + } + + job->resolve(value); +} + +void SocketApi::command_ASYNC_SET_WIDGET_PROPERTY(const QSharedPointer &job) +{ + auto &arguments = job->arguments(); + QString widgetName = arguments["objectName"].toString(); + auto widget = findWidget(widgetName); + if (!widget) { + QString message = QString(QLatin1String("Widget not found: 4: %1")).arg(widgetName); + job->reject(message); + return; + } + widget->setProperty(arguments["property"].toString().toUtf8().constData(), + arguments["value"]); + + job->resolve(); +} + +void SocketApi::command_ASYNC_WAIT_FOR_WIDGET_SIGNAL(const QSharedPointer &job) +{ + auto &arguments = job->arguments(); + QString widgetName = arguments["objectName"].toString(); + auto widget = findWidget(arguments["objectName"].toString()); + if (!widget) { + QString message = QString(QLatin1String("Widget not found: 5: %1")).arg(widgetName); + job->reject(message); + return; + } + + ListenerClosure *closure = new ListenerClosure([job]() { job->resolve("signal emitted"); }); + + auto signalSignature = arguments["signalSignature"].toString(); + signalSignature.prepend("2"); + auto utf8 = signalSignature.toUtf8(); + auto signalSignatureFinal = utf8.constData(); + connect(widget, signalSignatureFinal, closure, SLOT(closureSlot()), Qt::QueuedConnection); +} + +void SocketApi::command_ASYNC_TRIGGER_MENU_ACTION(const QSharedPointer &job) +{ + auto &arguments = job->arguments(); + + auto objectName = arguments["objectName"].toString(); + auto widget = findWidget(objectName); + if (!widget) { + QString message = QString(QLatin1String("Object not found: 1: %1")).arg(objectName); + job->reject(message); + return; + } + + auto children = widget->findChildren(); + for (auto childWidget : children) { + // foo is the popupwidget! + auto actions = childWidget->actions(); + for (auto action : actions) { + if (action->objectName() == arguments["actionName"].toString()) { + action->trigger(); + + job->resolve("action found"); + return; + } + } + } + + QString message = QString(QLatin1String("Action not found: 1: %1")).arg(arguments["actionName"].toString()); + job->reject(message); +} + +void SocketApi::command_ASYNC_ASSERT_ICON_IS_EQUAL(const QSharedPointer &job) +{ + auto widget = findWidget(job->arguments()[QLatin1String("queryString")].toString()); + if (!widget) { + QString message = QString(QLatin1String("Object not found: 6: %1")).arg(job->arguments()["queryString"].toString()); + job->reject(message); + return; + } + + auto propertyName = job->arguments()[QLatin1String("propertyPath")].toString(); + + auto segments = propertyName.split('.'); + + QObject *currentObject = widget; + QIcon value; + for (int i = 0; i < segments.count(); i++) { + auto segment = segments.at(i); + auto var = currentObject->property(segment.toUtf8().constData()); + + if (var.canConvert()) { + var.convert(QMetaType::QIcon); + value = var.value(); + break; + } + + auto tmpObject = var.value(); + if (tmpObject) { + currentObject = tmpObject; + } else { + job->reject(QString(QLatin1String("Icon not found: %1")).arg(propertyName)); + } + } + + auto iconName = job->arguments()[QLatin1String("iconName")].toString(); + if (value.name() == iconName) { + job->resolve(); + } else { + job->reject("iconName " + iconName + " does not match: " + value.name()); + } +} +#endif + +QString SocketApi::buildRegisterPathMessage(const QString &path) +{ + QFileInfo fi(path); + QString message = QLatin1String("REGISTER_PATH:"); + message.append(QDir::toNativeSeparators(fi.absoluteFilePath())); + return message; +} + +} // namespace OCC + +#include "socketapi.moc" diff --git a/src/gui/socketapi/socketapi.h b/src/gui/socketapi/socketapi.h new file mode 100644 index 000000000..cd7d9ff01 --- /dev/null +++ b/src/gui/socketapi/socketapi.h @@ -0,0 +1,176 @@ +/* + * Copyright (C) by Dominik Schmidt + * + * 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. + */ + +#ifndef SOCKETAPI_H +#define SOCKETAPI_H + +#include "syncfileitem.h" +#include "common/syncfilestatus.h" +#include "sharedialog.h" // for the ShareDialogStartPage +#include "common/syncjournalfilerecord.h" + +#include "config.h" + +#if defined(Q_OS_MAC) +#include "socketapisocket_mac.h" +#else +#include +using SocketApiServer = QLocalServer; +#endif + +class QUrl; +class QLocalSocket; +class QStringList; + +namespace OCC { + +class SyncFileStatus; +class Folder; +class SocketListener; +class DirectEditor; +class SocketApiJob; + +Q_DECLARE_LOGGING_CATEGORY(lcSocketApi) + +/** + * @brief The SocketApi class + * @ingroup gui + */ +class SocketApi : public QObject +{ + Q_OBJECT + +public: + explicit SocketApi(QObject *parent = nullptr); + virtual ~SocketApi(); + +public slots: + void slotUpdateFolderView(Folder *f); + void slotUnregisterPath(const QString &alias); + void slotRegisterPath(const QString &alias); + void broadcastStatusPushMessage(const QString &systemPath, SyncFileStatus fileStatus); + +signals: + void shareCommandReceived(const QString &sharePath, const QString &localPath, ShareDialogStartPage startPage); + +private slots: + void slotNewConnection(); + void onLostConnection(); + void slotSocketDestroyed(QObject *obj); + void slotReadSocket(); + + static void copyUrlToClipboard(const QString &link); + static void emailPrivateLink(const QString &link); + static void openPrivateLink(const QString &link); + +private: + // Helper structure for getting information on a file + // based on its local path - used for nearly all remote + // actions. + struct FileData + { + static FileData get(const QString &localFile); + SyncFileStatus syncFileStatus() const; + SyncJournalFileRecord journalRecord() const; + FileData parentFolder() const; + + // Relative path of the file locally, without any vfs suffix + QString folderRelativePathNoVfsSuffix() const; + + Folder *folder; + // Absolute path of the file locally. (May be a virtual file) + QString localPath; + // Relative path of the file locally, as in the DB. (May be a virtual file) + QString folderRelativePath; + // Path of the file on the server (In case of virtual file, it points to the actual file) + QString serverRelativePath; + }; + + void broadcastMessage(const QString &msg, bool doWait = false); + + // opens share dialog, sends reply + void processShareRequest(const QString &localFile, SocketListener *listener, ShareDialogStartPage startPage); + + Q_INVOKABLE void command_RETRIEVE_FOLDER_STATUS(const QString &argument, SocketListener *listener); + Q_INVOKABLE void command_RETRIEVE_FILE_STATUS(const QString &argument, SocketListener *listener); + + Q_INVOKABLE void command_VERSION(const QString &argument, SocketListener *listener); + + Q_INVOKABLE void command_SHARE_MENU_TITLE(const QString &argument, SocketListener *listener); + + // The context menu actions + Q_INVOKABLE void command_SHARE(const QString &localFile, SocketListener *listener); + Q_INVOKABLE void command_MANAGE_PUBLIC_LINKS(const QString &localFile, SocketListener *listener); + Q_INVOKABLE void command_COPY_PUBLIC_LINK(const QString &localFile, SocketListener *listener); + Q_INVOKABLE void command_COPY_PRIVATE_LINK(const QString &localFile, SocketListener *listener); + Q_INVOKABLE void command_EMAIL_PRIVATE_LINK(const QString &localFile, SocketListener *listener); + Q_INVOKABLE void command_OPEN_PRIVATE_LINK(const QString &localFile, SocketListener *listener); + Q_INVOKABLE void command_MAKE_AVAILABLE_LOCALLY(const QString &filesArg, SocketListener *listener); + Q_INVOKABLE void command_MAKE_ONLINE_ONLY(const QString &filesArg, SocketListener *listener); + Q_INVOKABLE void command_RESOLVE_CONFLICT(const QString &localFile, SocketListener *listener); + Q_INVOKABLE void command_DELETE_ITEM(const QString &localFile, SocketListener *listener); + Q_INVOKABLE void command_MOVE_ITEM(const QString &localFile, SocketListener *listener); + + // Windows Shell / Explorer pinning fallbacks, see issue: https://github.com/nextcloud/desktop/issues/1599 +#ifdef Q_OS_WIN + Q_INVOKABLE void command_COPYASPATH(const QString &localFile, SocketListener *listener); + Q_INVOKABLE void command_OPENNEWWINDOW(const QString &localFile, SocketListener *listener); + Q_INVOKABLE void command_OPEN(const QString &localFile, SocketListener *listener); +#endif + + // External sync + Q_INVOKABLE void command_V2_LIST_ACCOUNTS(const QString &argument, SocketListener *listener) const; + Q_INVOKABLE void command_V2_UPLOAD_FILES_FROM(const QString &argument, SocketListener *listener) const; + + // Fetch the private link and call targetFun + void fetchPrivateLinkUrlHelper(const QString &localFile, const std::function &targetFun); + + /** Sends translated/branded strings that may be useful to the integration */ + Q_INVOKABLE void command_GET_STRINGS(const QString &argument, SocketListener *listener); + + // Sends the context menu options relating to sharing to listener + void sendSharingContextMenuOptions(const FileData &fileData, SocketListener *listener, bool enabled); + + /** Send the list of menu item. (added in version 1.1) + * argument is a list of files for which the menu should be shown, separated by '\x1e' + * Reply with GET_MENU_ITEMS:BEGIN + * followed by several MENU_ITEM:[Action]:[flag]:[Text] + * If flag contains 'd', the menu should be disabled + * and ends with GET_MENU_ITEMS:END + */ + Q_INVOKABLE void command_GET_MENU_ITEMS(const QString &argument, SocketListener *listener); + + /// Direct Editing + Q_INVOKABLE void command_EDIT(const QString &localFile, SocketListener *listener); + DirectEditor* getDirectEditorForLocalFile(const QString &localFile); + +#if GUI_TESTING + Q_INVOKABLE void command_ASYNC_ASSERT_ICON_IS_EQUAL(const QSharedPointer &job); + Q_INVOKABLE void command_ASYNC_LIST_WIDGETS(const QSharedPointer &job); + Q_INVOKABLE void command_ASYNC_INVOKE_WIDGET_METHOD(const QSharedPointer &job); + Q_INVOKABLE void command_ASYNC_GET_WIDGET_PROPERTY(const QSharedPointer &job); + Q_INVOKABLE void command_ASYNC_SET_WIDGET_PROPERTY(const QSharedPointer &job); + Q_INVOKABLE void command_ASYNC_WAIT_FOR_WIDGET_SIGNAL(const QSharedPointer &job); + Q_INVOKABLE void command_ASYNC_TRIGGER_MENU_ACTION(const QSharedPointer &job); +#endif + + QString buildRegisterPathMessage(const QString &path); + + QSet _registeredAliases; + QList _listeners; + SocketApiServer _localServer; +}; +} + +#endif // SOCKETAPI_H diff --git a/src/gui/socketapi/socketapi_p.h b/src/gui/socketapi/socketapi_p.h new file mode 100644 index 000000000..38738c84b --- /dev/null +++ b/src/gui/socketapi/socketapi_p.h @@ -0,0 +1,143 @@ +/* + * Copyright (C) by Dominik Schmidt + * Copyright (C) by Klaas Freitag + * Copyright (C) by Roeland Jago Douma + * + * 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. + */ + +#ifndef SOCKETAPI_P_H +#define SOCKETAPI_P_H + +#include +#include +#include + +#include +#include + +#include +#include + +namespace OCC { + +class BloomFilter +{ + // Initialize with m=1024 bits and k=2 (high and low 16 bits of a qHash). + // For a client navigating in less than 100 directories, this gives us a probability less than + // (1-e^(-2*100/1024))^2 = 0.03147872136 false positives. + const static int NumBits = 1024; + +public: + BloomFilter() + : hashBits(NumBits) + { + } + + void storeHash(uint hash) + { + hashBits.setBit((hash & 0xFFFF) % NumBits); // NOLINT it's uint all the way and the modulo puts us back in the 0..1023 range + hashBits.setBit((hash >> 16) % NumBits); // NOLINT + } + bool isHashMaybeStored(uint hash) const + { + return hashBits.testBit((hash & 0xFFFF) % NumBits) // NOLINT + && hashBits.testBit((hash >> 16) % NumBits); // NOLINT + } + +private: + QBitArray hashBits; +}; + +class SocketListener +{ +public: + QPointer socket; + + explicit SocketListener(QIODevice *socket) + : socket(socket) + { + } + + void sendMessage(const QString &message, bool doWait = false) const; + void sendMessage(const QString &function, const QJsonObject &obj, bool doWait = false) const; + + void sendMessageIfDirectoryMonitored(const QString &message, uint systemDirectoryHash) const + { + if (_monitoredDirectoriesBloomFilter.isHashMaybeStored(systemDirectoryHash)) + sendMessage(message, false); + } + + void registerMonitoredDirectory(uint systemDirectoryHash) + { + _monitoredDirectoriesBloomFilter.storeHash(systemDirectoryHash); + } + +private: + BloomFilter _monitoredDirectoriesBloomFilter; +}; + +class ListenerClosure : public QObject +{ + Q_OBJECT +public: + using CallbackFunction = std::function; + ListenerClosure(CallbackFunction callback) + : callback_(callback) + { + } + +public slots: + void closureSlot() + { + callback_(); + deleteLater(); + } + +private: + CallbackFunction callback_; +}; + +class SocketApiJob : public QObject +{ + Q_OBJECT +public: + SocketApiJob(const QString &jobId, SocketListener *socketListener, const QJsonObject &arguments) + : _jobId(jobId) + , _socketListener(socketListener) + , _arguments(arguments) + { + } + + void resolve(const QString &response = QString()) + { + _socketListener->sendMessage(QLatin1String("RESOLVE|") + _jobId + '|' + response); + } + + void resolve(const QJsonObject &response) { resolve(QJsonDocument { response }.toJson()); } + + const QJsonObject &arguments() { return _arguments; } + + void reject(const QString &response) + { + _socketListener->sendMessage(QLatin1String("REJECT|") + _jobId + '|' + response); + } + +private: + QString _jobId; + SocketListener *_socketListener; + QJsonObject _arguments; +}; +} + +Q_DECLARE_METATYPE(OCC::SocketListener *) + +#endif // SOCKETAPI_P_H diff --git a/src/gui/socketapi/socketapisocket_mac.h b/src/gui/socketapi/socketapisocket_mac.h new file mode 100644 index 000000000..b76545351 --- /dev/null +++ b/src/gui/socketapi/socketapisocket_mac.h @@ -0,0 +1,69 @@ +/* + * Copyright (C) by Jocelyn Turcotte + * + * 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. + */ + +#ifndef SOCKETAPISOCKET_OSX_H +#define SOCKETAPISOCKET_OSX_H + +#include +#include + +class SocketApiServerPrivate; +class SocketApiSocketPrivate; + +class SocketApiSocket : public QIODevice +{ + Q_OBJECT +public: + SocketApiSocket(QObject *parent, SocketApiSocketPrivate *p); + ~SocketApiSocket(); + + qint64 readData(char *data, qint64 maxlen) override; + qint64 writeData(const char *data, qint64 len) override; + + bool isSequential() const override { return true; } + qint64 bytesAvailable() const override; + bool canReadLine() const override; + +signals: + void disconnected(); + +private: + // Use Qt's p-impl system to hide objective-c types from C++ code including this file + Q_DECLARE_PRIVATE(SocketApiSocket) + QScopedPointer d_ptr; + friend class SocketApiServerPrivate; +}; + +class SocketApiServer : public QObject +{ + Q_OBJECT +public: + SocketApiServer(); + ~SocketApiServer(); + + void close(); + bool listen(const QString &name); + SocketApiSocket *nextPendingConnection(); + + static bool removeServer(const QString &) { return false; } + +signals: + void newConnection(); + +private: + Q_DECLARE_PRIVATE(SocketApiServer) + QScopedPointer d_ptr; +}; + +#endif // SOCKETAPISOCKET_OSX_H diff --git a/src/gui/socketapi/socketapisocket_mac.mm b/src/gui/socketapi/socketapisocket_mac.mm new file mode 100644 index 000000000..926c34d8a --- /dev/null +++ b/src/gui/socketapi/socketapisocket_mac.mm @@ -0,0 +1,245 @@ +/* + * Copyright (C) by Jocelyn Turcotte + * + * 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. + */ + +#include "socketapisocket_mac.h" +#import + +@protocol ChannelProtocol +- (void)sendMessage:(NSData *)msg; +@end + +@protocol RemoteEndProtocol +- (void)registerTransmitter:(id)tx; +@end + +@interface LocalEnd : NSObject +@property SocketApiSocketPrivate *wrapper; +- (instancetype)initWithWrapper:(SocketApiSocketPrivate *)wrapper; +@end + +@interface Server : NSObject +@property SocketApiServerPrivate *wrapper; +- (instancetype)initWithWrapper:(SocketApiServerPrivate *)wrapper; +- (void)registerClient:(NSDistantObject *)remoteEnd; +@end + +class SocketApiSocketPrivate +{ +public: + SocketApiSocket *q_ptr; + + SocketApiSocketPrivate(NSDistantObject *remoteEnd); + ~SocketApiSocketPrivate(); + + // release remoteEnd + void disconnectRemote(); + + NSDistantObject *remoteEnd; + LocalEnd *localEnd; + QByteArray inBuffer; + bool isRemoteDisconnected = false; +}; + +class SocketApiServerPrivate +{ +public: + SocketApiServer *q_ptr; + + SocketApiServerPrivate(); + ~SocketApiServerPrivate(); + + QList pendingConnections; + NSConnection *connection; + Server *server; +}; + + +@implementation LocalEnd +- (instancetype)initWithWrapper:(SocketApiSocketPrivate *)wrapper +{ + self = [super init]; + self->_wrapper = wrapper; + return self; +} + +- (void)sendMessage:(NSData *)msg +{ + if (_wrapper) { + _wrapper->inBuffer += QByteArray::fromRawNSData(msg); + emit _wrapper->q_ptr->readyRead(); + } +} + +- (void)connectionDidDie:(NSNotification *)notification +{ +#pragma unused(notification) + if (_wrapper) { + _wrapper->disconnectRemote(); + emit _wrapper->q_ptr->disconnected(); + } +} +@end + +@implementation Server +- (instancetype)initWithWrapper:(SocketApiServerPrivate *)wrapper +{ + self = [super init]; + self->_wrapper = wrapper; + return self; +} + +- (void)registerClient:(NSDistantObject *)remoteEnd +{ + // This saves a few mach messages that would otherwise be needed to query the interface + [remoteEnd setProtocolForProxy:@protocol(RemoteEndProtocol)]; + + SocketApiServer *server = _wrapper->q_ptr; + SocketApiSocketPrivate *socketPrivate = new SocketApiSocketPrivate(remoteEnd); + SocketApiSocket *socket = new SocketApiSocket(server, socketPrivate); + _wrapper->pendingConnections.append(socket); + emit server->newConnection(); + + [remoteEnd registerTransmitter:socketPrivate->localEnd]; +} +@end + + +SocketApiSocket::SocketApiSocket(QObject *parent, SocketApiSocketPrivate *p) + : QIODevice(parent) + , d_ptr(p) +{ + Q_D(SocketApiSocket); + d->q_ptr = this; + open(ReadWrite); +} + +SocketApiSocket::~SocketApiSocket() +{ +} + +qint64 SocketApiSocket::readData(char *data, qint64 maxlen) +{ + Q_D(SocketApiSocket); + qint64 len = std::min(maxlen, static_cast(d->inBuffer.size())); + memcpy(data, d->inBuffer.constData(), len); + d->inBuffer.remove(0, len); + return len; +} + +qint64 SocketApiSocket::writeData(const char *data, qint64 len) +{ + Q_D(SocketApiSocket); + if (d->isRemoteDisconnected) + return -1; + + @try { + // FIXME: The NSConnection will make this block unless the function is marked as "oneway" + // in the protocol. This isn't async and reduces our performances but this currectly avoids + // a Mach queue deadlock during requests bursts of the legacy OwnCloudFinder extension. + // Since FinderSync already runs in a separate process, blocking isn't too critical. + [d->remoteEnd sendMessage:[NSData dataWithBytesNoCopy:const_cast(data) length:len freeWhenDone:NO]]; + return len; + } @catch (NSException *e) { + // connectionDidDie can be notified too late, also interpret any sending exception as a disconnection. + d->disconnectRemote(); + emit disconnected(); + return -1; + } +} + +qint64 SocketApiSocket::bytesAvailable() const +{ + Q_D(const SocketApiSocket); + return d->inBuffer.size() + QIODevice::bytesAvailable(); +} + +bool SocketApiSocket::canReadLine() const +{ + Q_D(const SocketApiSocket); + return d->inBuffer.indexOf('\n', int(pos())) != -1 || QIODevice::canReadLine(); +} + +SocketApiSocketPrivate::SocketApiSocketPrivate(NSDistantObject *remoteEnd) + : remoteEnd(remoteEnd) + , localEnd([[LocalEnd alloc] initWithWrapper:this]) +{ + [remoteEnd retain]; + // (Ab)use our objective-c object just to catch the notification + [[NSNotificationCenter defaultCenter] addObserver:localEnd + selector:@selector(connectionDidDie:) + name:NSConnectionDidDieNotification + object:[remoteEnd connectionForProxy]]; +} + +SocketApiSocketPrivate::~SocketApiSocketPrivate() +{ + disconnectRemote(); + + // The DO vended localEnd might still be referenced by the connection + localEnd.wrapper = nil; + [localEnd release]; +} + +void SocketApiSocketPrivate::disconnectRemote() +{ + if (isRemoteDisconnected) + return; + isRemoteDisconnected = true; + + [remoteEnd release]; +} + +SocketApiServer::SocketApiServer() + : d_ptr(new SocketApiServerPrivate) +{ + Q_D(SocketApiServer); + d->q_ptr = this; +} + +SocketApiServer::~SocketApiServer() +{ +} + +void SocketApiServer::close() +{ + // Assume we'll be destroyed right after +} + +bool SocketApiServer::listen(const QString &name) +{ + Q_D(SocketApiServer); + // Set the name of the root object + return [d->connection registerName:name.toNSString()]; +} + +SocketApiSocket *SocketApiServer::nextPendingConnection() +{ + Q_D(SocketApiServer); + return d->pendingConnections.takeFirst(); +} + +SocketApiServerPrivate::SocketApiServerPrivate() +{ + // Create the connection and server object to vend over Disributed Objects + connection = [[NSConnection alloc] init]; + server = [[Server alloc] initWithWrapper:this]; + [connection setRootObject:server]; +} + +SocketApiServerPrivate::~SocketApiServerPrivate() +{ + [connection release]; + server.wrapper = nil; + [server release]; +} diff --git a/src/gui/socketapi/socketuploadjob.cpp b/src/gui/socketapi/socketuploadjob.cpp new file mode 100644 index 000000000..8ade844b0 --- /dev/null +++ b/src/gui/socketapi/socketuploadjob.cpp @@ -0,0 +1,93 @@ +/* + * Copyright (C) by Hannah von Reth + * + * 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. + */ + +#include "socketuploadjob.h" +#include "socketapi_p.h" + +#include "accountmanager.h" +#include "common/syncjournaldb.h" +#include "syncengine.h" + +#include +#include +#include + +using namespace OCC; + +SocketUploadJob::SocketUploadJob(OCC::SocketListener *listener, const QString &argument, QObject *parent) + : QObject(parent) + , _listener(listener) +{ + const auto args = QJsonDocument::fromJson(argument.toUtf8()).object(); + _localPath = args[QLatin1String("localPath")].toString(); + _remotePath = args[QLatin1String("remotePath")].toString(); + if (!_remotePath.startsWith("/")) { + _remotePath = QLatin1Char('/') + _remotePath; + } + + _pattern = args[QLatin1String("pattern")].toString(); + // TODO: use uuid + const auto accname = args[QLatin1String("account")][QLatin1String("name")].toString(); + auto account = AccountManager::instance()->account(accname); + + ENFORCE(QFileInfo(_localPath).isAbsolute()) + ENFORCE(_tmp.open()) + + _db = new SyncJournalDb(_tmp.fileName(), this); + _engine = new SyncEngine(account->account(), _localPath.endsWith(QLatin1Char('/')) ? _localPath : _localPath + QLatin1Char('/'), _remotePath, _db); + _engine->setParent(_db); + + connect(_engine, &OCC::SyncEngine::itemCompleted, this, [this](const OCC::SyncFileItemPtr item) { + _syncedFiles.append(item->_file); + }); + + connect(_engine, &OCC::SyncEngine::finished, this, [this](bool ok) { + if (ok) { + finish({}); + } + }); + connect(_engine, &OCC::SyncEngine::syncError, this, &SocketUploadJob::finish); +} + +void SocketUploadJob::start() +{ + auto opt = _engine->syncOptions(); + opt.setFilePattern(_pattern); + if (!opt.fileRegex().isValid()) { + finish(opt.fileRegex().errorString()); + return; + } + _engine->setSyncOptions(opt); + + // create the dir, fail if it already exists + auto mkdir = new OCC::MkColJob(_engine->account(), _remotePath); + connect(mkdir, &OCC::MkColJob::finishedWithoutError, _engine, &OCC::SyncEngine::startSync); + connect(mkdir, &OCC::MkColJob::finishedWithError, this, [this](QNetworkReply *reply) { + if (reply->error() == 202) { + finish(QStringLiteral("Destination %1 already exists").arg(_remotePath)); + } else { + finish(reply->errorString()); + } + }); + mkdir->start(); +} + +void SocketUploadJob::finish(const QString &error) +{ + if (!_finished) { + _finished = true; + _listener->sendMessage(QStringLiteral("V2/UPLOAD_FILES_RESULT"), { { "localPath", _localPath }, { "error", error }, { "syncedFiles", QJsonArray::fromStringList(_syncedFiles) } }); + deleteLater(); + } +} diff --git a/src/gui/socketapi/socketuploadjob.h b/src/gui/socketapi/socketuploadjob.h new file mode 100644 index 000000000..477b8bf0c --- /dev/null +++ b/src/gui/socketapi/socketuploadjob.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) by Hannah von Reth + * + * 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. + */ + +#pragma once +#include +#include + +#include "socketapi.h" +#include "account.h" + +namespace OCC { + +class SyncJournalDb; +class SyncEngine; + +class SocketUploadJob : public QObject +{ + Q_OBJECT +public: + SocketUploadJob(OCC::SocketListener *listener, const QString &argument, QObject *parent = nullptr); + + void start(); + + void finish(const QString &error); + +private: + SocketListener *_listener; + QString _localPath; + QString _remotePath; + QString _pattern; + QTemporaryFile _tmp; + SyncJournalDb *_db; + SyncEngine *_engine; + QStringList _syncedFiles; + + bool _finished = false; +}; +} diff --git a/src/gui/socketapi_p.h b/src/gui/socketapi_p.h deleted file mode 100644 index da3e90b9c..000000000 --- a/src/gui/socketapi_p.h +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright (C) by Dominik Schmidt - * Copyright (C) by Klaas Freitag - * Copyright (C) by Roeland Jago Douma - * - * 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. - */ - -#ifndef SOCKETAPI_P_H -#define SOCKETAPI_P_H - -#include -#include -#include - -#include -#include - -#include -#include - -namespace OCC { - -class BloomFilter -{ - // Initialize with m=1024 bits and k=2 (high and low 16 bits of a qHash). - // For a client navigating in less than 100 directories, this gives us a probability less than - // (1-e^(-2*100/1024))^2 = 0.03147872136 false positives. - const static int NumBits = 1024; - -public: - BloomFilter() - : hashBits(NumBits) - { - } - - void storeHash(uint hash) - { - hashBits.setBit((hash & 0xFFFF) % NumBits); // NOLINT it's uint all the way and the modulo puts us back in the 0..1023 range - hashBits.setBit((hash >> 16) % NumBits); // NOLINT - } - bool isHashMaybeStored(uint hash) const - { - return hashBits.testBit((hash & 0xFFFF) % NumBits) // NOLINT - && hashBits.testBit((hash >> 16) % NumBits); // NOLINT - } - -private: - QBitArray hashBits; -}; - -class SocketListener -{ -public: - QPointer socket; - - explicit SocketListener(QIODevice *socket) - : socket(socket) - { - } - - void sendMessage(const QString &message, bool doWait = false) const; - - void sendMessageIfDirectoryMonitored(const QString &message, uint systemDirectoryHash) const - { - if (_monitoredDirectoriesBloomFilter.isHashMaybeStored(systemDirectoryHash)) - sendMessage(message, false); - } - - void registerMonitoredDirectory(uint systemDirectoryHash) - { - _monitoredDirectoriesBloomFilter.storeHash(systemDirectoryHash); - } - -private: - BloomFilter _monitoredDirectoriesBloomFilter; -}; - -class ListenerClosure : public QObject -{ - Q_OBJECT -public: - using CallbackFunction = std::function; - ListenerClosure(CallbackFunction callback) - : callback_(callback) - { - } - -public slots: - void closureSlot() - { - callback_(); - deleteLater(); - } - -private: - CallbackFunction callback_; -}; - -class SocketApiJob : public QObject -{ - Q_OBJECT -public: - SocketApiJob(const QString &jobId, SocketListener *socketListener, const QJsonObject &arguments) - : _jobId(jobId) - , _socketListener(socketListener) - , _arguments(arguments) - { - } - - void resolve(const QString &response = QString()) - { - _socketListener->sendMessage(QLatin1String("RESOLVE|") + _jobId + '|' + response); - } - - void resolve(const QJsonObject &response) { resolve(QJsonDocument{ response }.toJson()); } - - const QJsonObject &arguments() { return _arguments; } - - void reject(const QString &response) - { - _socketListener->sendMessage(QLatin1String("REJECT|") + _jobId + '|' + response); - } - -private: - QString _jobId; - SocketListener *_socketListener; - QJsonObject _arguments; -}; -} - -Q_DECLARE_METATYPE(OCC::SocketListener *) - -#endif // SOCKETAPI_P_H diff --git a/src/gui/socketapisocket_mac.h b/src/gui/socketapisocket_mac.h deleted file mode 100644 index b76545351..000000000 --- a/src/gui/socketapisocket_mac.h +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) by Jocelyn Turcotte - * - * 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. - */ - -#ifndef SOCKETAPISOCKET_OSX_H -#define SOCKETAPISOCKET_OSX_H - -#include -#include - -class SocketApiServerPrivate; -class SocketApiSocketPrivate; - -class SocketApiSocket : public QIODevice -{ - Q_OBJECT -public: - SocketApiSocket(QObject *parent, SocketApiSocketPrivate *p); - ~SocketApiSocket(); - - qint64 readData(char *data, qint64 maxlen) override; - qint64 writeData(const char *data, qint64 len) override; - - bool isSequential() const override { return true; } - qint64 bytesAvailable() const override; - bool canReadLine() const override; - -signals: - void disconnected(); - -private: - // Use Qt's p-impl system to hide objective-c types from C++ code including this file - Q_DECLARE_PRIVATE(SocketApiSocket) - QScopedPointer d_ptr; - friend class SocketApiServerPrivate; -}; - -class SocketApiServer : public QObject -{ - Q_OBJECT -public: - SocketApiServer(); - ~SocketApiServer(); - - void close(); - bool listen(const QString &name); - SocketApiSocket *nextPendingConnection(); - - static bool removeServer(const QString &) { return false; } - -signals: - void newConnection(); - -private: - Q_DECLARE_PRIVATE(SocketApiServer) - QScopedPointer d_ptr; -}; - -#endif // SOCKETAPISOCKET_OSX_H diff --git a/src/gui/socketapisocket_mac.mm b/src/gui/socketapisocket_mac.mm deleted file mode 100644 index f018ac752..000000000 --- a/src/gui/socketapisocket_mac.mm +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Copyright (C) by Jocelyn Turcotte - * - * 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. - */ - -#include "socketapisocket_mac.h" -#import - -@protocol ChannelProtocol -- (void)sendMessage:(NSData*)msg; -@end - -@protocol RemoteEndProtocol -- (void)registerTransmitter:(id)tx; -@end - -@interface LocalEnd : NSObject -@property SocketApiSocketPrivate *wrapper; -- (instancetype)initWithWrapper:(SocketApiSocketPrivate *)wrapper; -@end - -@interface Server : NSObject -@property SocketApiServerPrivate *wrapper; -- (instancetype)initWithWrapper:(SocketApiServerPrivate *)wrapper; -- (void)registerClient:(NSDistantObject *)remoteEnd; -@end - -class SocketApiSocketPrivate -{ -public: - SocketApiSocket *q_ptr; - - SocketApiSocketPrivate(NSDistantObject *remoteEnd); - ~SocketApiSocketPrivate(); - - // release remoteEnd - void disconnectRemote(); - - NSDistantObject *remoteEnd; - LocalEnd *localEnd; - QByteArray inBuffer; - bool isRemoteDisconnected = false; -}; - -class SocketApiServerPrivate -{ -public: - SocketApiServer *q_ptr; - - SocketApiServerPrivate(); - ~SocketApiServerPrivate(); - - QList pendingConnections; - NSConnection *connection; - Server *server; -}; - - -@implementation LocalEnd -- (instancetype)initWithWrapper:(SocketApiSocketPrivate *)wrapper -{ - self = [super init]; - self->_wrapper = wrapper; - return self; -} - -- (void)sendMessage:(NSData*)msg -{ - if (_wrapper) { - _wrapper->inBuffer += QByteArray::fromRawNSData(msg); - emit _wrapper->q_ptr->readyRead(); - } -} - -- (void)connectionDidDie:(NSNotification*)notification -{ -#pragma unused(notification) - if (_wrapper) { - _wrapper->disconnectRemote(); - emit _wrapper->q_ptr->disconnected(); - } -} -@end - -@implementation Server -- (instancetype)initWithWrapper:(SocketApiServerPrivate *)wrapper -{ - self = [super init]; - self->_wrapper = wrapper; - return self; -} - -- (void)registerClient:(NSDistantObject *)remoteEnd -{ - // This saves a few mach messages that would otherwise be needed to query the interface - [remoteEnd setProtocolForProxy:@protocol(RemoteEndProtocol)]; - - SocketApiServer *server = _wrapper->q_ptr; - SocketApiSocketPrivate *socketPrivate = new SocketApiSocketPrivate(remoteEnd); - SocketApiSocket *socket = new SocketApiSocket(server, socketPrivate); - _wrapper->pendingConnections.append(socket); - emit server->newConnection(); - - [remoteEnd registerTransmitter:socketPrivate->localEnd]; -} -@end - - -SocketApiSocket::SocketApiSocket(QObject *parent, SocketApiSocketPrivate *p) - : QIODevice(parent) - , d_ptr(p) -{ - Q_D(SocketApiSocket); - d->q_ptr = this; - open(ReadWrite); -} - -SocketApiSocket::~SocketApiSocket() -{ -} - -qint64 SocketApiSocket::readData(char *data, qint64 maxlen) -{ - Q_D(SocketApiSocket); - qint64 len = std::min(maxlen, static_cast(d->inBuffer.size())); - memcpy(data, d->inBuffer.constData(), len); - d->inBuffer.remove(0, len); - return len; -} - -qint64 SocketApiSocket::writeData(const char *data, qint64 len) -{ - Q_D(SocketApiSocket); - if (d->isRemoteDisconnected) - return -1; - - @try { - // FIXME: The NSConnection will make this block unless the function is marked as "oneway" - // in the protocol. This isn't async and reduces our performances but this currectly avoids - // a Mach queue deadlock during requests bursts of the legacy OwnCloudFinder extension. - // Since FinderSync already runs in a separate process, blocking isn't too critical. - [d->remoteEnd sendMessage:[NSData dataWithBytesNoCopy:const_cast(data) length:len freeWhenDone:NO]]; - return len; - } @catch(NSException* e) { - // connectionDidDie can be notified too late, also interpret any sending exception as a disconnection. - d->disconnectRemote(); - emit disconnected(); - return -1; - } -} - -qint64 SocketApiSocket::bytesAvailable() const -{ - Q_D(const SocketApiSocket); - return d->inBuffer.size() + QIODevice::bytesAvailable(); -} - -bool SocketApiSocket::canReadLine() const -{ - Q_D(const SocketApiSocket); - return d->inBuffer.indexOf('\n', int(pos())) != -1 || QIODevice::canReadLine(); -} - -SocketApiSocketPrivate::SocketApiSocketPrivate(NSDistantObject *remoteEnd) - : remoteEnd(remoteEnd) - , localEnd([[LocalEnd alloc] initWithWrapper:this]) -{ - [remoteEnd retain]; - // (Ab)use our objective-c object just to catch the notification - [[NSNotificationCenter defaultCenter] addObserver:localEnd - selector:@selector(connectionDidDie:) - name:NSConnectionDidDieNotification - object:[remoteEnd connectionForProxy]]; -} - -SocketApiSocketPrivate::~SocketApiSocketPrivate() -{ - disconnectRemote(); - - // The DO vended localEnd might still be referenced by the connection - localEnd.wrapper = nil; - [localEnd release]; -} - -void SocketApiSocketPrivate::disconnectRemote() -{ - if (isRemoteDisconnected) - return; - isRemoteDisconnected = true; - - [remoteEnd release]; -} - -SocketApiServer::SocketApiServer() - : d_ptr(new SocketApiServerPrivate) -{ - Q_D(SocketApiServer); - d->q_ptr = this; -} - -SocketApiServer::~SocketApiServer() -{ -} - -void SocketApiServer::close() -{ - // Assume we'll be destroyed right after -} - -bool SocketApiServer::listen(const QString &name) -{ - Q_D(SocketApiServer); - // Set the name of the root object - return [d->connection registerName:name.toNSString()]; -} - -SocketApiSocket *SocketApiServer::nextPendingConnection() -{ - Q_D(SocketApiServer); - return d->pendingConnections.takeFirst(); -} - -SocketApiServerPrivate::SocketApiServerPrivate() -{ - // Create the connection and server object to vend over Disributed Objects - connection = [[NSConnection alloc] init]; - server = [[Server alloc] initWithWrapper:this]; - [connection setRootObject:server]; -} - -SocketApiServerPrivate::~SocketApiServerPrivate() -{ - [connection release]; - server.wrapper = nil; - [server release]; -} diff --git a/src/libsync/discovery.cpp b/src/libsync/discovery.cpp index 8272ab249..7602a3bd4 100644 --- a/src/libsync/discovery.cpp +++ b/src/libsync/discovery.cpp @@ -67,8 +67,6 @@ void ProcessDirectoryJob::process() { ASSERT(_localQueryDone && _serverQueryDone); - QString localDir; - // Build lookup tables for local, remote and db entries. // For suffix-virtual files, the key will normally be the base file name // without the suffix. diff --git a/src/libsync/owncloudpropagator.cpp b/src/libsync/owncloudpropagator.cpp index ea082b9c9..352ec32c2 100644 --- a/src/libsync/owncloudpropagator.cpp +++ b/src/libsync/owncloudpropagator.cpp @@ -40,6 +40,7 @@ #include #include #include +#include #include namespace OCC { @@ -387,7 +388,7 @@ qint64 OwncloudPropagator::smallFileSize() return smallFileSize; } -void OwncloudPropagator::start(const SyncFileItemVector &items) +void OwncloudPropagator::start(SyncFileItemVector &&items) { Q_ASSERT(std::is_sorted(items.begin(), items.end())); @@ -396,6 +397,26 @@ void OwncloudPropagator::start(const SyncFileItemVector &items) * In order to do that we loop over the items. (which are sorted by destination) * When we enter a directory, we can create the directory job and push it on the stack. */ + const auto regex = syncOptions().fileRegex(); + if (regex.isValid()) { + QSet names; + for (auto &i : items) { + if (regex.match(i->_file).hasMatch()) { + int index = -1; + QStringRef ref; + do { + ref = i->_file.midRef(0, index); + names.insert(ref); + index = ref.lastIndexOf(QLatin1Char('/')); + } while (index > 0); + } + } + items.erase(std::remove_if(items.begin(), items.end(), [&names](auto i) { + return !names.contains(QStringRef { &i->_file }); + }), + items.end()); + } + _rootJob.reset(new PropagateRootDirectory(this)); QStack> directories; directories.push(qMakePair(QString(), _rootJob.data())); diff --git a/src/libsync/owncloudpropagator.h b/src/libsync/owncloudpropagator.h index c2df7749f..f03ef5759 100644 --- a/src/libsync/owncloudpropagator.h +++ b/src/libsync/owncloudpropagator.h @@ -421,7 +421,7 @@ public: ~OwncloudPropagator(); - void start(const SyncFileItemVector &_syncedItems); + void start(SyncFileItemVector &&_syncedItems); const SyncOptions &syncOptions() const; void setSyncOptions(const SyncOptions &syncOptions); diff --git a/src/libsync/syncengine.cpp b/src/libsync/syncengine.cpp index a3307f915..d531fcc7d 100644 --- a/src/libsync/syncengine.cpp +++ b/src/libsync/syncengine.cpp @@ -434,7 +434,7 @@ void SyncEngine::startSync() } if (s_anySyncRunning || _syncRunning) { - ASSERT(false); + ASSERT(false) return; } @@ -725,8 +725,7 @@ void SyncEngine::slotDiscoveryFinished() if (_needsUpdate) Q_EMIT started(); - _propagator->start(_syncItems); - _syncItems.clear(); + _propagator->start(std::move(_syncItems)); qCInfo(lcEngine) << "#### Post-Reconcile end #################################################### " << _stopWatch.addLapTime(QStringLiteral("Post-Reconcile Finished")) << "ms"; }; diff --git a/src/libsync/syncoptions.cpp b/src/libsync/syncoptions.cpp new file mode 100644 index 000000000..588a7c634 --- /dev/null +++ b/src/libsync/syncoptions.cpp @@ -0,0 +1,75 @@ +/* + * Copyright (C) by Olivier Goffart + * + * 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. + */ + +#include "syncoptions.h" +#include "common/utility.h" + +#include + +using namespace OCC; + +SyncOptions::SyncOptions() + : _vfs(new VfsOff) +{ +} + +SyncOptions::~SyncOptions() +{ +} + +void SyncOptions::fillFromEnvironmentVariables() +{ + QByteArray chunkSizeEnv = qgetenv("OWNCLOUD_CHUNK_SIZE"); + if (!chunkSizeEnv.isEmpty()) + _initialChunkSize = chunkSizeEnv.toUInt(); + + QByteArray minChunkSizeEnv = qgetenv("OWNCLOUD_MIN_CHUNK_SIZE"); + if (!minChunkSizeEnv.isEmpty()) + _minChunkSize = minChunkSizeEnv.toUInt(); + + QByteArray maxChunkSizeEnv = qgetenv("OWNCLOUD_MAX_CHUNK_SIZE"); + if (!maxChunkSizeEnv.isEmpty()) + _maxChunkSize = maxChunkSizeEnv.toUInt(); + + QByteArray targetChunkUploadDurationEnv = qgetenv("OWNCLOUD_TARGET_CHUNK_UPLOAD_DURATION"); + if (!targetChunkUploadDurationEnv.isEmpty()) + _targetChunkUploadDuration = std::chrono::milliseconds(targetChunkUploadDurationEnv.toUInt()); + + int maxParallel = qgetenv("OWNCLOUD_MAX_PARALLEL").toInt(); + if (maxParallel > 0) + _parallelNetworkJobs = maxParallel; +} + +void SyncOptions::verifyChunkSizes() +{ + _minChunkSize = qMin(_minChunkSize, _initialChunkSize); + _maxChunkSize = qMax(_maxChunkSize, _initialChunkSize); +} + +QRegularExpression SyncOptions::fileRegex() const +{ + return _fileRegex; +} + +void SyncOptions::setFilePattern(const QString &pattern) +{ + // full match or a path ending with this pattern + setPathPattern(QStringLiteral("(^|/|\\\\)") + pattern + QLatin1Char('$')); +} + +void SyncOptions::setPathPattern(const QString &pattern) +{ + _fileRegex.setPatternOptions(Utility::fsCasePreserving() ? QRegularExpression::CaseInsensitiveOption : QRegularExpression::NoPatternOption); + _fileRegex.setPattern(pattern); +} diff --git a/src/libsync/syncoptions.h b/src/libsync/syncoptions.h index 7dff264f0..f109ec7fc 100644 --- a/src/libsync/syncoptions.h +++ b/src/libsync/syncoptions.h @@ -15,18 +15,23 @@ #pragma once #include "owncloudlib.h" -#include +#include "common/vfs.h" + +#include #include +#include + #include -#include "common/vfs.h" + namespace OCC { /** * Value class containing the options given to the sync engine */ -struct OWNCLOUDSYNC_EXPORT SyncOptions +class OWNCLOUDSYNC_EXPORT SyncOptions { +public: SyncOptions(); ~SyncOptions(); @@ -82,6 +87,29 @@ struct OWNCLOUDSYNC_EXPORT SyncOptions * initial chunk size value. */ void verifyChunkSizes(); + + + /** A regular expression to match file names + * If no pattern is provided the default is an invalid regular expression. + */ + QRegularExpression fileRegex() const; + + /** + * A pattern like *.txt, matching only file names + */ + void setFilePattern(const QString &pattern); + + /** + * A pattern like /own.*\/.*txt matching the full path + */ + void setPathPattern(const QString &pattern); + +private: + /** + * Only sync files that mathc the expression + * Invalid pattern by default. + */ + QRegularExpression _fileRegex = QRegularExpression(QStringLiteral("(")); }; }