Quellcode durchsuchen

Implement COM Dll for CfApi shell extensins. Implement Thumbnail Provider.

Signed-off-by: alex-z <blackslayer4@gmail.com>
alex-z vor 3 Jahren
Ursprung
Commit
001deace2d
34 geänderte Dateien mit 1367 neuen und 18 gelöschten Zeilen
  1. 10 0
      NEXTCLOUD.cmake
  2. 2 1
      admin/win/msi/CMakeLists.txt
  3. 20 0
      admin/win/msi/RegistryCleanup.vbs.in
  4. 9 0
      config.h.in
  5. 1 1
      src/common/filesystembase.h
  6. 36 0
      src/common/shellextensionutils.cpp
  7. 35 0
      src/common/shellextensionutils.h
  8. 2 1
      src/common/utility.h
  9. 48 1
      src/common/utility_win.cpp
  10. 3 0
      src/common/vfs.h
  11. 1 1
      src/gui/CMakeLists.txt
  12. 4 0
      src/gui/application.cpp
  13. 4 0
      src/gui/application.h
  14. 1 0
      src/gui/folder.cpp
  15. 2 0
      src/gui/folderman.h
  16. 155 0
      src/gui/shellextensionsserver.cpp
  17. 52 0
      src/gui/shellextensionsserver.h
  18. 2 0
      src/libsync/vfs/cfapi/CMakeLists.txt
  19. 58 11
      src/libsync/vfs/cfapi/cfapiwrapper.cpp
  20. 3 1
      src/libsync/vfs/cfapi/cfapiwrapper.h
  21. 31 0
      src/libsync/vfs/cfapi/shellext/CMakeLists.txt
  22. 3 0
      src/libsync/vfs/cfapi/shellext/CfApiShellIntegration.def
  23. 97 0
      src/libsync/vfs/cfapi/shellext/cfapishellintegrationclassfactory.cpp
  24. 50 0
      src/libsync/vfs/cfapi/shellext/cfapishellintegrationclassfactory.h
  25. 58 0
      src/libsync/vfs/cfapi/shellext/dllmain.cpp
  26. 160 0
      src/libsync/vfs/cfapi/shellext/thumbnailprovider.cpp
  27. 52 0
      src/libsync/vfs/cfapi/shellext/thumbnailprovider.h
  28. 134 0
      src/libsync/vfs/cfapi/shellext/thumbnailprovideripc.cpp
  29. 46 0
      src/libsync/vfs/cfapi/shellext/thumbnailprovideripc.h
  30. 65 1
      src/libsync/vfs/cfapi/vfs_cfapi.cpp
  31. 2 0
      test/CMakeLists.txt
  32. 3 0
      test/syncenginetestutils.cpp
  33. 2 0
      test/syncenginetestutils.h
  34. 216 0
      test/testcfapishellextensionsipc.cpp

+ 10 - 0
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 )

+ 2 - 1
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

+ 20 - 0
admin/win/msi/RegistryCleanup.vbs → 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

+ 9 - 0
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

+ 1 - 1
src/common/filesystembase.h

@@ -25,7 +25,7 @@
 #include <QFileInfo>
 #include <QLoggingCategory>
 
-#include <ocsynclib.h>
+#include <csync/ocsynclib.h>
 
 class QFile;
 

+ 36 - 0
src/common/shellextensionutils.cpp

@@ -0,0 +1,36 @@
+#include "shellextensionutils.h"
+#include <QJsonDocument>
+#include <QLoggingCategory>
+
+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;
+    }
+}
+}

+ 35 - 0
src/common/shellextensionutils.h

@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#pragma once
+#include "config.h"
+#include <QByteArray>
+#include <QString>
+#include <QVariantMap>
+
+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);
+}
+}

+ 2 - 1
src/common/utility.h

@@ -21,7 +21,7 @@
 #define UTILITY_H
 
 
-#include "ocsynclib.h"
+#include "csync/ocsynclib.h"
 #include <QString>
 #include <QByteArray>
 #include <QDateTime>
