Sfoglia il codice sorgente

Implement URI handler for local file editing

Signed-off-by: alex-z <blackslayer4@gmail.com>
alex-z 3 anni fa
parent
commit
d42d3c057f

+ 1 - 0
NEXTCLOUD.cmake

@@ -5,6 +5,7 @@ set( APPLICATION_DOMAIN     "nextcloud.com" )
 set( APPLICATION_VENDOR     "Nextcloud GmbH" )
 set( APPLICATION_UPDATE_URL "https://updates.nextcloud.org/client/" CACHE STRING "URL for updater" )
 set( APPLICATION_HELP_URL   "" CACHE STRING "URL for the help menu" )
+set( APPLICATION_URI_HANDLER_SCHEME "nc")
 
 if(APPLE AND APPLICATION_NAME STREQUAL "Nextcloud" AND EXISTS "${CMAKE_SOURCE_DIR}/theme/colored/Nextcloud-macOS-icon.svg")
     set( APPLICATION_ICON_NAME "Nextcloud-macOS" )

+ 14 - 0
admin/win/msi/Nextcloud.wxs

@@ -190,6 +190,19 @@
                 <!-- Property to disable update checks -->
                 <RegistryValue Type="integer" Name="skipUpdateCheck" Value="[SKIPAUTOUPDATE]" />
             </RegistryKey>
+        </Component>
+		<!-- Register URI handler -->
+        <Component Id="RegistryUriHandler" Guid="*" Win64="$(var.PlatformWin64)">
+            <RegistryKey Root="HKCU" Key="Software\Classes\$(var.AppCommandOpenUrlScheme)" ForceCreateOnInstall="yes" ForceDeleteOnUninstall="yes">
+                <RegistryValue Type="string" Value="URL:$(var.AppName) Protocol" />
+				<RegistryValue Type="string" Name="URL Protocol" Value="" />
+            </RegistryKey>
+            <RegistryKey Root="HKCU" Key="Software\Classes\$(var.AppCommandOpenUrlScheme)\DefaultIcon" ForceCreateOnInstall="yes" ForceDeleteOnUninstall="yes">
+                <RegistryValue Type="string" Value="[INSTALLDIR]$(var.AppExe)" />
+            </RegistryKey>
+            <RegistryKey Root="HKCU" Key="Software\Classes\$(var.AppCommandOpenUrlScheme)\shell\open\command" ForceCreateOnInstall="yes" ForceDeleteOnUninstall="yes">
+                <RegistryValue Type="string" Value="&quot;[INSTALLDIR]$(var.AppExe)&quot; &quot;%1&quot;" />
+            </RegistryKey>
         </Component>
     </DirectoryRef>
 
@@ -200,6 +213,7 @@
 
         <ComponentRef Id="RegistryVersionInfo" />
         <ComponentRef Id="RegistryDefaultSettings" />
+		<ComponentRef Id="RegistryUriHandler" />
 
         <Feature Id="ShellExtensions" Title="Integration for Windows Explorer"
             Description="This feature requires a reboot." >

+ 2 - 0
admin/win/msi/OEM.wxi.in

@@ -28,6 +28,8 @@
 
     <?define AppHelpLink = "https://@APPLICATION_DOMAIN@/" ?>
     <?define AppInfoLink = "$(var.AppHelpLink)" ?>
+	
+	<?define AppCommandOpenUrlScheme = "@APPLICATION_URI_HANDLER_SCHEME@" ?>
 
     <!-- Custom license: To use it, also remove the "Skip the license page" stuff in the <UI> section
                          and uncomment <WixVariable Id="WixUILicenseRtf"...

+ 11 - 0
cmake/modules/MacOSXBundleInfo.plist.in

@@ -76,6 +76,17 @@
     </dict>
 </array>
 
+<key>CFBundleURLTypes</key>
+<array>
+    <dict>
+        <key>CFBundleURLName</key>
+        <string>@APPLICATION_NAME@ Edit Locally</string>
+        <key>CFBundleURLSchemes</key>
+        <array>
+            <string>@APPLICATION_URI_HANDLER_SCHEME@</string>
+        </array>
+    </dict>
+</array>
 
 </dict>
 </plist>

+ 1 - 0
config.h.in

@@ -32,6 +32,7 @@
 #cmakedefine APPLICATION_OCSP_STAPLING_ENABLED "@APPLICATION_OCSP_STAPLING_ENABLED@"
 #cmakedefine APPLICATION_FORBID_BAD_SSL "@APPLICATION_FORBID_BAD_SSL@"
 #define APPLICATION_DOTVIRTUALFILE_SUFFIX "." APPLICATION_VIRTUALFILE_SUFFIX
