# 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 )
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)
${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
+++ /dev/null
-On Error goto 0
-
-Const HKEY_LOCAL_MACHINE = &H80000002
-
-Const strObjRegistry = "winmgmts:\\.\root\default:StdRegProv"
-
-Function RegistryDeleteKeyRecursive(regRoot, strKeyPath)
- Set objRegistry = GetObject(strObjRegistry)
- objRegistry.EnumKey regRoot, strKeyPath, arrSubkeys
- If IsArray(arrSubkeys) Then
- For Each strSubkey In arrSubkeys
- RegistryDeleteKeyRecursive regRoot, strKeyPath & "\" & strSubkey
- Next
- End If
- objRegistry.DeleteKey regRoot, strKeyPath
-End Function
-
-Function RegistryListSubkeys(regRoot, strKeyPath)
- Set objRegistry = GetObject(strObjRegistry)
- objRegistry.EnumKey regRoot, strKeyPath, arrSubkeys
- RegistryListSubkeys = arrSubkeys
-End Function
-
-Function GetUserSID()
- Dim objWshNetwork, objUserAccount
-
- Set objWshNetwork = CreateObject("WScript.Network")
-
- Set objUserAccount = GetObject("winmgmts://" & objWshNetwork.UserDomain & "/root/cimv2").Get("Win32_UserAccount.Domain='" & objWshNetwork.ComputerName & "',Name='" & objWshNetwork.UserName & "'")
- GetUserSID = objUserAccount.SID
-End Function
-
-Function RegistryCleanupSyncRootManager()
- strSyncRootManagerKeyPath = "SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\SyncRootManager"
-
- arrSubKeys = RegistryListSubkeys(HKEY_LOCAL_MACHINE, strSyncRootManagerKeyPath)
-
- If IsArray(arrSubkeys) Then
- arrSubkeys=Filter(arrSubkeys, Session.Property("APPNAME"))
- End If
- If IsArray(arrSubkeys) Then
- arrSubkeys=Filter(arrSubkeys, GetUserSID())
- End If
-
- If IsArray(arrSubkeys) Then
- For Each strSubkey In arrSubkeys
- RegistryDeleteKeyRecursive HKEY_LOCAL_MACHINE, strSyncRootManagerKeyPath & "\" & strSubkey
- Next
- End If
-End Function
-
-Function RegistryCleanup()
- RegistryCleanupSyncRootManager()
-End Function
--- /dev/null
+On Error goto 0
+
+Const HKEY_LOCAL_MACHINE = &H80000002
+Const HKEY_CURRENT_USER = &H80000001
+
+Const strObjRegistry = "winmgmts:\\.\root\default:StdRegProv"
+
+Function RegistryDeleteKeyRecursive(regRoot, strKeyPath)
+ Set objRegistry = GetObject(strObjRegistry)
+ objRegistry.EnumKey regRoot, strKeyPath, arrSubkeys
+ If IsArray(arrSubkeys) Then
+ For Each strSubkey In arrSubkeys
+ RegistryDeleteKeyRecursive regRoot, strKeyPath & "\" & strSubkey
+ Next
+ End If
+ objRegistry.DeleteKey regRoot, strKeyPath
+End Function
+
+Function RegistryListSubkeys(regRoot, strKeyPath)
+ Set objRegistry = GetObject(strObjRegistry)
+ objRegistry.EnumKey regRoot, strKeyPath, arrSubkeys
+ RegistryListSubkeys = arrSubkeys
+End Function
+
+Function GetUserSID()
+ Dim objWshNetwork, objUserAccount
+
+ Set objWshNetwork = CreateObject("WScript.Network")
+
+ Set objUserAccount = GetObject("winmgmts://" & objWshNetwork.UserDomain & "/root/cimv2").Get("Win32_UserAccount.Domain='" & objWshNetwork.ComputerName & "',Name='" & objWshNetwork.UserName & "'")
+ GetUserSID = objUserAccount.SID
+End Function
+
+Function RegistryCleanupSyncRootManager()
+ strSyncRootManagerKeyPath = "SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\SyncRootManager"
+
+ arrSubKeys = RegistryListSubkeys(HKEY_LOCAL_MACHINE, strSyncRootManagerKeyPath)
+
+ If IsArray(arrSubkeys) Then
+ arrSubkeys=Filter(arrSubkeys, Session.Property("APPNAME"))
+ End If
+ If IsArray(arrSubkeys) Then
+ arrSubkeys=Filter(arrSubkeys, GetUserSID())
+ End If
+
+ If IsArray(arrSubkeys) Then
+ For Each strSubkey In arrSubkeys
+ RegistryDeleteKeyRecursive HKEY_LOCAL_MACHINE, strSyncRootManagerKeyPath & "\" & strSubkey
+ Next
+ 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
#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
#include <QFileInfo>
#include <QLoggingCategory>
-#include <ocsynclib.h>
+#include <csync/ocsynclib.h>
class QFile;
--- /dev/null
+#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;
+ }
+}
+}
--- /dev/null
+/*
+ * 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);
+}
+}
#define UTILITY_H
-#include "ocsynclib.h"
+#include "csync/ocsynclib.h"
#include <QString>
#include <QByteArray>
#include <QDateTime>
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
#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;
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 ) {
// Folder alias
QString alias;
+ // Folder registry navigation Pane CLSID
+ QString navigationPaneClsid;
+
/** The path to the synced folder on the account
*
* Always ends with /.
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)
#include "accountmanager.h"
#include "creds/abstractcredentials.h"
#include "pushnotifications.h"
+#include "shellextensionsserver.h"
#if defined(BUILD_UPDATER)
#include "updater/ocupdater.h"
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);
class Theme;
class Folder;
+class ShellExtensionsServer;
class SslErrorDialog;
/**
QScopedPointer<CrashReporter::Handler> _crashHandler;
#endif
QScopedPointer<FolderMan> _folderManager;
+#ifdef Q_OS_WIN
+ QScopedPointer<ShellExtensionsServer> _shellExtensionsServer;
+#endif
};
} // namespace OCC
vfsParams.filesystemPath = path();
vfsParams.displayName = shortGuiRemotePathOrAppName();
vfsParams.alias = alias();
+ vfsParams.navigationPaneClsid = navigationPaneClsid().toString();
vfsParams.remotePath = remotePathTrailingSlash();
vfsParams.account = _accountState->account();
vfsParams.journal = &_journal;
#include "syncfileitem.h"
class TestFolderMan;
+class TestCfApiShellExtensionsIPC;
namespace OCC {
explicit FolderMan(QObject *parent = nullptr);
friend class OCC::Application;
friend class ::TestFolderMan;
+ friend class ::TestCfApiShellExtensionsIPC;
};
} // namespace OCC
--- /dev/null
+/*
+ * 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
--- /dev/null
+/*
+ * 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
vfs_cfapi.h
vfs_cfapi.cpp
)
+
+ add_subdirectory(shellext)
target_link_libraries(nextcloudsync_vfs_cfapi PRIVATE
Nextcloud::sync
#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 ) )
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)
{
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
// 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 {
{ 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)) {
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()) {
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);
}
}
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) {
}
}
+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);
}
}
+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();
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);
--- /dev/null
+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}
+)
--- /dev/null
+EXPORTS
+ DllGetClassObject PRIVATE
+ DllCanUnloadNow PRIVATE
--- /dev/null
+/*
+ * 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);
+}
+}
--- /dev/null
+/*
+ * 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;
+};
+}
--- /dev/null
+/*
+ * 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;
+}
--- /dev/null
+/*
+ * 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;
+}
+}
--- /dev/null
+/*
+ * 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;
+};
+}
--- /dev/null
+/*
+ * 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 = {};
+}
--- /dev/null
+/*
+ * 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;
+};
+}
#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 {
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;
if (!result) {
qCCritical(lcCfApi) << "Unregistration failed for" << localPath << ":" << result.error();
}
+
+ if (!cfapi::isAnySyncRoot(params().providerName, params().account->displayName())) {
+ cfapi::unregisterShellExtensions();
+ }
}
bool VfsCfApi::socketApiPinStateActionsShown() const
)
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()
{
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);
qint64 bytesAvailable() const override;
QByteArray _body;
+ QMap<QNetworkRequest::KnownHeaders, QByteArray> _additionalHeaders;
+
static const int defaultDelay = 10;
};
--- /dev/null
+/*
+ * 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"