Skip to content

Commit

Permalink
Merge pull request #4771 from nextcloud/feature/handle-edit-locally
Browse files Browse the repository at this point in the history
Feature/handle edit locally
  • Loading branch information
allexzander authored Aug 3, 2022
2 parents 4f85f7a + d42d3c0 commit 0945466
Show file tree
Hide file tree
Showing 18 changed files with 269 additions and 29 deletions.
1 change: 1 addition & 0 deletions NEXTCLOUD.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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" )
Expand Down
14 changes: 14 additions & 0 deletions admin/win/msi/Nextcloud.wxs
Original file line number Diff line number Diff line change
Expand Up @@ -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>

Expand All @@ -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." >
Expand Down
2 changes: 2 additions & 0 deletions admin/win/msi/OEM.wxi.in
Original file line number Diff line number Diff line change
Expand Up @@ -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"...
Expand Down
11 changes: 11 additions & 0 deletions cmake/modules/MacOSXBundleInfo.plist.in
Original file line number Diff line number Diff line change
Expand Up @@ -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 change: 1 addition & 0 deletions config.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -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@"

Expand Down
20 changes: 20 additions & 0 deletions doc/architecture.rst
Original file line number Diff line number Diff line change
Expand Up @@ -440,3 +440,23 @@ Files that must be removed from the local storage only, need to be dehydrated vi

.. note::
* End-to-end Encryption works with Virtual Files (VFS) but only on a per-folder level. Folders with E2EE can be made available offline in their entirety, but the individual files in them can not be retrieved on demand. This is mainly due to two technical reasons. First, the Windows VFS API is not designed for handling encrypted files. Second, while the VFS is designed to deal mostly with large files, E2EE is mostly recommended for use with small files as encrypting and decrypting large files puts large demands on the computer infrastructure.

Local file editing
------------------

The Nextcloud desktop GUI client supports local editing when opening a URL that starts with
a scheme ``nc://`` followed by an ``open`` command, followed by a user email (with port when needed),
followed by file path relative to remote root.

Examples of URLs that Nextcloud can handle if the user email and a path to a file is correct:
- ``nc://open/admin@example.cloud:8080/Photos/lovely.jpg``
- ``nc://open/user@example.cloud/Photos/lovely.jpg``
- ``nc://open/user@example.cloud/Documents/sheets/report.xlsx``
- ``nc://open/user@example.cloud/Documents/docs/document.docx``

.. note::
* All the file paths that begin after user email are relative to remote root (``/``).
* The server is responsible for generating a correct URL that a user then clicks to edit file locally.
* The Nextcloud desktop client is registered in macOS, Linux, and Windows as a custom URI handler for the ``nc://`` scheme.
* The URL is parsed and validated by Nextcloud desktop client, so, opening an incorrectly formatted URL will not have any effect.
* The port after user email is necessary if the default :80 or :443 is not used. The rule of thumb is to always have a port added if you need it when accessing your server via Web UI
4 changes: 2 additions & 2 deletions mirall.desktop.in
Original file line number Diff line number Diff line change
@@ -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
Expand Down
11 changes: 0 additions & 11 deletions src/3rdparty/qtsingleapplication/qtsingleapplication.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
#include <qtlockedfile.h>

#include <QDir>
#include <QFileOpenEvent>
#include <QSharedMemory>
#include <QWidget>

Expand Down Expand Up @@ -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) {
Expand Down
1 change: 0 additions & 1 deletion src/3rdparty/qtsingleapplication/qtsingleapplication.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ class QtSingleApplication : public QApplication

void setActivationWindow(QWidget* aw, bool activateOnMessage = true);
QWidget* activationWindow() const;
bool event(QEvent *event) override;

QString applicationId() const;
void setBlock(bool value);
Expand Down
5 changes: 5 additions & 0 deletions src/common/utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/common/utility_mac.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,6 @@ QString Utility::getCurrentUserName()
return {};
}

void Utility::registerUriHandlerForLocalEditing() { /* URI handler is registered via MacOSXBundleInfo.plist.in */ }

} // namespace OCC
23 changes: 23 additions & 0 deletions src/common/utility_unix.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

#include <QStandardPaths>
#include <QtGlobal>
#include <QProcess>

namespace OCC {

Expand Down Expand Up @@ -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 changes: 2 additions & 0 deletions src/common/utility_win.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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++;
Expand Down
67 changes: 59 additions & 8 deletions src/gui/application.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,8 @@ Application::Application(int &argc, char **argv)
connect(_gui.data(), &ownCloudGui::isShowingSettingsDialog, this, &Application::slotGuiIsShowingSettings);

_gui->createTray();

handleEditLocallyFromOptions();
}

Application::~Application()
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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() + "'");
}
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}

Expand Down
8 changes: 7 additions & 1 deletion src/gui/application.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ class Application : public SharedTools::QtSingleApplication

ownCloudGui *gui() const;

bool event(QEvent *event) override;

public slots:
// TODO: this should not be public
void slotownCloudWizardDone(int);
Expand All @@ -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();
Expand All @@ -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.
Expand All @@ -135,6 +140,7 @@ protected slots:
bool _userTriggeredConnect;
bool _debugMode;
bool _backgroundMode;
QUrl _editFileLocallyUrl;

ClientProxy _proxy;

Expand Down
Loading

0 comments on commit 0945466

Please sign in to comment.