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

Signed-off-by: alex-z <blackslayer4@gmail.com>
This commit is contained in:
alex-z 2022-07-04 15:36:06 +03:00 committed by allexzander
parent d856e86e64
commit 001deace2d
34 changed files with 1367 additions and 18 deletions

View File

@ -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 )

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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 ) {

View File

@ -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 /.

View File

@ -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)

View File

@ -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);

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -9,6 +9,8 @@ if (WIN32)
vfs_cfapi.h
vfs_cfapi.cpp
)
add_subdirectory(shellext)
target_link_libraries(nextcloudsync_vfs_cfapi PRIVATE
Nextcloud::sync

View File

@ -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();

View File

@ -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);

View File

@ -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}
)

View File

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

View File

@ -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);
}
}

View File

@ -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;
};
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
};
}

View File

@ -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 = {};
}

View File

@ -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;
};
}

View File

@ -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

View File

@ -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()

View File

@ -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);

View File

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

View File

@ -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"