@@ -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<void(HKEY, const QString &)> &callback);
+    OCSYNC_EXPORT bool registryWalkValues(HKEY hRootKey, const QString &subKey, const std::function<void(const QString &, bool *)> &callback);
     OCSYNC_EXPORT QRect getTaskbarDimensions();
 
     // Possibly refactor to share code with UnixTimevalToFileTime in c_time.c

+ 48 - 1
src/common/utility_win.cpp

@@ -28,8 +28,11 @@
 #include <winbase.h>
 #include <windows.h>
 #include <winerror.h>
-
+#include <QCoreApplication>
+#include <QDir>
+#include <QFile>
 #include <QLibrary>
+#include <QSettings>
 
 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<void(const QString &, bool *)> &callback)
+{
+    HKEY hKey;
+    REGSAM sam = KEY_QUERY_VALUE;
+    LONG result = RegOpenKeyEx(hRootKey, reinterpret_cast<LPCWSTR>(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<LPWSTR>(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 ) {

+ 3 - 0
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 /.

+ 1 - 1
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)

+ 4 - 0
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);
 

+ 4 - 0
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<CrashReporter::Handler> _crashHandler;
 #endif
     QScopedPointer<FolderMan> _folderManager;
+#ifdef Q_OS_WIN
+    QScopedPointer<ShellExtensionsServer> _shellExtensionsServer;
+#endif
 };
 
 } // namespace OCC

+ 1 - 0
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;

+ 2 - 0
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

+ 155 - 0
src/gui/shellextensionsserver.cpp

@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#include "shellextensionsserver.h"
+#include "account.h"
+#include "accountstate.h"
+#include "common/shellextensionutils.h"
+#include "folder.h"
+#include "folderman.h"
+#include <QDir>
+#include <QJsonDocument>
+#include <QLocalSocket>
+
+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

+ 52 - 0
src/gui/shellextensionsserver.h

@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#pragma once
+
+#include <QObject>
+#include <QLocalServer>
+#include <QSize>
+
+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

+ 2 - 0
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

+ 58 - 11
src/libsync/vfs/cfapi/cfapiwrapper.cpp

@@ -33,6 +33,8 @@
 #include <comdef.h>
 #include <ntstatus.h>
 
+#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 &registryKeyToSet : 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<void, QString> OCC::CfApiWrapper::registerSyncRoot(const QString &path, const QString &providerName, const QString &providerVersion, const QString &folderAlias, const QString &displayName, const QString &accountDisplayName)
+OCC::Result<void, QString> 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<void, QString> 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<void, QString> 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<void, QString> 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();

+ 3 - 1
src/libsync/vfs/cfapi/cfapiwrapper.h

@@ -72,11 +72,13 @@ private:
     std::unique_ptr<CF_PLACEHOLDER_BASIC_INFO, Deleter> _data;
 };
 
-NEXTCLOUD_CFAPI_EXPORT Result<void, QString> registerSyncRoot(const QString &path, const QString &providerName, const QString &providerVersion, const QString &folderAlias, const QString &displayName, const QString &accountDisplayName);
+NEXTCLOUD_CFAPI_EXPORT Result<void, QString> 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<void, QString> unregisterSyncRoot(const QString &path, const QString &providerName, const QString &accountDisplayName);
 
 NEXTCLOUD_CFAPI_EXPORT Result<ConnectionKey, QString> connectSyncRoot(const QString &path, VfsCfApi *context);
 NEXTCLOUD_CFAPI_EXPORT Result<void, QString> disconnectSyncRoot(ConnectionKey &&key);
+NEXTCLOUD_CFAPI_EXPORT bool isAnySyncRoot(const QString &providerName, const QString &accountDisplayName);
 
 NEXTCLOUD_CFAPI_EXPORT bool isSparseFile(const QString &path);
 

+ 31 - 0
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}
+)

+ 3 - 0
src/libsync/vfs/cfapi/shellext/CfApiShellIntegration.def

@@ -0,0 +1,3 @@
+EXPORTS
+    DllGetClassObject       PRIVATE
+    DllCanUnloadNow         PRIVATE

+ 97 - 0
src/libsync/vfs/cfapi/shellext/cfapishellintegrationclassfactory.cpp

@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#include "cfapishellintegrationclassfactory.h"
+#include <new>
+
+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<IUnknown *>(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);
+}
+}