+#define APPLICATION_URI_HANDLER_SCHEME "@APPLICATION_URI_HANDLER_SCHEME@"
 #cmakedefine01 ENFORCE_VIRTUAL_FILES_SYNC_FOLDER
 #cmakedefine DO_NOT_USE_PROXY "@DO_NOT_USE_PROXY@"
 

File diff suppressed because it is too large
+ 0 - 0
doc/architecture.rst


+ 2 - 2
mirall.desktop.in

@@ -1,14 +1,14 @@
 [Desktop Entry]
 Categories=Utility;X-SuSE-SyncUtility;
 Type=Application
-Exec=@APPLICATION_EXECUTABLE@
+Exec=@APPLICATION_EXECUTABLE@ %u
 Name=@APPLICATION_NAME@ Desktop
 Comment=@APPLICATION_NAME@ desktop synchronization client
 GenericName=Folder Sync
 Icon=@APPLICATION_ICON_NAME@
 Keywords=@APPLICATION_NAME@;syncing;file;sharing;
 X-GNOME-Autostart-Delay=3
-MimeType=application/vnd.@APPLICATION_EXECUTABLE@;
+MimeType=application/vnd.@APPLICATION_EXECUTABLE@;x-scheme-handler/@APPLICATION_URI_HANDLER_SCHEME@;
 Actions=Quit;
 
 # Translations

+ 0 - 11
src/3rdparty/qtsingleapplication/qtsingleapplication.cpp

@@ -33,7 +33,6 @@
 #include <qtlockedfile.h>
 
 #include <QDir>
-#include <QFileOpenEvent>
 #include <QSharedMemory>
 #include <QWidget>
 
@@ -119,16 +118,6 @@ QtSingleApplication::~QtSingleApplication()
     lockfile.unlock();
 }
 
-bool QtSingleApplication::event(QEvent *event)
-{
-    if (event->type() == QEvent::FileOpen) {
-        auto *foe = static_cast<QFileOpenEvent*>(event);
-        emit fileOpenRequest(foe->file());
-        return true;
-    }
-    return QApplication::event(event);
-}
-
 bool QtSingleApplication::isRunning(qint64 pid)
 {
     if (pid == -1) {

+ 0 - 1
src/3rdparty/qtsingleapplication/qtsingleapplication.h

@@ -50,7 +50,6 @@ public:
 
     void setActivationWindow(QWidget* aw, bool activateOnMessage = true);
     QWidget* activationWindow() const;
-    bool event(QEvent *event) override;
 
     QString applicationId() const;
     void setBlock(bool value);

+ 5 - 0
src/common/utility.h

@@ -247,6 +247,11 @@ namespace Utility {
      */
     OCSYNC_EXPORT QString getCurrentUserName();
 
+    /**
+     * @brief Registers the desktop app as a handler for a custom URI to enable local editing
+     */
+    OCSYNC_EXPORT void registerUriHandlerForLocalEditing();
+
 #ifdef Q_OS_WIN
     OCSYNC_EXPORT bool registryKeyExists(HKEY hRootKey, const QString &subKey);
     OCSYNC_EXPORT QVariant registryGetKeyValue(HKEY hRootKey, const QString &subKey, const QString &valueName);

+ 2 - 0
src/common/utility_mac.cpp

@@ -141,4 +141,6 @@ QString Utility::getCurrentUserName()
     return {};
 }
 
+void Utility::registerUriHandlerForLocalEditing() { /* URI handler is registered via MacOSXBundleInfo.plist.in */ }
+
 } // namespace OCC

+ 23 - 0
src/common/utility_unix.cpp

@@ -19,6 +19,7 @@
 
 #include <QStandardPaths>
 #include <QtGlobal>
+#include <QProcess>
 
 namespace OCC {
 
@@ -113,4 +114,26 @@ QString Utility::getCurrentUserName()
     return {};
 }
 
+void Utility::registerUriHandlerForLocalEditing()
+{
+    const auto appImagePath = qEnvironmentVariable("APPIMAGE");
+    const auto runningInsideAppImage = !appImagePath.isNull() && QFile::exists(appImagePath);
+
+    if (!runningInsideAppImage) {
+        // only register x-scheme-handler if running inside appImage
+        return;
+    }
+
+    // mirall.desktop.in must have an x-scheme-handler mime type specified
+    const QString desktopFileName = QLatin1String(LINUX_APPLICATION_ID) + QLatin1String(".desktop");
+    QProcess process;
+    const QStringList args = {
+        QLatin1String("default"),
+        desktopFileName,
+        QStringLiteral("x-scheme-handler/%1").arg(QStringLiteral(APPLICATION_URI_HANDLER_SCHEME))
+    };
+    process.start(QStringLiteral("xdg-mime"), args, QIODevice::ReadOnly);
+    process.waitForFinished();
+}
+
 } // namespace OCC

