зеркало из https://github.com/nextcloud/desktop.git
Implement COM Dll for CfApi shell extensins. Implement Thumbnail Provider.
Signed-off-by: alex-z <blackslayer4@gmail.com>
This commit is contained in:
Родитель
d856e86e64
Коммит
001deace2d
|
@ -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 )
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
#include <QFileInfo>
|
||||
#include <QLoggingCategory>
|
||||
|
||||
#include <ocsynclib.h>
|
||||
#include <csync/ocsynclib.h>
|
||||
|
||||
class QFile;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 ) {
|
||||
|
|
|
@ -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 /.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -9,6 +9,8 @@ if (WIN32)
|
|||
vfs_cfapi.h
|
||||
vfs_cfapi.cpp
|
||||
)
|
||||
|
||||
add_subdirectory(shellext)
|
||||
|
||||
target_link_libraries(nextcloudsync_vfs_cfapi PRIVATE
|
||||
Nextcloud::sync
|
||||
|
|
|
@ -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 ®istryKeyToSet : qAsConst(registryKeysToSet)) {
|
||||
|
@ -457,9 +463,7 @@ bool createSyncRootRegistryKeys(const QString &providerName, const QString &fold
|
|||
|
||||
bool deleteSyncRootRegistryKey(const QString &syncRootPath, const QString &providerName, const QString &accountDisplayName)
|
||||
{
|
||||
const auto syncRootManagerRegistryKey = QStringLiteral(R"(SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\SyncRootManager\)");
|
||||
|
||||
if (OCC::Utility::registryKeyExists(HKEY_LOCAL_MACHINE, syncRootManagerRegistryKey)) {
|
||||
if (OCC::Utility::registryKeyExists(HKEY_LOCAL_MACHINE, syncRootManagerRegKey)) {
|
||||
const auto windowsSid = retrieveWindowsSid();
|
||||
Q_ASSERT(!windowsSid.isEmpty());
|
||||
if (windowsSid.isEmpty()) {
|
||||
|
@ -472,13 +476,13 @@ bool deleteSyncRootRegistryKey(const QString &syncRootPath, const QString &provi
|
|||
bool result = true;
|
||||
|
||||
// walk through each registered syncRootId
|
||||
OCC::Utility::registryWalkSubKeys(HKEY_LOCAL_MACHINE, syncRootManagerRegistryKey, [&](HKEY, const QString &syncRootId) {
|
||||
OCC::Utility::registryWalkSubKeys(HKEY_LOCAL_MACHINE, syncRootManagerRegKey, [&](HKEY, const QString &syncRootId) {
|
||||
// make sure we have matching syncRootId(providerName!windowsSid!accountDisplayName)
|
||||
if (syncRootId.startsWith(currentUserSyncRootIdPattern)) {
|
||||
const QString syncRootIdUserSyncRootsRegistryKey = syncRootManagerRegistryKey + syncRootId + QStringLiteral(R"(\UserSyncRoots\)");
|
||||
const QString syncRootIdUserSyncRootsRegistryKey = syncRootManagerRegKey + QStringLiteral("\\") + syncRootId + QStringLiteral(R"(\UserSyncRoots\)");
|
||||
// check if there is a 'windowsSid' Registry value under \UserSyncRoots and it matches the sync folder path we are removing
|
||||
if (OCC::Utility::registryGetKeyValue(HKEY_LOCAL_MACHINE, syncRootIdUserSyncRootsRegistryKey, windowsSid).toString() == syncRootPath) {
|
||||
const QString syncRootIdToDelete = syncRootManagerRegistryKey + syncRootId;
|
||||
const QString syncRootIdToDelete = syncRootManagerRegKey + QStringLiteral("\\") + syncRootId;
|
||||
result = OCC::Utility::registryDeleteKeyTree(HKEY_LOCAL_MACHINE, syncRootIdToDelete);
|
||||
}
|
||||
}
|
||||
|
@ -488,10 +492,10 @@ bool deleteSyncRootRegistryKey(const QString &syncRootPath, const QString &provi
|
|||
return true;
|
||||
}
|
||||
|
||||
OCC::Result<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();
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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}
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
EXPORTS
|
||||
DllGetClassObject PRIVATE
|
||||
DllCanUnloadNow PRIVATE
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
}
|
|
@ -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 = {};
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
}
|
|
@ -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 ¶ms)
|
||||
{
|
||||
cfapi::registerShellExtension();
|
||||
const auto localPath = QDir::toNativeSeparators(params.filesystemPath);
|
||||
|
||||
const auto registerResult = cfapi::registerSyncRoot(localPath, params.providerName, params.providerVersion, params.alias, params.displayName, params.account->displayName());
|
||||
const auto registerResult = cfapi::registerSyncRoot(localPath, params.providerName, params.providerVersion, params.alias, params.navigationPaneClsid, params.displayName, params.account->displayName());
|
||||
if (!registerResult) {
|
||||
qCCritical(lcCfApi) << "Initialization failed, couldn't register sync root:" << registerResult.error();
|
||||
return;
|
||||
|
@ -93,6 +153,10 @@ void VfsCfApi::unregisterFolder()
|
|||
if (!result) {
|
||||
qCCritical(lcCfApi) << "Unregistration failed for" << localPath << ":" << result.error();
|
||||
}
|
||||
|
||||
if (!cfapi::isAnySyncRoot(params().providerName, params().account->displayName())) {
|
||||
cfapi::unregisterShellExtensions();
|
||||
}
|
||||
}
|
||||
|
||||
bool VfsCfApi::socketApiPinStateActionsShown() const
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -352,6 +352,8 @@ public:
|
|||
qint64 bytesAvailable() const override;
|
||||
QByteArray _body;
|
||||
|
||||
QMap<QNetworkRequest::KnownHeaders, QByteArray> _additionalHeaders;
|
||||
|
||||
static const int defaultDelay = 10;
|
||||
};
|
||||
|
||||
|
|
|
@ -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"
|
Загрузка…
Ссылка в новой задаче