From 001deace2d5cc9c0c4bcf782ac21f42fe17177d7 Mon Sep 17 00:00:00 2001 From: alex-z Date: Mon, 4 Jul 2022 15:36:06 +0300 Subject: [PATCH] Implement COM Dll for CfApi shell extensins. Implement Thumbnail Provider. Signed-off-by: alex-z --- NEXTCLOUD.cmake | 10 + admin/win/msi/CMakeLists.txt | 3 +- ...stryCleanup.vbs => RegistryCleanup.vbs.in} | 20 ++ config.h.in | 9 + src/common/filesystembase.h | 2 +- src/common/shellextensionutils.cpp | 36 +++ src/common/shellextensionutils.h | 35 +++ src/common/utility.h | 3 +- src/common/utility_win.cpp | 49 +++- src/common/vfs.h | 3 + src/gui/CMakeLists.txt | 2 +- src/gui/application.cpp | 4 + src/gui/application.h | 4 + src/gui/folder.cpp | 1 + src/gui/folderman.h | 2 + src/gui/shellextensionsserver.cpp | 155 +++++++++++++ src/gui/shellextensionsserver.h | 52 +++++ src/libsync/vfs/cfapi/CMakeLists.txt | 2 + src/libsync/vfs/cfapi/cfapiwrapper.cpp | 69 +++++- src/libsync/vfs/cfapi/cfapiwrapper.h | 4 +- src/libsync/vfs/cfapi/shellext/CMakeLists.txt | 31 +++ .../cfapi/shellext/CfApiShellIntegration.def | 3 + .../cfapishellintegrationclassfactory.cpp | 97 ++++++++ .../cfapishellintegrationclassfactory.h | 50 ++++ src/libsync/vfs/cfapi/shellext/dllmain.cpp | 58 +++++ .../vfs/cfapi/shellext/thumbnailprovider.cpp | 160 +++++++++++++ .../vfs/cfapi/shellext/thumbnailprovider.h | 52 +++++ .../cfapi/shellext/thumbnailprovideripc.cpp | 134 +++++++++++ .../vfs/cfapi/shellext/thumbnailprovideripc.h | 46 ++++ src/libsync/vfs/cfapi/vfs_cfapi.cpp | 66 +++++- test/CMakeLists.txt | 2 + test/syncenginetestutils.cpp | 3 + test/syncenginetestutils.h | 2 + test/testcfapishellextensionsipc.cpp | 216 ++++++++++++++++++ 34 files changed, 1367 insertions(+), 18 deletions(-) rename admin/win/msi/{RegistryCleanup.vbs => RegistryCleanup.vbs.in} (69%) create mode 100644 src/common/shellextensionutils.cpp create mode 100644 src/common/shellextensionutils.h create mode 100644 src/gui/shellextensionsserver.cpp create mode 100644 src/gui/shellextensionsserver.h create mode 100644 src/libsync/vfs/cfapi/shellext/CMakeLists.txt create mode 100644 src/libsync/vfs/cfapi/shellext/CfApiShellIntegration.def create mode 100644 src/libsync/vfs/cfapi/shellext/cfapishellintegrationclassfactory.cpp create mode 100644 src/libsync/vfs/cfapi/shellext/cfapishellintegrationclassfactory.h create mode 100644 src/libsync/vfs/cfapi/shellext/dllmain.cpp create mode 100644 src/libsync/vfs/cfapi/shellext/thumbnailprovider.cpp create mode 100644 src/libsync/vfs/cfapi/shellext/thumbnailprovider.h create mode 100644 src/libsync/vfs/cfapi/shellext/thumbnailprovideripc.cpp create mode 100644 src/libsync/vfs/cfapi/shellext/thumbnailprovideripc.h create mode 100644 test/testcfapishellextensionsipc.cpp diff --git a/NEXTCLOUD.cmake b/NEXTCLOUD.cmake index 67b56ccc5..a28ef387e 100644 --- a/NEXTCLOUD.cmake +++ b/NEXTCLOUD.cmake @@ -69,6 +69,16 @@ if(WIN32) # MSI Upgrade Code (without brackets) set( WIN_MSI_UPGRADE_CODE "FD2FCCA9-BB8F-4485-8F70-A0621B84A7F4" ) + # CfAPI Shell Extensions + set( CFAPI_SHELL_EXTENSIONS_LIB_NAME CfApiShellExtensions ) + + set( CFAPI_SHELLEXT_APPID_REG "{E314A650-DCA4-416E-974E-18EA37C213EA}") + set( CFAPI_SHELLEXT_APPID_DISPLAY_NAME "${APPLICATION_NAME} CfApi Shell Extensions" ) + + set( CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID "6FF9B5B6-389F-444A-9FDD-A286C36EA079" ) + set( CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG "{${CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID}}" ) + set( CFAPI_SHELLEXT_THUMBNAIL_HANDLER_DISPLAY_NAME "${APPLICATION_NAME} Thumbnail Handler" ) + # Windows build options option( BUILD_WIN_MSI "Build MSI scripts and helper DLL" OFF ) option( BUILD_WIN_TOOLS "Build Win32 migration tools" OFF ) diff --git a/admin/win/msi/CMakeLists.txt b/admin/win/msi/CMakeLists.txt index 540bacad9..933037dac 100644 --- a/admin/win/msi/CMakeLists.txt +++ b/admin/win/msi/CMakeLists.txt @@ -16,6 +16,7 @@ endif() set(MSI_INSTALLER_FILENAME "${APPLICATION_SHORTNAME}-${VERSION}${VERSION_SUFFIX}-${MSI_BUILD_ARCH}.msi") +configure_file(RegistryCleanup.vbs.in ${CMAKE_CURRENT_BINARY_DIR}/RegistryCleanup.vbs) configure_file(OEM.wxi.in ${CMAKE_CURRENT_BINARY_DIR}/OEM.wxi) configure_file(collect-transform.xsl.in ${CMAKE_CURRENT_BINARY_DIR}/collect-transform.xsl) configure_file(make-msi.bat.in ${CMAKE_CURRENT_BINARY_DIR}/make-msi.bat) @@ -26,7 +27,7 @@ install(FILES ${CMAKE_CURRENT_BINARY_DIR}/make-msi.bat Platform.wxi Nextcloud.wxs - RegistryCleanup.vbs + ${CMAKE_CURRENT_BINARY_DIR}/RegistryCleanup.vbs RegistryCleanupCustomAction.wxs gui/banner.bmp gui/dialog.bmp diff --git a/admin/win/msi/RegistryCleanup.vbs b/admin/win/msi/RegistryCleanup.vbs.in similarity index 69% rename from admin/win/msi/RegistryCleanup.vbs rename to admin/win/msi/RegistryCleanup.vbs.in index 50e26c8ca..35c3b7651 100644 --- a/admin/win/msi/RegistryCleanup.vbs +++ b/admin/win/msi/RegistryCleanup.vbs.in @@ -1,6 +1,7 @@ On Error goto 0 Const HKEY_LOCAL_MACHINE = &H80000002 +Const HKEY_CURRENT_USER = &H80000001 Const strObjRegistry = "winmgmts:\\.\root\default:StdRegProv" @@ -49,6 +50,25 @@ Function RegistryCleanupSyncRootManager() End If End Function +Function RegistryCleanupCfApiShellExtensions() + Set objRegistry = GetObject(strObjRegistry) + + strShellExtThumbnailHandlerAppId = "Software\Classes\AppID\@CFAPI_SHELLEXT_APPID_REG@" + strShellExtThumbnailHandlerClsId = "Software\Classes\CLSID\@CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG@" + + rootKey = HKEY_CURRENT_USER + + If objRegistry.EnumKey(rootKey, strShellExtThumbnailHandlerAppId, arrSubKeys) = 0 Then + RegistryDeleteKeyRecursive rootKey, strShellExtThumbnailHandlerAppId + End If + + If objRegistry.EnumKey(rootKey, strShellExtThumbnailHandlerClsId, arrSubKeys) = 0 Then + RegistryDeleteKeyRecursive rootKey, strShellExtThumbnailHandlerClsId + End If + +End Function + Function RegistryCleanup() RegistryCleanupSyncRootManager() + RegistryCleanupCfApiShellExtensions() End Function diff --git a/config.h.in b/config.h.in index c62386cc6..d2afefca1 100644 --- a/config.h.in +++ b/config.h.in @@ -44,4 +44,13 @@ #cmakedefine BUILD_UPDATER "@BUILD_UPDATER@" +#cmakedefine CFAPI_SHELLEXT_APPID_REG "@CFAPI_SHELLEXT_APPID_REG@" +#cmakedefine CFAPI_SHELLEXT_APPID_DISPLAY_NAME "@CFAPI_SHELLEXT_APPID_DISPLAY_NAME@" + +#cmakedefine CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID "@CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID@" +#cmakedefine CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG "@CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG@" +#cmakedefine CFAPI_SHELLEXT_THUMBNAIL_HANDLER_DISPLAY_NAME "@CFAPI_SHELLEXT_THUMBNAIL_HANDLER_DISPLAY_NAME@" + +#cmakedefine CFAPI_SHELL_EXTENSIONS_LIB_NAME "@CFAPI_SHELL_EXTENSIONS_LIB_NAME@" + #endif diff --git a/src/common/filesystembase.h b/src/common/filesystembase.h index b15cb4fa1..bc0b592c2 100644 --- a/src/common/filesystembase.h +++ b/src/common/filesystembase.h @@ -25,7 +25,7 @@ #include #include -#include +#include class QFile; diff --git a/src/common/shellextensionutils.cpp b/src/common/shellextensionutils.cpp new file mode 100644 index 000000000..d6f4b244c --- /dev/null +++ b/src/common/shellextensionutils.cpp @@ -0,0 +1,36 @@ +#include "shellextensionutils.h" +#include +#include + +namespace VfsShellExtensions { + +Q_LOGGING_CATEGORY(lcShellExtensionUtils, "nextcloud.gui.shellextensionutils", QtInfoMsg) + +QString VfsShellExtensions::serverNameForApplicationName(const QString &applicationName) +{ + return applicationName + QStringLiteral(":VfsShellExtensionsServer"); +} + +QString VfsShellExtensions::serverNameForApplicationNameDefault() +{ + return serverNameForApplicationName(APPLICATION_NAME); +} +namespace Protocol { + QByteArray createJsonMessage(const QVariantMap &message) + { + QVariantMap messageCopy = message; + messageCopy[QStringLiteral("version")] = Version; + return QJsonDocument::fromVariant((messageCopy)).toJson(QJsonDocument::Compact); + } + + bool validateProtocolVersion(const QVariantMap &message) + { + const auto valid = message.value(QStringLiteral("version")) == Version; + if (!valid) { + qCWarning(lcShellExtensionUtils) << "Invalid shell extensions IPC protocol: " << message.value(QStringLiteral("version")) << " vs " << Version; + } + Q_ASSERT(valid); + return valid; + } +} +} diff --git a/src/common/shellextensionutils.h b/src/common/shellextensionutils.h new file mode 100644 index 000000000..ca0d9922d --- /dev/null +++ b/src/common/shellextensionutils.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) by Oleksandr Zolotov + * + * 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 "config.h" +#include +#include +#include + +namespace VfsShellExtensions { +QString serverNameForApplicationName(const QString &applicationName); +QString serverNameForApplicationNameDefault(); + +namespace Protocol { + static constexpr auto ThumbnailProviderRequestKey = "thumbnailProviderRequest"; + static constexpr auto ThumbnailProviderRequestFilePathKey = "filePath"; + static constexpr auto ThumbnailProviderRequestFileSizeKey = "fileSize"; + static constexpr auto ThumnailProviderDataKey = "thumbnailData"; + static constexpr auto Version = "1.0"; + + QByteArray createJsonMessage(const QVariantMap &message); + bool validateProtocolVersion(const QVariantMap &message); +} +} diff --git a/src/common/utility.h b/src/common/utility.h index 45df7f647..98dc3030a 100644 --- a/src/common/utility.h +++ b/src/common/utility.h @@ -21,7 +21,7 @@ #define UTILITY_H -#include "ocsynclib.h" +#include "csync/ocsynclib.h" #include #include #include @@ -254,6 +254,7 @@ namespace Utility { OCSYNC_EXPORT bool registryDeleteKeyTree(HKEY hRootKey, const QString &subKey); OCSYNC_EXPORT bool registryDeleteKeyValue(HKEY hRootKey, const QString &subKey, const QString &valueName); OCSYNC_EXPORT bool registryWalkSubKeys(HKEY hRootKey, const QString &subKey, const std::function &callback); + OCSYNC_EXPORT bool registryWalkValues(HKEY hRootKey, const QString &subKey, const std::function &callback); OCSYNC_EXPORT QRect getTaskbarDimensions(); // Possibly refactor to share code with UnixTimevalToFileTime in c_time.c diff --git a/src/common/utility_win.cpp b/src/common/utility_win.cpp index 80907ea5d..ed322669b 100644 --- a/src/common/utility_win.cpp +++ b/src/common/utility_win.cpp @@ -28,8 +28,11 @@ #include #include #include - +#include +#include +#include #include +#include extern Q_CORE_EXPORT int qt_ntfs_permission_lookup; @@ -354,6 +357,50 @@ bool Utility::registryWalkSubKeys(HKEY hRootKey, const QString &subKey, const st return retCode != ERROR_NO_MORE_ITEMS; } +bool Utility::registryWalkValues(HKEY hRootKey, const QString &subKey, const std::function &callback) +{ + HKEY hKey; + REGSAM sam = KEY_QUERY_VALUE; + LONG result = RegOpenKeyEx(hRootKey, reinterpret_cast(subKey.utf16()), 0, sam, &hKey); + ASSERT(result == ERROR_SUCCESS); + if (result != ERROR_SUCCESS) { + return false; + } + + DWORD maxValueNameSize = 0; + result = RegQueryInfoKey(hKey, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, &maxValueNameSize, nullptr, nullptr, nullptr); + ASSERT(result == ERROR_SUCCESS); + if (result != ERROR_SUCCESS) { + RegCloseKey(hKey); + return false; + } + + QString valueName; + valueName.reserve(maxValueNameSize + 1); + + DWORD retCode = ERROR_SUCCESS; + bool done = false; + for (DWORD i = 0; retCode == ERROR_SUCCESS; ++i) { + Q_ASSERT(unsigned(valueName.capacity()) > maxValueNameSize); + valueName.resize(valueName.capacity()); + DWORD valueNameSize = valueName.size(); + retCode = RegEnumValue(hKey, i, reinterpret_cast(valueName.data()), &valueNameSize, nullptr, nullptr, nullptr, nullptr); + + ASSERT(result == ERROR_SUCCESS || retCode == ERROR_NO_MORE_ITEMS); + if (retCode == ERROR_SUCCESS) { + valueName.resize(valueNameSize); + callback(valueName, &done); + + if (done) { + break; + } + } + } + + RegCloseKey(hKey); + return retCode != ERROR_NO_MORE_ITEMS; +} + DWORD Utility::convertSizeToDWORD(size_t &convertVar) { if( convertVar > UINT_MAX ) { diff --git a/src/common/vfs.h b/src/common/vfs.h index 19160e341..572c297a4 100644 --- a/src/common/vfs.h +++ b/src/common/vfs.h @@ -49,6 +49,9 @@ struct OCSYNC_EXPORT VfsSetupParams // Folder alias QString alias; + // Folder registry navigation Pane CLSID + QString navigationPaneClsid; + /** The path to the synced folder on the account * * Always ends with /. diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index e2a817aa5..5a1157813 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -291,7 +291,7 @@ IF( NOT WIN32 AND NOT APPLE ) set(client_SRCS ${client_SRCS} folderwatcher_linux.cpp) ENDIF() IF( WIN32 ) -set(client_SRCS ${client_SRCS} folderwatcher_win.cpp) +set(client_SRCS ${client_SRCS} folderwatcher_win.cpp shellextensionsserver.cpp ${CMAKE_SOURCE_DIR}/src/common/shellextensionutils.cpp) ENDIF() IF( APPLE ) list(APPEND client_SRCS folderwatcher_mac.cpp) diff --git a/src/gui/application.cpp b/src/gui/application.cpp index d0cbb500f..bc0ceb6c2 100644 --- a/src/gui/application.cpp +++ b/src/gui/application.cpp @@ -35,6 +35,7 @@ #include "accountmanager.h" #include "creds/abstractcredentials.h" #include "pushnotifications.h" +#include "shellextensionsserver.h" #if defined(BUILD_UPDATER) #include "updater/ocupdater.h" @@ -319,6 +320,9 @@ Application::Application(int &argc, char **argv) qCInfo(lcApplication) << "VFS suffix plugin is available"; _folderManager.reset(new FolderMan); +#ifdef Q_OS_WIN + _shellExtensionsServer.reset(new ShellExtensionsServer); +#endif connect(this, &SharedTools::QtSingleApplication::messageReceived, this, &Application::slotParseMessage); diff --git a/src/gui/application.h b/src/gui/application.h index 3f1436e65..048b795a6 100644 --- a/src/gui/application.h +++ b/src/gui/application.h @@ -46,6 +46,7 @@ Q_DECLARE_LOGGING_CATEGORY(lcApplication) class Theme; class Folder; +class ShellExtensionsServer; class SslErrorDialog; /** @@ -144,6 +145,9 @@ private: QScopedPointer _crashHandler; #endif QScopedPointer _folderManager; +#ifdef Q_OS_WIN + QScopedPointer _shellExtensionsServer; +#endif }; } // namespace OCC diff --git a/src/gui/folder.cpp b/src/gui/folder.cpp index 0ad6e8bad..757ec9736 100644 --- a/src/gui/folder.cpp +++ b/src/gui/folder.cpp @@ -498,6 +498,7 @@ void Folder::startVfs() vfsParams.filesystemPath = path(); vfsParams.displayName = shortGuiRemotePathOrAppName(); vfsParams.alias = alias(); + vfsParams.navigationPaneClsid = navigationPaneClsid().toString(); vfsParams.remotePath = remotePathTrailingSlash(); vfsParams.account = _accountState->account(); vfsParams.journal = &_journal; diff --git a/src/gui/folderman.h b/src/gui/folderman.h index 5a6c234e7..14575da46 100644 --- a/src/gui/folderman.h +++ b/src/gui/folderman.h @@ -26,6 +26,7 @@ #include "syncfileitem.h" class TestFolderMan; +class TestCfApiShellExtensionsIPC; namespace OCC { @@ -362,6 +363,7 @@ private: explicit FolderMan(QObject *parent = nullptr); friend class OCC::Application; friend class ::TestFolderMan; + friend class ::TestCfApiShellExtensionsIPC; }; } // namespace OCC diff --git a/src/gui/shellextensionsserver.cpp b/src/gui/shellextensionsserver.cpp new file mode 100644 index 000000000..fa97a1b62 --- /dev/null +++ b/src/gui/shellextensionsserver.cpp @@ -0,0 +1,155 @@ +/* + * Copyright (C) by Oleksandr Zolotov + * + * 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 "shellextensionsserver.h" +#include "account.h" +#include "accountstate.h" +#include "common/shellextensionutils.h" +#include "folder.h" +#include "folderman.h" +#include +#include +#include + +namespace OCC { + +ShellExtensionsServer::ShellExtensionsServer(QObject *parent) + : QObject(parent) +{ + _localServer.listen(VfsShellExtensions::serverNameForApplicationNameDefault()); + connect(&_localServer, &QLocalServer::newConnection, this, &ShellExtensionsServer::slotNewConnection); +} + +ShellExtensionsServer::~ShellExtensionsServer() +{ + if (!_localServer.isListening()) { + return; + } + _localServer.close(); +} + +void ShellExtensionsServer::sendJsonMessageWithVersion(QLocalSocket *socket, const QVariantMap &message) +{ + socket->write(VfsShellExtensions::Protocol::createJsonMessage(message)); + socket->waitForBytesWritten(); +} + +void ShellExtensionsServer::sendEmptyDataAndCloseSession(QLocalSocket *socket) +{ + sendJsonMessageWithVersion(socket, QVariantMap{}); + closeSession(socket); +} + +void ShellExtensionsServer::closeSession(QLocalSocket *socket) +{ + connect(socket, &QLocalSocket::disconnected, this, [socket] { + socket->close(); + socket->deleteLater(); + }); + socket->disconnectFromServer(); +} + +void ShellExtensionsServer::processThumbnailRequest(QLocalSocket *socket, const ThumbnailRequestInfo &thumbnailRequestInfo) +{ + if (!thumbnailRequestInfo.isValid()) { + sendEmptyDataAndCloseSession(socket); + return; + } + + const auto folder = FolderMan::instance()->folder(thumbnailRequestInfo.folderAlias); + + if (!folder) { + sendEmptyDataAndCloseSession(socket); + return; + } + + const auto fileInfo = QFileInfo(thumbnailRequestInfo.path); + const auto filePathRelative = QFileInfo(thumbnailRequestInfo.path).canonicalFilePath().remove(folder->path()); + + SyncJournalFileRecord record; + if (!folder->journalDb()->getFileRecord(filePathRelative, &record) || !record.isValid()) { + sendEmptyDataAndCloseSession(socket); + return; + } + + QUrlQuery queryItems; + queryItems.addQueryItem(QStringLiteral("fileId"), record._fileId); + queryItems.addQueryItem(QStringLiteral("x"), QString::number(thumbnailRequestInfo.size.width())); + queryItems.addQueryItem(QStringLiteral("y"), QString::number(thumbnailRequestInfo.size.height())); + const QUrl jobUrl = Utility::concatUrlPath(folder->accountState()->account()->url(), QStringLiteral("/index.php/core/preview"), queryItems); + const auto job = new SimpleNetworkJob(folder->accountState()->account()); + job->startRequest(QByteArrayLiteral("GET"), jobUrl); + connect(job, &SimpleNetworkJob::finishedSignal, this, [socket, this](QNetworkReply *reply) { + const auto contentType = reply->header(QNetworkRequest::ContentTypeHeader).toByteArray(); + if (!contentType.startsWith(QByteArrayLiteral("image/"))) { + sendEmptyDataAndCloseSession(socket); + return; + } + + auto messageReplyWithThumbnail = QVariantMap { + {VfsShellExtensions::Protocol::ThumnailProviderDataKey, reply->readAll().toBase64()} + }; + sendJsonMessageWithVersion(socket, messageReplyWithThumbnail); + closeSession(socket); + }); +} + +void ShellExtensionsServer::slotNewConnection() +{ + const auto socket = _localServer.nextPendingConnection(); + + if (!socket) { + return; + } + + socket->waitForReadyRead(); + const auto message = QJsonDocument::fromJson(socket->readAll()).toVariant().toMap(); + + if (!VfsShellExtensions::Protocol::validateProtocolVersion(message)) { + sendEmptyDataAndCloseSession(socket); + return; + } + + const auto thumbnailRequestMessage = message.value(VfsShellExtensions::Protocol::ThumbnailProviderRequestKey).toMap(); + const auto thumbnailFilePath = QDir::fromNativeSeparators(thumbnailRequestMessage.value(VfsShellExtensions::Protocol::ThumbnailProviderRequestFilePathKey).toString()); + const auto thumbnailFileSize = thumbnailRequestMessage.value(VfsShellExtensions::Protocol::ThumbnailProviderRequestFileSizeKey).toMap(); + + if (thumbnailFilePath.isEmpty() || thumbnailFileSize.isEmpty()) { + sendEmptyDataAndCloseSession(socket); + return; + } + + QString foundFolderAlias; + for (const auto folder : FolderMan::instance()->map()) { + if (thumbnailFilePath.startsWith(folder->path())) { + foundFolderAlias = folder->alias(); + break; + } + } + + if (foundFolderAlias.isEmpty()) { + sendEmptyDataAndCloseSession(socket); + return; + } + + const auto thumbnailRequestInfo = ThumbnailRequestInfo { + thumbnailFilePath, + QSize(thumbnailFileSize.value("width").toInt(), thumbnailFileSize.value("height").toInt()), + foundFolderAlias + }; + + processThumbnailRequest(socket, thumbnailRequestInfo); +} + +} // namespace OCC diff --git a/src/gui/shellextensionsserver.h b/src/gui/shellextensionsserver.h new file mode 100644 index 000000000..0699219ba --- /dev/null +++ b/src/gui/shellextensionsserver.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) by Oleksandr Zolotov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#pragma once + +#include +#include +#include + +class QLocalSocket; + +namespace OCC { +class ShellExtensionsServer : public QObject +{ + struct ThumbnailRequestInfo + { + QString path; + QSize size; + QString folderAlias; + + bool isValid() const { return !path.isEmpty() && !size.isEmpty() && !folderAlias.isEmpty(); } + }; + + Q_OBJECT +public: + ShellExtensionsServer(QObject *parent = nullptr); + ~ShellExtensionsServer() override; + +private: + void sendJsonMessageWithVersion(QLocalSocket *socket, const QVariantMap &message); + void sendEmptyDataAndCloseSession(QLocalSocket *socket); + void closeSession(QLocalSocket *socket); + void processThumbnailRequest(QLocalSocket *socket, const ThumbnailRequestInfo &thumbnailRequestInfo); + +private slots: + void slotNewConnection(); + +private: + QLocalServer _localServer; +}; +} // namespace OCC diff --git a/src/libsync/vfs/cfapi/CMakeLists.txt b/src/libsync/vfs/cfapi/CMakeLists.txt index f882087d8..f6b2084dc 100644 --- a/src/libsync/vfs/cfapi/CMakeLists.txt +++ b/src/libsync/vfs/cfapi/CMakeLists.txt @@ -9,6 +9,8 @@ if (WIN32) vfs_cfapi.h vfs_cfapi.cpp ) + + add_subdirectory(shellext) target_link_libraries(nextcloudsync_vfs_cfapi PRIVATE Nextcloud::sync diff --git a/src/libsync/vfs/cfapi/cfapiwrapper.cpp b/src/libsync/vfs/cfapi/cfapiwrapper.cpp index 089a84964..9f0fb85fd 100644 --- a/src/libsync/vfs/cfapi/cfapiwrapper.cpp +++ b/src/libsync/vfs/cfapi/cfapiwrapper.cpp @@ -33,6 +33,8 @@ #include #include +#include "config.h" + Q_LOGGING_CATEGORY(lcCfApiWrapper, "nextcloud.sync.vfs.cfapi.wrapper", QtInfoMsg) #define FIELD_SIZE( type, field ) ( sizeof( ( (type*)0 )->field ) ) @@ -44,6 +46,8 @@ namespace { constexpr auto syncRootFlagsFull = 34; constexpr auto syncRootFlagsNoCfApiContextMenu = 2; +constexpr auto syncRootManagerRegKey = R"(SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\SyncRootManager)"; + void cfApiSendTransferInfo(const CF_CONNECTION_KEY &connectionKey, const CF_TRANSFER_KEY &transferKey, NTSTATUS status, void *buffer, qint64 offset, qint64 currentBlockLength, qint64 totalLength) { @@ -407,7 +411,7 @@ QString retrieveWindowsSid() return {}; } -bool createSyncRootRegistryKeys(const QString &providerName, const QString &folderAlias, const QString &displayName, const QString &accountDisplayName, const QString &syncRootPath) +bool createSyncRootRegistryKeys(const QString &providerName, const QString &folderAlias, const QString &navigationPaneClsid, const QString &displayName, const QString &accountDisplayName, const QString &syncRootPath) { // We must set specific Registry keys to make the progress bar refresh correctly and also add status icons into Windows Explorer // More about this here: https://docs.microsoft.com/en-us/windows/win32/shell/integrate-cloud-storage @@ -422,7 +426,7 @@ bool createSyncRootRegistryKeys(const QString &providerName, const QString &fold // folder registry keys go like: Nextcloud!S-1-5-21-2096452760-2617351404-2281157308-1001!user@nextcloud.lan:8080!0, Nextcloud!S-1-5-21-2096452760-2617351404-2281157308-1001!user@nextcloud.lan:8080!1, etc. for each sync folder const auto syncRootId = QString("%1!%2!%3!%4").arg(providerName).arg(windowsSid).arg(accountDisplayName).arg(folderAlias); - const QString providerSyncRootIdRegistryKey = QStringLiteral(R"(SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\SyncRootManager\)") + syncRootId; + const QString providerSyncRootIdRegistryKey = syncRootManagerRegKey + QStringLiteral("\\") + syncRootId; const QString providerSyncRootIdUserSyncRootsRegistryKey = providerSyncRootIdRegistryKey + QStringLiteral(R"(\UserSyncRoots\)"); struct RegistryKeyInfo { @@ -438,7 +442,9 @@ bool createSyncRootRegistryKeys(const QString &providerName, const QString &fold { providerSyncRootIdRegistryKey, QStringLiteral("Flags"), REG_DWORD, flags }, { providerSyncRootIdRegistryKey, QStringLiteral("DisplayNameResource"), REG_EXPAND_SZ, displayName }, { providerSyncRootIdRegistryKey, QStringLiteral("IconResource"), REG_EXPAND_SZ, QString(QDir::toNativeSeparators(qApp->applicationFilePath()) + QStringLiteral(",0")) }, - { providerSyncRootIdUserSyncRootsRegistryKey, windowsSid, REG_SZ, syncRootPath } + { providerSyncRootIdUserSyncRootsRegistryKey, windowsSid, REG_SZ, syncRootPath}, + { providerSyncRootIdRegistryKey, QStringLiteral("ThumbnailProvider"), REG_SZ, CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG}, + { providerSyncRootIdRegistryKey, QStringLiteral("NamespaceCLSID"), REG_SZ, QString(navigationPaneClsid)} }; for (const auto ®istryKeyToSet : qAsConst(registryKeysToSet)) { @@ -457,9 +463,7 @@ bool createSyncRootRegistryKeys(const QString &providerName, const QString &fold bool deleteSyncRootRegistryKey(const QString &syncRootPath, const QString &providerName, const QString &accountDisplayName) { - const auto syncRootManagerRegistryKey = QStringLiteral(R"(SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\SyncRootManager\)"); - - if (OCC::Utility::registryKeyExists(HKEY_LOCAL_MACHINE, syncRootManagerRegistryKey)) { + if (OCC::Utility::registryKeyExists(HKEY_LOCAL_MACHINE, syncRootManagerRegKey)) { const auto windowsSid = retrieveWindowsSid(); Q_ASSERT(!windowsSid.isEmpty()); if (windowsSid.isEmpty()) { @@ -472,13 +476,13 @@ bool deleteSyncRootRegistryKey(const QString &syncRootPath, const QString &provi bool result = true; // walk through each registered syncRootId - OCC::Utility::registryWalkSubKeys(HKEY_LOCAL_MACHINE, syncRootManagerRegistryKey, [&](HKEY, const QString &syncRootId) { + OCC::Utility::registryWalkSubKeys(HKEY_LOCAL_MACHINE, syncRootManagerRegKey, [&](HKEY, const QString &syncRootId) { // make sure we have matching syncRootId(providerName!windowsSid!accountDisplayName) if (syncRootId.startsWith(currentUserSyncRootIdPattern)) { - const QString syncRootIdUserSyncRootsRegistryKey = syncRootManagerRegistryKey + syncRootId + QStringLiteral(R"(\UserSyncRoots\)"); + const QString syncRootIdUserSyncRootsRegistryKey = syncRootManagerRegKey + QStringLiteral("\\") + syncRootId + QStringLiteral(R"(\UserSyncRoots\)"); // check if there is a 'windowsSid' Registry value under \UserSyncRoots and it matches the sync folder path we are removing if (OCC::Utility::registryGetKeyValue(HKEY_LOCAL_MACHINE, syncRootIdUserSyncRootsRegistryKey, windowsSid).toString() == syncRootPath) { - const QString syncRootIdToDelete = syncRootManagerRegistryKey + syncRootId; + const QString syncRootIdToDelete = syncRootManagerRegKey + QStringLiteral("\\") + syncRootId; result = OCC::Utility::registryDeleteKeyTree(HKEY_LOCAL_MACHINE, syncRootIdToDelete); } } @@ -488,10 +492,10 @@ bool deleteSyncRootRegistryKey(const QString &syncRootPath, const QString &provi return true; } -OCC::Result OCC::CfApiWrapper::registerSyncRoot(const QString &path, const QString &providerName, const QString &providerVersion, const QString &folderAlias, const QString &displayName, const QString &accountDisplayName) +OCC::Result OCC::CfApiWrapper::registerSyncRoot(const QString &path, const QString &providerName, const QString &providerVersion, const QString &folderAlias, const QString &navigationPaneClsid, const QString &displayName, const QString &accountDisplayName) { // even if we fail to register our sync root with shell, we can still proceed with using the VFS - const auto createRegistryKeyResult = createSyncRootRegistryKeys(providerName, folderAlias, displayName, accountDisplayName, path); + const auto createRegistryKeyResult = createSyncRootRegistryKeys(providerName, folderAlias, navigationPaneClsid, displayName, accountDisplayName, path); Q_ASSERT(createRegistryKeyResult); if (!createRegistryKeyResult) { @@ -532,6 +536,24 @@ OCC::Result OCC::CfApiWrapper::registerSyncRoot(const QString &pa } } +void unregisterSyncRootShellExtensions(const QString &providerName, const QString &folderAlias, const QString &accountDisplayName) +{ + const auto windowsSid = retrieveWindowsSid(); + Q_ASSERT(!windowsSid.isEmpty()); + if (windowsSid.isEmpty()) { + qCWarning(lcCfApiWrapper) << "Failed to unregister SyncRoot Shell Extensions!"; + return; + } + + const auto syncRootId = QString("%1!%2!%3!%4").arg(providerName).arg(windowsSid).arg(accountDisplayName).arg(folderAlias); + + const QString providerSyncRootIdRegistryKey = syncRootManagerRegKey + QStringLiteral("\\") + syncRootId; + + OCC::Utility::registryDeleteKeyValue(HKEY_LOCAL_MACHINE, providerSyncRootIdRegistryKey, QStringLiteral("ThumbnailProvider")); + + qCInfo(lcCfApiWrapper) << "Successfully unregistered SyncRoot Shell Extensions!"; +} + OCC::Result OCC::CfApiWrapper::unregisterSyncRoot(const QString &path, const QString &providerName, const QString &accountDisplayName) { const auto deleteRegistryKeyResult = deleteSyncRootRegistryKey(path, providerName, accountDisplayName); @@ -579,6 +601,31 @@ OCC::Result OCC::CfApiWrapper::disconnectSyncRoot(ConnectionKey & } } +bool OCC::CfApiWrapper::isAnySyncRoot(const QString &providerName, const QString &accountDisplayName) +{ + const auto windowsSid = retrieveWindowsSid(); + Q_ASSERT(!windowsSid.isEmpty()); + if (windowsSid.isEmpty()) { + qCWarning(lcCfApiWrapper) << "Could not retrieve Windows Sid."; + return false; + } + + const auto syncRootPrefix = QString("%1!%2!%3!").arg(providerName).arg(windowsSid).arg(accountDisplayName); + + if (Utility::registryKeyExists(HKEY_LOCAL_MACHINE, syncRootManagerRegKey)) { + bool foundSyncRoots = false; + Utility::registryWalkSubKeys(HKEY_LOCAL_MACHINE, syncRootManagerRegKey, + [&foundSyncRoots, &syncRootPrefix](HKEY key, const QString &subKey) { + if (subKey.startsWith(syncRootPrefix)) { + foundSyncRoots = true; + } + }); + return foundSyncRoots; + } + + return false; +} + bool OCC::CfApiWrapper::isSparseFile(const QString &path) { const auto p = path.toStdWString(); diff --git a/src/libsync/vfs/cfapi/cfapiwrapper.h b/src/libsync/vfs/cfapi/cfapiwrapper.h index 5993a7f46..2e9bbee27 100644 --- a/src/libsync/vfs/cfapi/cfapiwrapper.h +++ b/src/libsync/vfs/cfapi/cfapiwrapper.h @@ -72,11 +72,13 @@ private: std::unique_ptr _data; }; -NEXTCLOUD_CFAPI_EXPORT Result registerSyncRoot(const QString &path, const QString &providerName, const QString &providerVersion, const QString &folderAlias, const QString &displayName, const QString &accountDisplayName); +NEXTCLOUD_CFAPI_EXPORT Result registerSyncRoot(const QString &path, const QString &providerName, const QString &providerVersion, const QString &folderAlias, const QString &navigationPaneClsid, const QString &displayName, const QString &accountDisplayName); +NEXTCLOUD_CFAPI_EXPORT void unregisterSyncRootShellExtensions(const QString &providerName, const QString &folderAlias, const QString &accountDisplayName); NEXTCLOUD_CFAPI_EXPORT Result unregisterSyncRoot(const QString &path, const QString &providerName, const QString &accountDisplayName); NEXTCLOUD_CFAPI_EXPORT Result connectSyncRoot(const QString &path, VfsCfApi *context); NEXTCLOUD_CFAPI_EXPORT Result disconnectSyncRoot(ConnectionKey &&key); +NEXTCLOUD_CFAPI_EXPORT bool isAnySyncRoot(const QString &providerName, const QString &accountDisplayName); NEXTCLOUD_CFAPI_EXPORT bool isSparseFile(const QString &path); diff --git a/src/libsync/vfs/cfapi/shellext/CMakeLists.txt b/src/libsync/vfs/cfapi/shellext/CMakeLists.txt new file mode 100644 index 000000000..966ae60e0 --- /dev/null +++ b/src/libsync/vfs/cfapi/shellext/CMakeLists.txt @@ -0,0 +1,31 @@ +add_library(CfApiShellExtensions MODULE + dllmain.cpp + cfapishellintegrationclassfactory.cpp + thumbnailprovider.cpp + thumbnailprovideripc.cpp + ${CMAKE_SOURCE_DIR}/src/common/shellextensionutils.cpp + CfApiShellIntegration.def +) + +target_link_libraries(CfApiShellExtensions shlwapi Gdiplus Nextcloud::csync Qt5::Core Qt5::Network) + +target_include_directories(CfApiShellExtensions PRIVATE ${GeneratedFilesPath}) + +target_include_directories(CfApiShellExtensions PRIVATE ${CMAKE_SOURCE_DIR}) + +set_target_properties(CfApiShellExtensions + PROPERTIES + LIBRARY_OUTPUT_NAME + ${CFAPI_SHELL_EXTENSIONS_LIB_NAME} + RUNTIME_OUTPUT_NAME + ${CFAPI_SHELL_EXTENSIONS_LIB_NAME} + LIBRARY_OUTPUT_DIRECTORY + ${BIN_OUTPUT_DIRECTORY} + RUNTIME_OUTPUT_DIRECTORY + ${BIN_OUTPUT_DIRECTORY} +) + +install(TARGETS CfApiShellExtensions + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_BINDIR} +) diff --git a/src/libsync/vfs/cfapi/shellext/CfApiShellIntegration.def b/src/libsync/vfs/cfapi/shellext/CfApiShellIntegration.def new file mode 100644 index 000000000..4ef029088 --- /dev/null +++ b/src/libsync/vfs/cfapi/shellext/CfApiShellIntegration.def @@ -0,0 +1,3 @@ +EXPORTS + DllGetClassObject PRIVATE + DllCanUnloadNow PRIVATE diff --git a/src/libsync/vfs/cfapi/shellext/cfapishellintegrationclassfactory.cpp b/src/libsync/vfs/cfapi/shellext/cfapishellintegrationclassfactory.cpp new file mode 100644 index 000000000..9522a087f --- /dev/null +++ b/src/libsync/vfs/cfapi/shellext/cfapishellintegrationclassfactory.cpp @@ -0,0 +1,97 @@ +/* + * Copyright (C) by Oleksandr Zolotov + * + * 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 "cfapishellintegrationclassfactory.h" +#include + +extern long dllReferenceCount; + +namespace VfsShellExtensions { + +HRESULT CfApiShellIntegrationClassFactory::CreateInstance( + REFCLSID clsid, const ClassObjectInit *classObjectInits, size_t classObjectInitsCount, REFIID riid, void **ppv) +{ + for (size_t i = 0; i < classObjectInitsCount; ++i) { + if (clsid == *classObjectInits[i].clsid) { + IClassFactory *classFactory = + new (std::nothrow) CfApiShellIntegrationClassFactory(classObjectInits[i].pfnCreate); + if (!classFactory) { + return E_OUTOFMEMORY; + } + const auto hresult = classFactory->QueryInterface(riid, ppv); + classFactory->Release(); + return hresult; + } + } + return CLASS_E_CLASSNOTAVAILABLE; +} + +// IUnknown +IFACEMETHODIMP CfApiShellIntegrationClassFactory::QueryInterface(REFIID riid, void **ppv) +{ + *ppv = nullptr; + + if (IsEqualIID(IID_IUnknown, riid) || IsEqualIID(IID_IClassFactory, riid)) { + *ppv = static_cast(this); + AddRef(); + return S_OK; + } else { + return E_NOINTERFACE; + } +} + +IFACEMETHODIMP_(ULONG) CfApiShellIntegrationClassFactory::AddRef() +{ + return InterlockedIncrement(&_referenceCount); +} + +IFACEMETHODIMP_(ULONG) CfApiShellIntegrationClassFactory::Release() +{ + const auto refCount = InterlockedDecrement(&_referenceCount); + if (refCount == 0) { + delete this; + } + return refCount; +} + +IFACEMETHODIMP CfApiShellIntegrationClassFactory::CreateInstance(IUnknown *punkOuter, REFIID riid, void **ppv) +{ + if (punkOuter) { + return CLASS_E_NOAGGREGATION; + } + return _pfnCreate(riid, ppv); +} + +IFACEMETHODIMP CfApiShellIntegrationClassFactory::LockServer(BOOL fLock) +{ + if (fLock) { + InterlockedIncrement(&dllReferenceCount); + } else { + InterlockedDecrement(&dllReferenceCount); + } + return S_OK; +} + +CfApiShellIntegrationClassFactory::CfApiShellIntegrationClassFactory(PFNCREATEINSTANCE pfnCreate) + : _referenceCount(1) + , _pfnCreate(pfnCreate) +{ + InterlockedIncrement(&dllReferenceCount); +} + +CfApiShellIntegrationClassFactory::~CfApiShellIntegrationClassFactory() +{ + InterlockedDecrement(&dllReferenceCount); +} +} diff --git a/src/libsync/vfs/cfapi/shellext/cfapishellintegrationclassfactory.h b/src/libsync/vfs/cfapi/shellext/cfapishellintegrationclassfactory.h new file mode 100644 index 000000000..fe0459ada --- /dev/null +++ b/src/libsync/vfs/cfapi/shellext/cfapishellintegrationclassfactory.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) by Oleksandr Zolotov + * + * 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 + +namespace VfsShellExtensions { + +using PFNCREATEINSTANCE = HRESULT (*)(REFIID riid, void **ppvObject); +struct ClassObjectInit +{ + const CLSID *clsid; + PFNCREATEINSTANCE pfnCreate; +}; + +class CfApiShellIntegrationClassFactory : public IClassFactory +{ +public: + CfApiShellIntegrationClassFactory(PFNCREATEINSTANCE pfnCreate); + + IFACEMETHODIMP_(ULONG) AddRef(); + IFACEMETHODIMP CreateInstance(IUnknown *pUnkOuter, REFIID riid, void **ppv); + + static HRESULT CreateInstance( + REFCLSID clsid, const ClassObjectInit *classObjectInits, size_t classObjectInitsCount, REFIID riid, void **ppv); + + IFACEMETHODIMP LockServer(BOOL fLock); + IFACEMETHODIMP QueryInterface(REFIID riid, void **ppv); + IFACEMETHODIMP_(ULONG) Release(); + +protected: + ~CfApiShellIntegrationClassFactory(); + +private: + long _referenceCount; + + PFNCREATEINSTANCE _pfnCreate; +}; +} diff --git a/src/libsync/vfs/cfapi/shellext/dllmain.cpp b/src/libsync/vfs/cfapi/shellext/dllmain.cpp new file mode 100644 index 000000000..c9edff1dd --- /dev/null +++ b/src/libsync/vfs/cfapi/shellext/dllmain.cpp @@ -0,0 +1,58 @@ +/* + * Copyright (C) by Oleksandr Zolotov + * + * 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 "cfapishellintegrationclassfactory.h" +#include "thumbnailprovider.h" +#include + +long dllReferenceCount = 0; + +HINSTANCE instanceHandle = NULL; + +HRESULT ThumbnailProvider_CreateInstance(REFIID riid, void **ppv); + +const VfsShellExtensions::ClassObjectInit listClassesSupported[] = { + {&__uuidof(VfsShellExtensions::ThumbnailProvider), ThumbnailProvider_CreateInstance} +}; + +STDAPI_(BOOL) DllMain(HINSTANCE hInstance, DWORD dwReason, void *) +{ + if (dwReason == DLL_PROCESS_ATTACH) { + instanceHandle = hInstance; + DisableThreadLibraryCalls(hInstance); + } + + return TRUE; +} + +STDAPI DllCanUnloadNow() +{ + return dllReferenceCount == 0 ? S_OK : S_FALSE; +} + +STDAPI DllGetClassObject(REFCLSID clsid, REFIID riid, void **ppv) +{ + return VfsShellExtensions::CfApiShellIntegrationClassFactory::CreateInstance(clsid, listClassesSupported, ARRAYSIZE(listClassesSupported), riid, ppv); +} + +HRESULT ThumbnailProvider_CreateInstance(REFIID riid, void **ppv) +{ + auto *thumbnailProvider = new (std::nothrow) VfsShellExtensions::ThumbnailProvider(); + if (!thumbnailProvider) { + return E_OUTOFMEMORY; + } + const auto hresult = thumbnailProvider->QueryInterface(riid, ppv); + thumbnailProvider->Release(); + return hresult; +} diff --git a/src/libsync/vfs/cfapi/shellext/thumbnailprovider.cpp b/src/libsync/vfs/cfapi/shellext/thumbnailprovider.cpp new file mode 100644 index 000000000..18c0ba905 --- /dev/null +++ b/src/libsync/vfs/cfapi/shellext/thumbnailprovider.cpp @@ -0,0 +1,160 @@ +/* + * Copyright (C) by Oleksandr Zolotov + * + * 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. + */ + +// global compilation flag configuring windows sdk headers +// preventing inclusion of min and max macros clashing with +#define NOMINMAX 1 + +// override byte to prevent clashes with +#define byte win_byte_override + +#include // gdi plus requires Windows.h +// ...includes for other windows header that may use byte... + +// Define min max macros required by GDI+ headers. +#ifndef max +#define max(a, b) (((a) > (b)) ? (a) : (b)) +#else +#error max macro is already defined +#endif +#ifndef min +#define min(a, b) (((a) < (b)) ? (a) : (b)) +#else +#error min macro is already defined +#endif + +#include + +// Undefine min max macros so they won't collide with header content. +#undef min +#undef max + +// Undefine byte macros so it won't collide with header content. +#undef byte + +#include "thumbnailprovider.h" +#include +#include +#include + +namespace VfsShellExtensions { + +std::pair hBitmapAndAlphaTypeFromData(const QByteArray &thumbnailData) +{ + if (thumbnailData.isEmpty()) { + return {NULL, WTSAT_UNKNOWN}; + } + + Gdiplus::Bitmap *gdiPlusBitmap = nullptr; + ULONG_PTR gdiPlusToken; + Gdiplus::GdiplusStartupInput gdiPlusStartupInput; + if (Gdiplus::GdiplusStartup(&gdiPlusToken, &gdiPlusStartupInput, nullptr) != Gdiplus::Status::Ok) { + return {NULL, WTSAT_UNKNOWN}; + } + + const auto handleFailure = [gdiPlusToken]() -> std::pair { + Gdiplus::GdiplusShutdown(gdiPlusToken); + return {NULL, WTSAT_UNKNOWN}; + }; + + const std::vector bitmapData(thumbnailData.begin(), thumbnailData.end()); + auto const stream{::SHCreateMemStream(&bitmapData[0], static_cast(bitmapData.size()))}; + + if (!stream) { + return handleFailure(); + } + gdiPlusBitmap = Gdiplus::Bitmap::FromStream(stream); + + auto hasAlpha = false; + HBITMAP hBitmap = NULL; + if (gdiPlusBitmap) { + hasAlpha = Gdiplus::IsAlphaPixelFormat(gdiPlusBitmap->GetPixelFormat()); + if (gdiPlusBitmap->GetHBITMAP(Gdiplus::Color(0, 0, 0), &hBitmap) != Gdiplus::Status::Ok) { + return handleFailure(); + } + } + + Gdiplus::GdiplusShutdown(gdiPlusToken); + + return {hBitmap, hasAlpha ? WTSAT_ARGB : WTSAT_RGB}; +} + +ThumbnailProvider::ThumbnailProvider() + : _referenceCount(1) +{ +} + +IFACEMETHODIMP ThumbnailProvider::QueryInterface(REFIID riid, void **ppv) +{ + static const QITAB qit[] = { + QITABENT(ThumbnailProvider, IInitializeWithItem), + QITABENT(ThumbnailProvider, IThumbnailProvider), + {0}, + }; + return QISearch(this, qit, riid, ppv); +} + +IFACEMETHODIMP_(ULONG) ThumbnailProvider::AddRef() +{ + return InterlockedIncrement(&_referenceCount); +} + +IFACEMETHODIMP_(ULONG) ThumbnailProvider::Release() +{ + const auto refCount = InterlockedDecrement(&_referenceCount); + if (refCount == 0) { + delete this; + } + return refCount; +} + +IFACEMETHODIMP ThumbnailProvider::Initialize(_In_ IShellItem *item, _In_ DWORD mode) +{ + HRESULT hresult = item->QueryInterface(__uuidof(_shellItem), reinterpret_cast(&_shellItem)); + if (FAILED(hresult)) { + return hresult; + } + + LPWSTR pszName = NULL; + hresult = _shellItem->GetDisplayName(SIGDN_FILESYSPATH, &pszName); + if (FAILED(hresult)) { + return hresult; + } + + _shellItemPath = QString::fromWCharArray(pszName); + + return S_OK; +} + +IFACEMETHODIMP ThumbnailProvider::GetThumbnail(_In_ UINT cx, _Out_ HBITMAP *bitmap, _Out_ WTS_ALPHATYPE *alphaType) +{ + *bitmap = nullptr; + *alphaType = WTSAT_UNKNOWN; + + const auto thumbnailDataReceived = _thumbnailProviderIpc.fetchThumbnailForFile(_shellItemPath, QSize(cx, cx)); + + if (thumbnailDataReceived.isEmpty()) { + return E_FAIL; + } + + const auto bitmapAndAlphaType = hBitmapAndAlphaTypeFromData(thumbnailDataReceived); + if (!bitmapAndAlphaType.first) { + return E_FAIL; + } + *bitmap = bitmapAndAlphaType.first; + *alphaType = bitmapAndAlphaType.second; + + return S_OK; +} +} diff --git a/src/libsync/vfs/cfapi/shellext/thumbnailprovider.h b/src/libsync/vfs/cfapi/shellext/thumbnailprovider.h new file mode 100644 index 000000000..66256c3a0 --- /dev/null +++ b/src/libsync/vfs/cfapi/shellext/thumbnailprovider.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) by Oleksandr Zolotov + * + * 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 "thumbnailprovideripc.h" +#include +#include +#include "config.h" +#include + +namespace VfsShellExtensions { +std::pair hBitmapAndAlphaTypeFromData(const QByteArray &thumbnailData); + +_COM_SMARTPTR_TYPEDEF(IShellItem2, IID_IShellItem2); + +class __declspec(uuid(CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID)) ThumbnailProvider : public IInitializeWithItem, + public IThumbnailProvider +{ +public: + ThumbnailProvider(); + + virtual ~ThumbnailProvider() = default; + + IFACEMETHODIMP QueryInterface(REFIID riid, void **ppv); + + IFACEMETHODIMP_(ULONG) AddRef(); + + IFACEMETHODIMP_(ULONG) Release(); + + IFACEMETHODIMP Initialize(_In_ IShellItem *item, _In_ DWORD mode); + + IFACEMETHODIMP GetThumbnail(_In_ UINT cx, _Out_ HBITMAP *bitmap, _Out_ WTS_ALPHATYPE *alphaType); + +private: + long _referenceCount; + + IShellItem2Ptr _shellItem; + QString _shellItemPath; + ThumbnailProviderIpc _thumbnailProviderIpc; +}; +} diff --git a/src/libsync/vfs/cfapi/shellext/thumbnailprovideripc.cpp b/src/libsync/vfs/cfapi/shellext/thumbnailprovideripc.cpp new file mode 100644 index 000000000..8a6d057b5 --- /dev/null +++ b/src/libsync/vfs/cfapi/shellext/thumbnailprovideripc.cpp @@ -0,0 +1,134 @@ +/* + * Copyright (C) by Oleksandr Zolotov + * + * 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 "thumbnailprovideripc.h" +#include "common/shellextensionutils.h" +#include "common/utility.h" +#include +#include +#include +#include +#include +#include +#include +namespace { +// we don't want to block the Explorer for too long (default is 30K, so we'd keep it at 10K, except QLocalSocket::waitForDisconnected()) +constexpr auto socketTimeoutMs = 10000; +} + +namespace VfsShellExtensions { + +ThumbnailProviderIpc::ThumbnailProviderIpc() +{ + _localSocket.reset(new QLocalSocket()); +} +ThumbnailProviderIpc::~ThumbnailProviderIpc() +{ + disconnectSocketFromServer(); +} + +QByteArray ThumbnailProviderIpc::fetchThumbnailForFile(const QString &filePath, const QSize &size) +{ + QByteArray result; + const auto sendMessageAndReadyRead = [this](QVariantMap &message) { + _localSocket->write(VfsShellExtensions::Protocol::createJsonMessage(message)); + return _localSocket->waitForBytesWritten(socketTimeoutMs) && _localSocket->waitForReadyRead(socketTimeoutMs); + }; + + const auto mainServerName = getServerNameForPath(filePath); + + if (mainServerName.isEmpty()) { + return result; + } + + // #1 Connect to the local server + if (!connectSocketToServer(mainServerName)) { + return result; + } + + auto messageRequestThumbnailForFile = QVariantMap { + { + VfsShellExtensions::Protocol::ThumbnailProviderRequestKey, + QVariantMap { + {VfsShellExtensions::Protocol::ThumbnailProviderRequestFilePathKey, filePath}, + {VfsShellExtensions::Protocol::ThumbnailProviderRequestFileSizeKey, QVariantMap{{QStringLiteral("width"), size.width()}, {QStringLiteral("height"), size.height()}}} + } + } + }; + + // #2 Request a thumbnail of a 'size' for a 'filePath' + if (!sendMessageAndReadyRead(messageRequestThumbnailForFile)) { + return result; + } + + // #3 Read the thumbnail data (read all as the thumbnail size is usually less than 1MB) + const auto message = QJsonDocument::fromJson(_localSocket->readAll()).toVariant().toMap(); + if (!VfsShellExtensions::Protocol::validateProtocolVersion(message)) { + return result; + } + result = QByteArray::fromBase64(message.value(VfsShellExtensions::Protocol::ThumnailProviderDataKey).toByteArray()); + disconnectSocketFromServer(); + + return result; +} + +bool ThumbnailProviderIpc::disconnectSocketFromServer() +{ + const auto isConnectedOrConnecting = _localSocket->state() == QLocalSocket::ConnectedState || _localSocket->state() == QLocalSocket::ConnectingState; + if (isConnectedOrConnecting) { + _localSocket->disconnectFromServer(); + const auto isNotConnected = _localSocket->state() == QLocalSocket::UnconnectedState || _localSocket->state() == QLocalSocket::ClosingState; + return isNotConnected || _localSocket->waitForDisconnected(); + } + return true; +} + +QString ThumbnailProviderIpc::getServerNameForPath(const QString &filePath) +{ + if (!overrideServerName.isEmpty()) { + return overrideServerName; + } + // SyncRootManager Registry key contains all registered folders for Cf API. It will give us the correct name of the current app based on the folder path + QString serverName; + constexpr auto syncRootManagerRegKey = R"(SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\SyncRootManager)"; + + if (OCC::Utility::registryKeyExists(HKEY_LOCAL_MACHINE, syncRootManagerRegKey)) { + OCC::Utility::registryWalkSubKeys(HKEY_LOCAL_MACHINE, syncRootManagerRegKey, [&](HKEY, const QString &syncRootId) { + const QString syncRootIdUserSyncRootsRegistryKey = syncRootManagerRegKey + QStringLiteral("\\") + syncRootId + QStringLiteral(R"(\UserSyncRoots\)"); + OCC::Utility::registryWalkValues(HKEY_LOCAL_MACHINE, syncRootIdUserSyncRootsRegistryKey, [&](const QString &userSyncRootName, bool *done) { + const auto userSyncRootValue = QDir::fromNativeSeparators(OCC::Utility::registryGetKeyValue(HKEY_LOCAL_MACHINE, syncRootIdUserSyncRootsRegistryKey, userSyncRootName).toString()); + if (QDir::fromNativeSeparators(filePath).startsWith(userSyncRootValue)) { + const auto syncRootIdSplit = syncRootId.split(QLatin1Char('!'), Qt::SkipEmptyParts); + if (!syncRootIdSplit.isEmpty()) { + serverName = VfsShellExtensions::serverNameForApplicationName(syncRootIdSplit.first()); + *done = true; + } + } + }); + }); + } + return serverName; +} + +bool ThumbnailProviderIpc::connectSocketToServer(const QString &serverName) +{ + if (!disconnectSocketFromServer()) { + return false; + } + _localSocket->setServerName(serverName); + _localSocket->connectToServer(); + return _localSocket->state() == QLocalSocket::ConnectedState || _localSocket->waitForConnected(socketTimeoutMs); +} +QString ThumbnailProviderIpc::overrideServerName = {}; +} diff --git a/src/libsync/vfs/cfapi/shellext/thumbnailprovideripc.h b/src/libsync/vfs/cfapi/shellext/thumbnailprovideripc.h new file mode 100644 index 000000000..e55cc81e9 --- /dev/null +++ b/src/libsync/vfs/cfapi/shellext/thumbnailprovideripc.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) by Oleksandr Zolotov + * + * 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 + +class QString; +class QSize; +class QLocalSocket; + +#include +#include + +namespace VfsShellExtensions { +class ThumbnailProviderIpc +{ +public: + ThumbnailProviderIpc(); + ~ThumbnailProviderIpc(); + + QByteArray fetchThumbnailForFile(const QString &filePath, const QSize &size); + +private: + bool connectSocketToServer(const QString &serverName); + bool disconnectSocketFromServer(); + + static QString getServerNameForPath(const QString &filePath); + +public: + // for unit tests (as Registry does not work on a CI VM) + static QString overrideServerName; + +private: + QScopedPointer _localSocket; +}; +} diff --git a/src/libsync/vfs/cfapi/vfs_cfapi.cpp b/src/libsync/vfs/cfapi/vfs_cfapi.cpp index aec9289ac..db0d3653c 100644 --- a/src/libsync/vfs/cfapi/vfs_cfapi.cpp +++ b/src/libsync/vfs/cfapi/vfs_cfapi.cpp @@ -22,14 +22,73 @@ #include "syncfileitem.h" #include "filesystem.h" #include "common/syncjournaldb.h" +#include "config.h" #include #include +#include + Q_LOGGING_CATEGORY(lcCfApi, "nextcloud.sync.vfs.cfapi", QtInfoMsg) namespace cfapi { using namespace OCC::CfApiWrapper; + +constexpr auto appIdRegKey = R"(Software\Classes\AppID\)"; +constexpr auto clsIdRegKey = R"(Software\Classes\CLSID\)"; +const auto rootKey = HKEY_CURRENT_USER; + +bool registerShellExtension() +{ + // assume CFAPI_SHELL_EXTENSIONS_LIB_NAME is always in the same folder as the main executable + const auto shellExtensionDllPath = QDir::toNativeSeparators(QString(QCoreApplication::applicationDirPath() + QStringLiteral("/") + CFAPI_SHELL_EXTENSIONS_LIB_NAME + QStringLiteral(".dll"))); + if (!QFileInfo::exists(shellExtensionDllPath)) { + Q_ASSERT(false); + qCWarning(lcCfApi) << "Register CfAPI shell extensions failed. Dll does not exist in " + << QCoreApplication::applicationDirPath(); + return false; + } + + const QString appIdPath = QString() % appIdRegKey % CFAPI_SHELLEXT_APPID_REG; + if (!OCC::Utility::registrySetKeyValue(rootKey, appIdPath, {}, REG_SZ, CFAPI_SHELLEXT_APPID_DISPLAY_NAME)) { + return false; + } + if (!OCC::Utility::registrySetKeyValue(rootKey, appIdPath, QStringLiteral("DllSurrogate"), REG_SZ, {})) { + return false; + } + + const QString clsidPath = QString() % clsIdRegKey % CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG; + const QString clsidServerPath = clsidPath % R"(\InprocServer32)"; + + if (!OCC::Utility::registrySetKeyValue(rootKey, clsidPath, QStringLiteral("AppID"), REG_SZ, CFAPI_SHELLEXT_APPID_REG)) { + return false; + } + if (!OCC::Utility::registrySetKeyValue(rootKey, clsidPath, {}, REG_SZ, CFAPI_SHELLEXT_THUMBNAIL_HANDLER_DISPLAY_NAME)) { + return false; + } + if (!OCC::Utility::registrySetKeyValue(rootKey, clsidServerPath, {}, REG_SZ, shellExtensionDllPath)) { + return false; + } + if (!OCC::Utility::registrySetKeyValue(rootKey, clsidServerPath, QStringLiteral("ThreadingModel"), REG_SZ, QStringLiteral("Apartment"))) { + return false; + } + + return true; +} + +void unregisterShellExtensions() +{ + const QString appIdPath = QString() % appIdRegKey % CFAPI_SHELLEXT_APPID_REG; + if (OCC::Utility::registryKeyExists(rootKey, appIdPath)) { + OCC::Utility::registryDeleteKeyTree(rootKey, appIdPath); + } + + const QString clsidPath = QString() % clsIdRegKey % CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG; + if (OCC::Utility::registryKeyExists(rootKey, clsidPath)) { + OCC::Utility::registryDeleteKeyTree(rootKey, clsidPath); + } +} + } namespace OCC { @@ -61,9 +120,10 @@ QString VfsCfApi::fileSuffix() const void VfsCfApi::startImpl(const VfsSetupParams ¶ms) { + cfapi::registerShellExtension(); const auto localPath = QDir::toNativeSeparators(params.filesystemPath); - const auto registerResult = cfapi::registerSyncRoot(localPath, params.providerName, params.providerVersion, params.alias, params.displayName, params.account->displayName()); + const auto registerResult = cfapi::registerSyncRoot(localPath, params.providerName, params.providerVersion, params.alias, params.navigationPaneClsid, params.displayName, params.account->displayName()); if (!registerResult) { qCCritical(lcCfApi) << "Initialization failed, couldn't register sync root:" << registerResult.error(); return; @@ -93,6 +153,10 @@ void VfsCfApi::unregisterFolder() if (!result) { qCCritical(lcCfApi) << "Unregistration failed for" << localPath << ":" << result.error(); } + + if (!cfapi::isAnySyncRoot(params().providerName, params().account->displayName())) { + cfapi::unregisterShellExtensions(); + } } bool VfsCfApi::socketApiPinStateActionsShown() const diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a6e6304c9..d7ff56515 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -75,6 +75,8 @@ if (WIN32) ) nextcloud_add_test(SyncCfApi) + nextcloud_add_test(CfApiShellExtensionsIPC) + target_sources(CfApiShellExtensionsIPCTest PRIVATE "${CMAKE_SOURCE_DIR}/src/libsync/vfs/cfapi/shellext/thumbnailprovideripc.cpp") elseif(LINUX) # elseif(LINUX OR APPLE) nextcloud_add_test(SyncXAttr) endif() diff --git a/test/syncenginetestutils.cpp b/test/syncenginetestutils.cpp index 340cf82ba..f01772cb2 100644 --- a/test/syncenginetestutils.cpp +++ b/test/syncenginetestutils.cpp @@ -841,6 +841,9 @@ void FakePayloadReply::respond() { setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200); setHeader(QNetworkRequest::ContentLengthHeader, _body.size()); + for (auto it = _additionalHeaders.constKeyValueBegin(); it != _additionalHeaders.constKeyValueEnd(); ++it) { + setHeader(it->first, it->second); + } emit metaDataChanged(); emit readyRead(); setFinished(true); diff --git a/test/syncenginetestutils.h b/test/syncenginetestutils.h index 27311f2b0..e44d5d9cb 100644 --- a/test/syncenginetestutils.h +++ b/test/syncenginetestutils.h @@ -352,6 +352,8 @@ public: qint64 bytesAvailable() const override; QByteArray _body; + QMap _additionalHeaders; + static const int defaultDelay = 10; }; diff --git a/test/testcfapishellextensionsipc.cpp b/test/testcfapishellextensionsipc.cpp new file mode 100644 index 000000000..6f1e5ba68 --- /dev/null +++ b/test/testcfapishellextensionsipc.cpp @@ -0,0 +1,216 @@ +/* + * This software is in the public domain, furnished "as is", without technical + * support, and with no warranty, express or implied, as to its usefulness for + * any purpose. + * + */ + +#include +#include +#include +#include "syncenginetestutils.h" +#include "common/vfs.h" +#include "common/shellextensionutils.h" +#include "config.h" +#include + +#include "folderman.h" +#include "account.h" +#include "accountstate.h" +#include "accountmanager.h" +#include "testhelper.h" +#include "vfs/cfapi/shellext/thumbnailprovideripc.h" +#include "shellextensionsserver.h" + +using namespace OCC; + +class TestCfApiShellExtensionsIPC : public QObject +{ + Q_OBJECT + + FolderMan _fm; + + FakeFolder fakeFolder{FileInfo()}; + + QScopedPointer fakeQnam; + OCC::AccountPtr account; + OCC::AccountState* accountState; + + QScopedPointer _shellExtensionsServer; + + QStringList dummmyImageNames = { + "A/photos/imageJpg.jpg", + "A/photos/imagePng.png", + "A/photos/imagePng.bmp", + }; + QMap dummyImages; + + QString currentImage; + +private slots: + void initTestCase() + { + VfsShellExtensions::ThumbnailProviderIpc::overrideServerName = VfsShellExtensions::serverNameForApplicationNameDefault(); + + _shellExtensionsServer.reset(new ShellExtensionsServer); + + for (const auto &dummyImageName : dummmyImageNames) { + const auto extension = dummyImageName.split(".").last(); + const auto format = dummyImageName.endsWith("PNG", Qt::CaseInsensitive) ? QImage::Format_ARGB32 : QImage::Format_RGB32; + QImage image(QSize(640, 480), format); + QPainter painter(&image); + painter.setBrush(QBrush(Qt::red)); + painter.fillRect(QRectF(0, 0, 640, 480), Qt::red); + QByteArray byteArray; + QBuffer buffer(&byteArray); + buffer.open(QIODevice::WriteOnly); + image.save(&buffer, extension.toStdString().c_str()); + dummyImages.insert(dummyImageName, byteArray); + } + + fakeQnam.reset(new FakeQNAM({})); + account = OCC::Account::create(); + account->setCredentials(new FakeCredentials{fakeQnam.data()}); + account->setUrl(QUrl(("http://example.de"))); + + accountState = new OCC::AccountState(account); + + OCC::AccountManager::instance()->addAccount(account); + + FolderMan *folderman = FolderMan::instance(); + QCOMPARE(folderman, &_fm); + QVERIFY(folderman->addFolder(accountState, folderDefinition(fakeFolder.localPath()))); + + fakeQnam->setOverride( + [this](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) { + Q_UNUSED(device); + QNetworkReply *reply = nullptr; + + const auto urlQuery = QUrlQuery(req.url()); + const auto fileId = urlQuery.queryItemValue(QStringLiteral("fileId")); + const auto x = urlQuery.queryItemValue(QStringLiteral("x")).toInt(); + const auto y = urlQuery.queryItemValue(QStringLiteral("y")).toInt(); + const auto path = req.url().path(); + + if (fileId.isEmpty() || x <= 0 || y <= 0) { + reply = new FakePayloadReply(op, req, {}, nullptr); + } else { + const auto foundImageIt = dummyImages.find(currentImage); + + QByteArray byteArray; + if (foundImageIt != dummyImages.end()) { + byteArray = foundImageIt.value(); + } + + currentImage.clear(); + + auto fakePayloadReply = new FakePayloadReply(op, req, byteArray, nullptr); + + QMap additionalHeaders = { + {QNetworkRequest::KnownHeaders::ContentTypeHeader, "image/jpeg"}}; + fakePayloadReply->_additionalHeaders = additionalHeaders; + + reply = fakePayloadReply; + } + + return reply; + }); + }; + + void testRequestThumbnails() + { + FolderMan *folderman = FolderMan::instance(); + QVERIFY(folderman); + auto folder = FolderMan::instance()->folderForPath(fakeFolder.localPath()); + QVERIFY(folder); + + folder->setVirtualFilesEnabled(true); + + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + ItemCompletedSpy completeSpy(fakeFolder); + + auto cleanup = [&]() { + completeSpy.clear(); + }; + cleanup(); + + // Create a virtual file for remote files + fakeFolder.remoteModifier().mkdir("A"); + fakeFolder.remoteModifier().mkdir("A/photos"); + for (const auto &dummyImageName : dummmyImageNames) { + fakeFolder.remoteModifier().insert(dummyImageName, 256); + } + QVERIFY(fakeFolder.syncOnce()); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + + cleanup(); + // just add records from fake folder's journal to real one's to make test work + SyncJournalFileRecord record; + auto realFolder = FolderMan::instance()->folderForPath(fakeFolder.localPath()); + QVERIFY(realFolder); + for (const auto &dummyImageName : dummmyImageNames) { + if (fakeFolder.syncJournal().getFileRecord(dummyImageName, &record)) { + realFolder->journalDb()->setFileRecord(record); + } + } + + // #1 Test every fake image fetching. Everything must succeed. + for (const auto &dummyImageName : dummmyImageNames) { + QEventLoop loop; + QByteArray thumbnailReplyData; + currentImage = dummyImageName; + // emulate thumbnail request from a separate thread (just like the real shell extension does) + std::thread t([&] { + VfsShellExtensions::ThumbnailProviderIpc thumbnailProviderIpc; + thumbnailReplyData = thumbnailProviderIpc.fetchThumbnailForFile( + fakeFolder.localPath() + dummyImageName, QSize(256, 256)); + QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection); + }); + loop.exec(); + t.detach(); + QVERIFY(!thumbnailReplyData.isEmpty()); + const auto imageFromData = QImage::fromData(thumbnailReplyData); + QVERIFY(!imageFromData.isNull()); + } + + // #2 Test wrong image fetching. It must fail. + QEventLoop loop; + QByteArray thumbnailReplyData; + std::thread t1([&] { + VfsShellExtensions::ThumbnailProviderIpc thumbnailProviderIpc; + thumbnailReplyData = thumbnailProviderIpc.fetchThumbnailForFile( + fakeFolder.localPath() + QString("A/photos/wrong.jpg"), QSize(256, 256)); + QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection); + }); + loop.exec(); + t1.detach(); + QVERIFY(thumbnailReplyData.isEmpty()); + + // #3 Test one image fetching, but set incorrect size. It must fail. + currentImage = dummyImages.keys().first(); + std::thread t2([&] { + VfsShellExtensions::ThumbnailProviderIpc thumbnailProviderIpc; + thumbnailReplyData = thumbnailProviderIpc.fetchThumbnailForFile(fakeFolder.localPath() + currentImage, {}); + QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection); + }); + loop.exec(); + t2.detach(); + QVERIFY(thumbnailReplyData.isEmpty()); + } + + void cleanupTestCase() + { + VfsShellExtensions::ThumbnailProviderIpc::overrideServerName.clear(); + + if (auto folder = FolderMan::instance()->folderForPath(fakeFolder.localPath())) { + folder->setVirtualFilesEnabled(false); + } + FolderMan::instance()->unloadAndDeleteAllFolders(); + if (auto accountToDelete = OCC::AccountManager::instance()->accounts().first()) { + OCC::AccountManager::instance()->deleteAccount(accountToDelete.data()); + } + } +}; + +QTEST_GUILESS_MAIN(TestCfApiShellExtensionsIPC) +#include "testcfapishellextensionsipc.moc"