+ 2 - 0
src/common/utility_win.cpp

@@ -448,6 +448,8 @@ QString Utility::getCurrentUserName()
     return QString::fromWCharArray(username);
 }
 
+void Utility::registerUriHandlerForLocalEditing() { /* URI handler is registered via Nextcloud.wxs */ }
+
 Utility::NtfsPermissionLookupRAII::NtfsPermissionLookupRAII()
 {
     qt_ntfs_permission_lookup++;

+ 59 - 8
src/gui/application.cpp

@@ -406,6 +406,8 @@ Application::Application(int &argc, char **argv)
     connect(_gui.data(), &ownCloudGui::isShowingSettingsDialog, this, &Application::slotGuiIsShowingSettings);
 
     _gui->createTray();
+
+    handleEditLocallyFromOptions();
 }
 
 Application::~Application()
@@ -572,6 +574,8 @@ void Application::slotParseMessage(const QString &msg, QObject *)
             qApp->quit();
         }
 
+        handleEditLocallyFromOptions();
+
     } else if (msg.startsWith(QLatin1String("MSG_SHOWMAINDIALOG"))) {
         qCInfo(lcApplication) << "Running for" << _startedAt.elapsed() / 1000.0 << "sec";
         if (_startedAt.elapsed() < 10 * 1000) {
@@ -647,7 +651,17 @@ void Application::parseOptions(const QStringList &options)
         } else if (option.endsWith(QStringLiteral(APPLICATION_DOTVIRTUALFILE_SUFFIX))) {
             // virtual file, open it after the Folder were created (if the app is not terminated)
             QTimer::singleShot(0, this, [this, option] { openVirtualFile(option); });
-        } else {
+        } else if (option.startsWith(QStringLiteral(APPLICATION_URI_HANDLER_SCHEME "://open"))) {
+            // see the section Local file editing of the Architecture page of the user documenation
+            _editFileLocallyUrl = QUrl::fromUserInput(option);
+            if (!_editFileLocallyUrl.isValid()) {
+                _editFileLocallyUrl.clear();
+                const auto errorParsingLocalFileEditingUrl = QStringLiteral("The supplied url for local file editing '%1' is invalid!").arg(option);
+                qCInfo(lcApplication) << errorParsingLocalFileEditingUrl;
+                showHint(errorParsingLocalFileEditingUrl.toStdString());
+            }
+        }
+        else {
             showHint("Unrecognized option '" + option.toStdString() + "'");
         }
     }
@@ -728,6 +742,32 @@ void Application::setHelp()
     _helpOnly = true;
 }
 