+ 50 - 0
src/libsync/vfs/cfapi/shellext/cfapishellintegrationclassfactory.h

@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#pragma once
+#include <unknwn.h>
+
+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;
+};
+}

+ 58 - 0
src/libsync/vfs/cfapi/shellext/dllmain.cpp

@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#include "cfapishellintegrationclassfactory.h"
+#include "thumbnailprovider.h"
+#include <comdef.h>
+
+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;
+}

+ 160 - 0
src/libsync/vfs/cfapi/shellext/thumbnailprovider.cpp

@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+//  global compilation flag configuring windows sdk headers
+//  preventing inclusion of min and max macros clashing with <limits>
+#define NOMINMAX 1
+
+//  override byte to prevent clashes with <cstddef>
+#define byte win_byte_override
+
+#include <Windows.h> // 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 <gdiplus.h>
+
+//  Undefine min max macros so they won't collide with <limits> header content.
+#undef min
+#undef max
+
+//  Undefine byte macros so it won't collide with <cstddef> header content.
+#undef byte
+
+#include "thumbnailprovider.h"
+#include <vector>
+#include <shlwapi.h>
+#include <QSize>
+
+namespace VfsShellExtensions {
+
+std::pair<HBITMAP, WTS_ALPHATYPE> 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<HBITMAP, WTS_ALPHATYPE> {
+        Gdiplus::GdiplusShutdown(gdiPlusToken);
+        return {NULL, WTSAT_UNKNOWN};
+    };
+
+    const std::vector<unsigned char> bitmapData(thumbnailData.begin(), thumbnailData.end());
+    auto const stream{::SHCreateMemStream(&bitmapData[0], static_cast<UINT>(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<void **>(&_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;
+}
+}

+ 52 - 0
src/libsync/vfs/cfapi/shellext/thumbnailprovider.h

@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#pragma once
+#include "thumbnailprovideripc.h"
+#include <thumbcache.h>
+#include <comdef.h>
+#include "config.h"
+#include <QString>
+
+namespace VfsShellExtensions {
+std::pair<HBITMAP, WTS_ALPHATYPE> 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;
+};
+}

+ 134 - 0
src/libsync/vfs/cfapi/shellext/thumbnailprovideripc.cpp

@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#include "thumbnailprovideripc.h"
+#include "common/shellextensionutils.h"
+#include "common/utility.h"
+#include <QString>
+#include <QSize>
+#include <QtNetwork/QLocalSocket>
+#include <QJsonDocument>
+#include <QObject>
+#include <QDir>
+#include <Windows.h>
+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 = {};
+}

+ 46 - 0
src/libsync/vfs/cfapi/shellext/thumbnailprovideripc.h

@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#pragma once
+
+class QString;
+class QSize;
+class QLocalSocket;
+
+#include <QByteArray>
+#include <QScopedPointer>
+
+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<QLocalSocket> _localSocket;
+};
+}

+ 65 - 1
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 <cfapi.h>
 #include <comdef.h>
 
+#include <QCoreApplication>
+
 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 &params)
 {
+    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

+ 2 - 0
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()

+ 3 - 0
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);

+ 2 - 0
test/syncenginetestutils.h

@@ -352,6 +352,8 @@ public:
     qint64 bytesAvailable() const override;
     QByteArray _body;
 
+    QMap<QNetworkRequest::KnownHeaders, QByteArray> _additionalHeaders;
+
     static const int defaultDelay = 10;
 };
 

+ 216 - 0
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 <QtTest>
+#include <QImage>
+#include <QPainter>
+#include "syncenginetestutils.h"
+#include "common/vfs.h"
+#include "common/shellextensionutils.h"
+#include "config.h"
+#include <syncengine.h>
+
+#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> fakeQnam;
+    OCC::AccountPtr account;
+    OCC::AccountState* accountState;
+
+    QScopedPointer<ShellExtensionsServer> _shellExtensionsServer;
+
+    QStringList dummmyImageNames = {
+        "A/photos/imageJpg.jpg",
+        "A/photos/imagePng.png",
+        "A/photos/imagePng.bmp",
+    };
+    QMap<QString, QByteArray> 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<QNetworkRequest::KnownHeaders, QByteArray> 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"