sharemanager.cpp
shareusergroupwidget.cpp
sharee.cpp
- socketapi.cpp
sslbutton.cpp
sslerrordialog.cpp
syncrunfilelog.cpp
Qt5::QuickControls2
)
+add_subdirectory(socketapi)
+
if(Qt5WebEngine_FOUND AND Qt5WebEngineWidgets_FOUND)
target_link_libraries(nextcloudCore PUBLIC Qt5::WebEngineWidgets)
endif()
#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"
#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"
#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"
+++ /dev/null
-/*
- * Copyright (C) by Dominik Schmidt <dev@dominik-schmidt.de>
- * Copyright (C) by Klaas Freitag <freitag@owncloud.com>
- * Copyright (C) by Roeland Jago Douma <roeland@famdouma.nl>
- *
- * 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 <array>
-#include <QBitArray>
-#include <QUrl>
-#include <QMetaMethod>
-#include <QMetaObject>
-#include <QStringList>
-#include <QScopedPointer>
-#include <QFile>
-#include <QDir>
-#include <QApplication>
-#include <QLocalSocket>
-#include <QStringBuilder>
-#include <QMessageBox>
-#include <QInputDialog>
-#include <QFileDialog>
-
-
-#include <QAction>
-#include <QJsonDocument>
-#include <QJsonObject>
-#include <QWidget>
-
-#include <QClipboard>
-#include <QDesktopServices>
-
-#include <QProcess>
-#include <QStandardPaths>
-
-#ifdef Q_OS_MAC
-#include <CoreFoundation/CoreFoundation.h>
-#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<QObject*> allObjects(const QList<QWidget*> &widgets) {
- QList<QObject*> objects;
- std::copy(widgets.constBegin(), widgets.constEnd(), std::back_inserter(objects));
-
- objects << qApp;
-
- return objects;
-}
-
-QObject *findWidget(const QString &queryString, const QList<QWidget*> &widgets = QApplication::allWidgets())
-{
- auto objects = allObjects(widgets);
-
- QList<QObject*>::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<QWidget*>());
- 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<QObject*> 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 *>("SocketListener*");
- qRegisterMetaType<QSharedPointer<SocketApiJob>>("QSharedPointer<SocketApiJob>");
-
- 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<QIODevice *>(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<QIODevice *>(obj);
- _listeners.erase(std::remove_if(_listeners.begin(), _listeners.end(), ListenerHasSocketPred(socket)), _listeners.end());
-}
-
-void SocketApi::slotReadSocket()
-{
- auto *socket = qobject_cast<QIODevice *>(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<SocketApiJob>)" : "(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<SocketApiJob>(
- new SocketApiJob(jobId.toString(), listener, json), &QObject::deleteLater);
- if (indexOfMethod != -1) {
- staticMetaObject.method(indexOfMethod)
- .invoke(this, Qt::QueuedConnection,
- Q_ARG(QSharedPointer<SocketApiJob>, 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<void(const QString &link)> 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<QSharedPointer<Share>> &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<LinkShare>(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<LinkShare> &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<void(const QString &url)> _targetFun;
-};
-
-#else
-
-class GetOrCreatePublicLinkShare : public QObject
-{
- Q_OBJECT
-public:
- GetOrCreatePublicLinkShare(const AccountPtr &, const QString &,
- std::function<void(const QString &link)>, 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<void(const QString &url)> &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<std::pair<const char *, QString>, 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<VfsItemAvailability>();
- 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<SocketApiJob> &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<SocketApiJob> &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<SocketApiJob> &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<QString>()) {
- var.convert(QMetaType::QString);
- value = var.value<QString>();
- break;
- }
-
- auto tmpObject = var.value<QObject*>();
- 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<SocketApiJob> &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<SocketApiJob> &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<SocketApiJob> &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<QWidget *>();
- 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<SocketApiJob> &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<QIcon>()) {
- var.convert(QMetaType::QIcon);
- value = var.value<QIcon>();
- break;
- }
-
- auto tmpObject = var.value<QObject*>();
- 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"
+++ /dev/null
-/*
- * Copyright (C) by Dominik Schmidt <dev@dominik-schmidt.de>
- *
- * 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 <QLocalServer>
-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<void(const QString &url)> &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<SocketApiJob> &job);
- Q_INVOKABLE void command_ASYNC_LIST_WIDGETS(const QSharedPointer<SocketApiJob> &job);
- Q_INVOKABLE void command_ASYNC_INVOKE_WIDGET_METHOD(const QSharedPointer<SocketApiJob> &job);
- Q_INVOKABLE void command_ASYNC_GET_WIDGET_PROPERTY(const QSharedPointer<SocketApiJob> &job);
- Q_INVOKABLE void command_ASYNC_SET_WIDGET_PROPERTY(const QSharedPointer<SocketApiJob> &job);
- Q_INVOKABLE void command_ASYNC_WAIT_FOR_WIDGET_SIGNAL(const QSharedPointer<SocketApiJob> &job);
- Q_INVOKABLE void command_ASYNC_TRIGGER_MENU_ACTION(const QSharedPointer<SocketApiJob> &job);
-#endif
-
- QString buildRegisterPathMessage(const QString &path);
-
- QSet<QString> _registeredAliases;
- QList<SocketListener> _listeners;
- SocketApiServer _localServer;
-};
-}
-
-#endif // SOCKETAPI_H
--- /dev/null
+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()
--- /dev/null
+/*
+ * Copyright (C) by Dominik Schmidt <dev@dominik-schmidt.de>
+ * Copyright (C) by Klaas Freitag <freitag@owncloud.com>
+ * Copyright (C) by Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * 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 <array>
+#include <QBitArray>
+#include <QUrl>
+#include <QMetaMethod>
+#include <QMetaObject>
+#include <QStringList>
+#include <QScopedPointer>
+#include <QFile>
+#include <QDir>
+#include <QApplication>
+#include <QLocalSocket>
+#include <QStringBuilder>
+#include <QMessageBox>
+#include <QInputDialog>
+#include <QFileDialog>
+
+
+#include <QAction>
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QWidget>
+
+#include <QClipboard>
+#include <QDesktopServices>
+
+#include <QProcess>
+#include <QStandardPaths>
+#include <QTemporaryFile>
+#include <networkjobs.h>
+
+#ifdef Q_OS_MAC
+#include <CoreFoundation/CoreFoundation.h>
+#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<QObject *> allObjects(const QList<QWidget *> &widgets)
+{
+ QList<QObject *> objects;
+ std::copy(widgets.constBegin(), widgets.constEnd(), std::back_inserter(objects));
+
+ objects << qApp;
+
+ return objects;
+}
+
+QObject *findWidget(const QString &queryString, const QList<QWidget *> &widgets = QApplication::allWidgets())
+{
+ auto objects = allObjects(widgets);
+
+ QList<QObject *>::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<QWidget *>());
+ 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<QObject *> 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 *>("SocketListener*");
+ qRegisterMetaType<QSharedPointer<SocketApiJob>>("QSharedPointer<SocketApiJob>");
+
+ 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<QIODevice *>(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<QIODevice *>(obj);
+ _listeners.erase(std::remove_if(_listeners.begin(), _listeners.end(), ListenerHasSocketPred(socket)), _listeners.end());
+}
+
+void SocketApi::slotReadSocket()
+{
+ auto *socket = qobject_cast<QIODevice *>(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<SocketApiJob>)" : "(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<SocketApiJob>(
+ new SocketApiJob(jobId.toString(), listener, json), &QObject::deleteLater);
+ if (indexOfMethod != -1) {
+ staticMetaObject.method(indexOfMethod)
+ .invoke(this, Qt::QueuedConnection,
+ Q_ARG(QSharedPointer<SocketApiJob>, 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<QSharedPointer<Share>> &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<LinkShare>(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<LinkShare> &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<void(const QString &link)>, 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<void(const QString &url)> &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<std::pair<const char *, QString>, 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<VfsItemAvailability>();
+ 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<SocketApiJob> &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<SocketApiJob> &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<SocketApiJob> &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<QString>()) {
+ var.convert(QMetaType::QString);
+ value = var.value<QString>();
+ break;
+ }
+
+ auto tmpObject = var.value<QObject *>();
+ 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<SocketApiJob> &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<SocketApiJob> &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<SocketApiJob> &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<QWidget *>();
+ 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<SocketApiJob> &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<QIcon>()) {
+ var.convert(QMetaType::QIcon);
+ value = var.value<QIcon>();
+ break;
+ }
+
+ auto tmpObject = var.value<QObject *>();
+ 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"
--- /dev/null
+/*
+ * Copyright (C) by Dominik Schmidt <dev@dominik-schmidt.de>
+ *
+ * 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 <QLocalServer>
+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<void(const QString &url)> &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<SocketApiJob> &job);
+ Q_INVOKABLE void command_ASYNC_LIST_WIDGETS(const QSharedPointer<SocketApiJob> &job);
+ Q_INVOKABLE void command_ASYNC_INVOKE_WIDGET_METHOD(const QSharedPointer<SocketApiJob> &job);
+ Q_INVOKABLE void command_ASYNC_GET_WIDGET_PROPERTY(const QSharedPointer<SocketApiJob> &job);
+ Q_INVOKABLE void command_ASYNC_SET_WIDGET_PROPERTY(const QSharedPointer<SocketApiJob> &job);
+ Q_INVOKABLE void command_ASYNC_WAIT_FOR_WIDGET_SIGNAL(const QSharedPointer<SocketApiJob> &job);
+ Q_INVOKABLE void command_ASYNC_TRIGGER_MENU_ACTION(const QSharedPointer<SocketApiJob> &job);
+#endif
+
+ QString buildRegisterPathMessage(const QString &path);
+
+ QSet<QString> _registeredAliases;
+ QList<SocketListener> _listeners;
+ SocketApiServer _localServer;
+};
+}
+
+#endif // SOCKETAPI_H
--- /dev/null
+/*
+ * Copyright (C) by Dominik Schmidt <dev@dominik-schmidt.de>
+ * Copyright (C) by Klaas Freitag <freitag@owncloud.com>
+ * Copyright (C) by Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * 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 <functional>
+#include <QBitArray>
+#include <QPointer>
+
+#include <QJsonDocument>
+#include <QJsonObject>
+
+#include <memory>
+#include <QTimer>
+
+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<QIODevice> 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<void()>;
+ 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
--- /dev/null
+/*
+ * Copyright (C) by Jocelyn Turcotte <jturcotte@woboq.com>
+ *
+ * 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 <QAbstractSocket>
+#include <QIODevice>
+
+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<SocketApiSocketPrivate> 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<SocketApiServerPrivate> d_ptr;
+};
+
+#endif // SOCKETAPISOCKET_OSX_H
--- /dev/null
+/*
+ * Copyright (C) by Jocelyn Turcotte <jturcotte@woboq.com>
+ *
+ * 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 <Cocoa/Cocoa.h>
+
+@protocol ChannelProtocol <NSObject>
+- (void)sendMessage:(NSData *)msg;
+@end
+
+@protocol RemoteEndProtocol <NSObject, ChannelProtocol>
+- (void)registerTransmitter:(id)tx;
+@end
+
+@interface LocalEnd : NSObject <ChannelProtocol>
+@property SocketApiSocketPrivate *wrapper;
+- (instancetype)initWithWrapper:(SocketApiSocketPrivate *)wrapper;
+@end
+
+@interface Server : NSObject
+@property SocketApiServerPrivate *wrapper;
+- (instancetype)initWithWrapper:(SocketApiServerPrivate *)wrapper;
+- (void)registerClient:(NSDistantObject<RemoteEndProtocol> *)remoteEnd;
+@end
+
+class SocketApiSocketPrivate
+{
+public:
+ SocketApiSocket *q_ptr;
+
+ SocketApiSocketPrivate(NSDistantObject<ChannelProtocol> *remoteEnd);
+ ~SocketApiSocketPrivate();
+
+ // release remoteEnd
+ void disconnectRemote();
+
+ NSDistantObject<ChannelProtocol> *remoteEnd;
+ LocalEnd *localEnd;
+ QByteArray inBuffer;
+ bool isRemoteDisconnected = false;
+};
+
+class SocketApiServerPrivate
+{
+public:
+ SocketApiServer *q_ptr;
+
+ SocketApiServerPrivate();
+ ~SocketApiServerPrivate();
+
+ QList<SocketApiSocket *> 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<RemoteEndProtocol> *)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<qint64>(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<char *>(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<ChannelProtocol> *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];
+}
--- /dev/null
+/*
+ * Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
+ *
+ * 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 <QFileInfo>
+#include <QJsonArray>
+#include <QRegularExpression>
+
+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();
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
+ *
+ * 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 <QObject>
+#include <QTemporaryFile>
+
+#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;
+};
+}
+++ /dev/null
-/*
- * Copyright (C) by Dominik Schmidt <dev@dominik-schmidt.de>
- * Copyright (C) by Klaas Freitag <freitag@owncloud.com>
- * Copyright (C) by Roeland Jago Douma <roeland@famdouma.nl>
- *
- * 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 <functional>
-#include <QBitArray>
-#include <QPointer>
-
-#include <QJsonDocument>
-#include <QJsonObject>
-
-#include <memory>
-#include <QTimer>
-
-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<QIODevice> 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<void()>;
- 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
+++ /dev/null
-/*
- * Copyright (C) by Jocelyn Turcotte <jturcotte@woboq.com>
- *
- * 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 <QAbstractSocket>
-#include <QIODevice>
-
-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<SocketApiSocketPrivate> 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<SocketApiServerPrivate> d_ptr;
-};
-
-#endif // SOCKETAPISOCKET_OSX_H
+++ /dev/null
-/*
- * Copyright (C) by Jocelyn Turcotte <jturcotte@woboq.com>
- *
- * 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 <Cocoa/Cocoa.h>
-
-@protocol ChannelProtocol <NSObject>
-- (void)sendMessage:(NSData*)msg;
-@end
-
-@protocol RemoteEndProtocol <NSObject, ChannelProtocol>
-- (void)registerTransmitter:(id)tx;
-@end
-
-@interface LocalEnd : NSObject <ChannelProtocol>
-@property SocketApiSocketPrivate *wrapper;
-- (instancetype)initWithWrapper:(SocketApiSocketPrivate *)wrapper;
-@end
-
-@interface Server : NSObject
-@property SocketApiServerPrivate *wrapper;
-- (instancetype)initWithWrapper:(SocketApiServerPrivate *)wrapper;
-- (void)registerClient:(NSDistantObject <RemoteEndProtocol> *)remoteEnd;
-@end
-
-class SocketApiSocketPrivate
-{
-public:
- SocketApiSocket *q_ptr;
-
- SocketApiSocketPrivate(NSDistantObject <ChannelProtocol> *remoteEnd);
- ~SocketApiSocketPrivate();
-
- // release remoteEnd
- void disconnectRemote();
-
- NSDistantObject <ChannelProtocol> *remoteEnd;
- LocalEnd *localEnd;
- QByteArray inBuffer;
- bool isRemoteDisconnected = false;
-};
-
-class SocketApiServerPrivate
-{
-public:
- SocketApiServer *q_ptr;
-
- SocketApiServerPrivate();
- ~SocketApiServerPrivate();
-
- QList<SocketApiSocket*> 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 <RemoteEndProtocol> *)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<qint64>(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<char *>(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 <ChannelProtocol> *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];
-}
{
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.
#include <QTimer>
#include <QObject>
#include <QTimerEvent>
+#include <QRegularExpression>
#include <qmath.h>
namespace OCC {
return smallFileSize;
}
-void OwncloudPropagator::start(const SyncFileItemVector &items)
+void OwncloudPropagator::start(SyncFileItemVector &&items)
{
Q_ASSERT(std::is_sorted(items.begin(), items.end()));
* 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<QStringRef> 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<QPair<QString /* directory name */, PropagateDirectory * /* job */>> directories;
directories.push(qMakePair(QString(), _rootJob.data()));
~OwncloudPropagator();
- void start(const SyncFileItemVector &_syncedItems);
+ void start(SyncFileItemVector &&_syncedItems);
const SyncOptions &syncOptions() const;
void setSyncOptions(const SyncOptions &syncOptions);
}
if (s_anySyncRunning || _syncRunning) {
- ASSERT(false);
+ ASSERT(false)
return;
}
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";
};
--- /dev/null
+/*
+ * Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
+ *
+ * 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 <QRegularExpression>
+
+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);
+}
#pragma once
#include "owncloudlib.h"
-#include <QString>
+#include "common/vfs.h"
+
+#include <QRegularExpression>
#include <QSharedPointer>
+#include <QString>
+
#include <chrono>
-#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();
* 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("("));
};
}