+void Application::handleEditLocallyFromOptions()
+{
+    if (!_editFileLocallyUrl.isValid()) {
+        return;
+    }
+
+    handleEditLocally(_editFileLocallyUrl);
+    _editFileLocallyUrl.clear();
+}
+
+void Application::handleEditLocally(const QUrl &url) const
+{
+    auto pathSplit = url.path().split('/', Qt::SkipEmptyParts);
+
+    if (pathSplit.size() < 2) {
+        qCWarning(lcApplication) << "Invalid URL for file local editing: " + pathSplit.join('/');
+        return;
+    }
+
+    // for a sample URL "nc://open/admin@nextcloud.lan:8080/Photos/lovely.jpg", QUrl::path would return "admin@nextcloud.lan:8080/Photos/lovely.jpg"
+    const auto accountDisplayName = pathSplit.takeFirst();
+    const auto fileRemotePath = pathSplit.join('/');
+
+    FolderMan::instance()->editFileLocally(accountDisplayName, fileRemotePath);
+}
+
 QString substLang(const QString &lang)
 {
     // Map the more appropriate script codes
@@ -855,15 +895,26 @@ void Application::tryTrayAgain()
 
 bool Application::event(QEvent *event)
 {
-#ifdef Q_OS_MAC
     if (event->type() == QEvent::FileOpen) {
-        QFileOpenEvent *openEvent = static_cast<QFileOpenEvent *>(event);
-        qCDebug(lcApplication) << "QFileOpenEvent" << openEvent->file();
-        // virtual file, open it after the Folder were created (if the app is not terminated)
-        QString fn = openEvent->file();
-        QTimer::singleShot(0, this, [this, fn] { openVirtualFile(fn); });
+        const auto openEvent = static_cast<QFileOpenEvent *>(event);
+        qCDebug(lcApplication) << "macOS: Received a QFileOpenEvent";
+
+        if(!openEvent->file().isEmpty()) {
+            qCDebug(lcApplication) << "QFileOpenEvent" << openEvent->file();
+            // virtual file, open it after the Folder were created (if the app is not terminated)
+            const auto fn = openEvent->file();
+            QTimer::singleShot(0, this, [this, fn] { openVirtualFile(fn); });
+        } else if (!openEvent->url().isEmpty() && openEvent->url().isValid()) {
+            // On macOS, Qt does not handle receiving a custom URI as it does on other systems (as an application argument).
+            // Instead, it sends out a QFileOpenEvent. We therefore need custom handling for our URI handling on macOS.
+            qCInfo(lcApplication) << "macOS: Opening local file for editing: " << openEvent->url();
+            handleEditLocally(openEvent->url());
+        } else {
+            const auto errorParsingLocalFileEditingUrl = QStringLiteral("The supplied url for local file editing '%1' is invalid!").arg(openEvent->url().toString());
+            qCInfo(lcApplication) << errorParsingLocalFileEditingUrl;
+            showHint(errorParsingLocalFileEditingUrl.toStdString());
+        }
     }
-#endif
     return SharedTools::QtSingleApplication::event(event);
 }
 

+ 7 - 1
src/gui/application.h

@@ -72,6 +72,8 @@ public:
 
     ownCloudGui *gui() const;
 
+    bool event(QEvent *event) override;
+
 public slots:
     // TODO: this should not be public
     void slotownCloudWizardDone(int);
@@ -85,11 +87,12 @@ public slots:
     /// Attempt to show() the tray icon again. Used if no systray was available initially.
     void tryTrayAgain();
 
+    void handleEditLocally(const QUrl &url) const;
+
 protected:
     void parseOptions(const QStringList &);
     void setupTranslations();
     void setupLogging();
-    bool event(QEvent *event) override;
 
 signals:
     void folderRemoved();
@@ -109,6 +112,8 @@ protected slots:
 private:
     void setHelp();
 
+    void handleEditLocallyFromOptions();
+
     /**
      * Maybe a newer version of the client was used with this config file:
      * if so, backup, confirm with user and remove the config that can't be read.
@@ -135,6 +140,7 @@ private:
     bool _userTriggeredConnect;
     bool _debugMode;
     bool _backgroundMode;
+    QUrl _editFileLocallyUrl;
 
     ClientProxy _proxy;
 

+ 55 - 6
src/gui/cocoainitializer_mac.mm

@@ -17,23 +17,72 @@
 #import <Foundation/NSAutoreleasePool.h>
 #import <AppKit/NSApplication.h>
 
+#include "application.h"
+
+/* In theory, we should be able to just capture QFileOpenEvents
+ * when we open our custom URLs in our Application class and be
+ * done with it, but in practice the QFileOpenEvent often doesn't
+ * get sent for our URLs. We have this in place to work around
+ * the issue.
+ *
+ * This class sets a callback selector on URL-related events
+ * before the application is fully done launching. This lets us
+ * properly receive and process "open url" events even if the
+ * client was closed when these events were sent. */
+
+@interface URLEventHandler : NSObject
+@end
+
+@implementation URLEventHandler
+- (id)init {
+    self = [super init];
+
+    if (self) {
+        NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
+        [defaultCenter addObserver:self
+                        selector:@selector(applicationWillFinishLaunching:)
+                        name:NSApplicationWillFinishLaunchingNotification
+                        object:nil];
+    }
+    return self;
+}
+
+- (void)applicationWillFinishLaunching:(NSNotification *)aNotification {
+    [[NSAppleEventManager sharedAppleEventManager] setEventHandler:self
+                                                       andSelector:@selector(handleURLEvent:withReplyEvent:)
+                                                     forEventClass:kInternetEventClass
+                                                        andEventID:kAEGetURL];
+}
+
+- (void)handleURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent
+{
+    NSURL* url = [NSURL URLWithString:[[event paramDescriptorForKeyword:keyDirectObject] stringValue]];
+    const auto app = qobject_cast<OCC::Application *>(QApplication::instance());
+    const auto qtUrl = QUrl::fromNSURL(url);
+    app->handleEditLocally(qtUrl);
+}
+
+@end
+
 namespace OCC {
 namespace Mac {
 
 class CocoaInitializer::Private {
-  public:
+public:
     NSAutoreleasePool* autoReleasePool;
+    URLEventHandler* handler;
 };
 
 CocoaInitializer::CocoaInitializer() {
-  d = new CocoaInitializer::Private();
-  NSApplicationLoad();
-  d->autoReleasePool = [[NSAutoreleasePool alloc] init];
+    d = new CocoaInitializer::Private();
+    d->handler = [[URLEventHandler alloc] init];
+    NSApplicationLoad();
+    d->autoReleasePool = [[NSAutoreleasePool alloc] init];
 }
 
 CocoaInitializer::~CocoaInitializer() {
-  [d->autoReleasePool release];
-  delete d;
+    [d->autoReleasePool release];
+    delete d;
 }
 
 } // namespace Mac

+ 62 - 0
src/gui/folderman.cpp

@@ -36,6 +36,8 @@
 #include <QMutableSetIterator>
 #include <QSet>
 #include <QNetworkProxy>
+#include <QDesktopServices>
+#include <QtConcurrent>
 
 static const char versionC[] = "version";
 static const int maxFoldersVersion = 1;
@@ -163,6 +165,8 @@ void FolderMan::registerFolderWithSocketApi(Folder *folder)
 
 int FolderMan::setupFolders()
 {
+    Utility::registerUriHandlerForLocalEditing();
+
     unloadAndDeleteAllFolders();
 
     QStringList skipSettingsKeys;
@@ -1402,6 +1406,64 @@ void FolderMan::setDirtyNetworkLimits()
     }
 }
 
+void FolderMan::editFileLocally(const QString &accountDisplayName, const QString &relPath)
+{
+    const auto showError = [this](const OCC::AccountStatePtr accountState, const QString &errorMessage, const QString &subject) {
+        if (accountState && accountState->account()) {
+            const auto foundFolder = std::find_if(std::cbegin(map()), std::cend(map()), [accountState](const auto &folder) {
+                return accountState->account()->davUrl() == folder->remoteUrl();
+            });
+
+            if (foundFolder != std::cend(map())) {
+                (*foundFolder)->syncEngine().addErrorToGui(SyncFileItem::SoftError, errorMessage, subject);
+            }
+        }
+
+        // to make sure the error is not missed, show a message box in addition
+        const auto messageBox = new QMessageBox;
+        messageBox->setAttribute(Qt::WA_DeleteOnClose);
+        messageBox->setText(errorMessage);
+        messageBox->setInformativeText(subject);
+        messageBox->setIcon(QMessageBox::Warning);
+        messageBox->addButton(QMessageBox::StandardButton::Ok);
+        messageBox->show();
+        messageBox->activateWindow();
+        messageBox->raise();
+    };
+
+    const auto accountFound = AccountManager::instance()->account(accountDisplayName);
+
+    if (!accountFound) {
+        qCWarning(lcFolderMan) << "Could not find an account " << accountDisplayName << " to edit file " << relPath << " locally.";
+        showError(accountFound, tr("Could not find an account for local editing"), accountDisplayName);
+        return;
+    }
+
+    const auto foundFiles = findFileInLocalFolders(relPath, accountFound->account());
+
+    if (foundFiles.isEmpty()) {
+        for (const auto &folder : map()) {
+            bool result = false;
+            const auto excludedThroughSelectiveSync = folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &result);
+            for (const auto &excludedPath : excludedThroughSelectiveSync) {
+                if (relPath.startsWith(excludedPath)) {
+                    showError(accountFound, tr("Could not find a file for local editing. Make sure it is not excluded via selective sync."), relPath);
+                    return;
+                }
+            }
+        }
+
+        showError(accountFound, tr("Could not find a file for local editing. Make sure its path is valid and it is synced locally."), relPath);
+        return;
+    }
+
+    // In case the VFS mode is enabled and a file is not yet hydrated, we must call QDesktopServices::openUrl from a separate thread, or, there will be a freeze.
+    // To avoid searching for a specific folder and checking if the VFS is enabled - we just always call it from a separate thread.
+    QtConcurrent::run([foundFiles] {
+        QDesktopServices::openUrl(QUrl::fromLocalFile(foundFiles.first()));
+    });
+}
+
 void FolderMan::trayOverallStatus(const QList<Folder *> &folders,
     SyncResult::Status *status, bool *unresolvedConflicts)
 {

+ 3 - 0
src/gui/folderman.h

@@ -202,6 +202,9 @@ public:
     void setDirtyProxy();
     void setDirtyNetworkLimits();
 
+    /** opens a file with default app, if the file is present **/
+    void editFileLocally(const QString &accountDisplayName, const QString &relPath);
+
 signals:
     /**
       * signal to indicate a folder has changed its sync state.

Some files were not shown because too many files changed in this diff