Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/desktop.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorallexzander <blackslayer4@gmail.com>2022-09-02 12:48:52 +0300
committerMatthieu Gallien <matthieu_gallien@yahoo.fr>2022-10-04 19:32:51 +0300
commit289a641d74bab0fbe1ad4e43c081432028a77446 (patch)
tree44b5272aa4b6c0cfa6faf459daf08472514e2fb2 /src
parentaf0764d5b28436fc8f6ad25fffb84e07954c3b63 (diff)
VFS Windows: Display the sharing state and lock state in the 'Status' column of Windows Explorer
Signed-off-by: allexzander <blackslayer4@gmail.com>
Diffstat (limited to 'src')
-rw-r--r--src/common/shellextensionutils.cpp1
-rw-r--r--src/common/shellextensionutils.h7
-rw-r--r--src/common/syncjournaldb.cpp26
-rw-r--r--src/common/syncjournalfilerecord.h2
-rw-r--r--src/gui/CMakeLists.txt61
-rw-r--r--src/gui/ocsjob.cpp21
-rw-r--r--src/gui/ocsjob.h7
-rw-r--r--src/gui/ocssharejob.cpp11
-rw-r--r--src/gui/ocssharejob.h4
-rw-r--r--src/gui/shellextensionsserver.cpp270
-rw-r--r--src/gui/shellextensionsserver.h27
-rw-r--r--src/gui/socketapi/socketapi.cpp3
-rw-r--r--src/libsync/bulkpropagatorjob.cpp2
-rw-r--r--src/libsync/discovery.cpp10
-rw-r--r--src/libsync/propagateremotemkdir.cpp2
-rw-r--r--src/libsync/syncfileitem.cpp4
-rw-r--r--src/libsync/syncfileitem.h3
-rw-r--r--src/libsync/vfs/cfapi/cfapiwrapper.cpp4
-rw-r--r--src/libsync/vfs/cfapi/shellext/CMakeLists.txt183
-rw-r--r--src/libsync/vfs/cfapi/shellext/CustomStateProvider.idl21
-rw-r--r--src/libsync/vfs/cfapi/shellext/configvfscfapishellext.h.in20
-rw-r--r--src/libsync/vfs/cfapi/shellext/customstateprovider.cpp104
-rw-r--r--src/libsync/vfs/cfapi/shellext/customstateprovider.h46
-rw-r--r--src/libsync/vfs/cfapi/shellext/customstateprovideripc.cpp104
-rw-r--r--src/libsync/vfs/cfapi/shellext/customstateprovideripc.h43
-rw-r--r--src/libsync/vfs/cfapi/shellext/dllmain.cpp19
-rw-r--r--src/libsync/vfs/cfapi/shellext/ipccommon.cpp50
-rw-r--r--src/libsync/vfs/cfapi/shellext/ipccommon.h21
-rw-r--r--src/libsync/vfs/cfapi/shellext/thumbnailprovider.cpp7
-rw-r--r--src/libsync/vfs/cfapi/shellext/thumbnailprovider.h2
-rw-r--r--src/libsync/vfs/cfapi/shellext/thumbnailprovideripc.cpp27
-rw-r--r--src/libsync/vfs/cfapi/vfs_cfapi.cpp49
32 files changed, 1028 insertions, 133 deletions
diff --git a/src/common/shellextensionutils.cpp b/src/common/shellextensionutils.cpp
index d6f4b244c..b2b59be80 100644
--- a/src/common/shellextensionutils.cpp
+++ b/src/common/shellextensionutils.cpp
@@ -29,7 +29,6 @@ namespace Protocol {
if (!valid) {
qCWarning(lcShellExtensionUtils) << "Invalid shell extensions IPC protocol: " << message.value(QStringLiteral("version")) << " vs " << Version;
}
- Q_ASSERT(valid);
return valid;
}
}
diff --git a/src/common/shellextensionutils.h b/src/common/shellextensionutils.h
index ca0d9922d..d0b2e071d 100644
--- a/src/common/shellextensionutils.h
+++ b/src/common/shellextensionutils.h
@@ -23,11 +23,14 @@ QString serverNameForApplicationName(const QString &applicationName);
QString serverNameForApplicationNameDefault();
namespace Protocol {
+ static constexpr auto CustomStateProviderRequestKey = "customStateProviderRequest";
+ static constexpr auto CustomStateDataKey = "customStateData";
+ static constexpr auto CustomStateStatesKey = "states";
+ static constexpr auto FilePathKey = "filePath";
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";
+ static constexpr auto Version = "2.0";
QByteArray createJsonMessage(const QVariantMap &message);
bool validateProtocolVersion(const QVariantMap &message);
diff --git a/src/common/syncjournaldb.cpp b/src/common/syncjournaldb.cpp
index ba4a84d4e..d5ede0f01 100644
--- a/src/common/syncjournaldb.cpp
+++ b/src/common/syncjournaldb.cpp
@@ -49,7 +49,7 @@ Q_LOGGING_CATEGORY(lcDb, "nextcloud.sync.database", QtInfoMsg)
#define GET_FILE_RECORD_QUERY \
"SELECT path, inode, modtime, type, md5, fileid, remotePerm, filesize," \
" ignoredChildrenRemote, contentchecksumtype.name || ':' || contentChecksum, e2eMangledName, isE2eEncrypted, " \
- " lock, lockOwnerDisplayName, lockOwnerId, lockType, lockOwnerEditor, lockTime, lockTimeout " \
+ " lock, lockOwnerDisplayName, lockOwnerId, lockType, lockOwnerEditor, lockTime, lockTimeout, isShared, lastShareStateFetchedTimestmap " \
" FROM metadata" \
" LEFT JOIN checksumtype as contentchecksumtype ON metadata.contentChecksumTypeId == contentchecksumtype.id"
@@ -74,6 +74,8 @@ static void fillFileRecordFromGetQuery(SyncJournalFileRecord &rec, SqlQuery &que
rec._lockstate._lockEditorApp = query.stringValue(16);
rec._lockstate._lockTime = query.int64Value(17);
rec._lockstate._lockTimeout = query.int64Value(18);
+ rec._isShared = query.intValue(19) > 0;
+ rec._lastShareStateFetchedTimestmap = query.int64Value(20);
}
static QByteArray defaultJournalMode(const QString &dbPath)
@@ -727,6 +729,8 @@ bool SyncJournalDb::updateMetadataTableStructure()
addColumn(QStringLiteral("contentChecksumTypeId"), QStringLiteral("INTEGER"));
addColumn(QStringLiteral("e2eMangledName"), QStringLiteral("TEXT"));
addColumn(QStringLiteral("isE2eEncrypted"), QStringLiteral("INTEGER"));
+ addColumn(QStringLiteral("isShared"), QStringLiteral("INTEGER"));
+ addColumn(QStringLiteral("lastShareStateFetchedTimestmap"), QStringLiteral("INTEGER"));
auto uploadInfoColumns = tableColumns("uploadinfo");
if (uploadInfoColumns.isEmpty())
@@ -881,13 +885,17 @@ Result<void, QString> SyncJournalDb::setFileRecord(const SyncJournalFileRecord &
}
qCInfo(lcDb) << "Updating file record for path:" << record.path() << "inode:" << record._inode
- << "modtime:" << record._modtime << "type:" << record._type
- << "etag:" << record._etag << "fileId:" << record._fileId << "remotePerm:" << record._remotePerm.toString()
+ << "modtime:" << record._modtime << "type:" << record._type << "etag:" << record._etag
+ << "fileId:" << record._fileId << "remotePerm:" << record._remotePerm.toString()
<< "fileSize:" << record._fileSize << "checksum:" << record._checksumHeader
<< "e2eMangledName:" << record.e2eMangledName() << "isE2eEncrypted:" << record._isE2eEncrypted
- << "lock:" << (record._lockstate._locked ? "true" : "false") << "lock owner type:" << record._lockstate._lockOwnerType
- << "lock owner:" << record._lockstate._lockOwnerDisplayName << "lock owner id:" << record._lockstate._lockOwnerId
- << "lock editor:" << record._lockstate._lockEditorApp;
+ << "lock:" << (record._lockstate._locked ? "true" : "false")
+ << "lock owner type:" << record._lockstate._lockOwnerType
+ << "lock owner:" << record._lockstate._lockOwnerDisplayName
+ << "lock owner id:" << record._lockstate._lockOwnerId
+ << "lock editor:" << record._lockstate._lockEditorApp
+ << "isShared:" << record._isShared
+ << "lastShareStateFetchedTimestmap:" << record._lastShareStateFetchedTimestmap;
const qint64 phash = getPHash(record._path);
if (!checkConnect()) {
@@ -913,8 +921,8 @@ Result<void, QString> SyncJournalDb::setFileRecord(const SyncJournalFileRecord &
const auto query = _queryManager.get(PreparedSqlQueryManager::SetFileRecordQuery, QByteArrayLiteral("INSERT OR REPLACE INTO metadata "
"(phash, pathlen, path, inode, uid, gid, mode, modtime, type, md5, fileid, remotePerm, filesize, ignoredChildrenRemote, "
"contentChecksum, contentChecksumTypeId, e2eMangledName, isE2eEncrypted, lock, lockType, lockOwnerDisplayName, lockOwnerId, "
- "lockOwnerEditor, lockTime, lockTimeout) "
- "VALUES (?1 , ?2, ?3 , ?4 , ?5 , ?6 , ?7, ?8 , ?9 , ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25);"),
+ "lockOwnerEditor, lockTime, lockTimeout, isShared, lastShareStateFetchedTimestmap) "
+ "VALUES (?1 , ?2, ?3 , ?4 , ?5 , ?6 , ?7, ?8 , ?9 , ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27);"),
_db);
if (!query) {
return query->error();
@@ -945,6 +953,8 @@ Result<void, QString> SyncJournalDb::setFileRecord(const SyncJournalFileRecord &
query->bindValue(23, record._lockstate._lockEditorApp);
query->bindValue(24, record._lockstate._lockTime);
query->bindValue(25, record._lockstate._lockTimeout);
+ query->bindValue(26, record._isShared);
+ query->bindValue(27, record._lastShareStateFetchedTimestmap);
if (!query->exec()) {
return query->error();
diff --git a/src/common/syncjournalfilerecord.h b/src/common/syncjournalfilerecord.h
index 13cf3ef8a..4e011e616 100644
--- a/src/common/syncjournalfilerecord.h
+++ b/src/common/syncjournalfilerecord.h
@@ -81,6 +81,8 @@ public:
QByteArray _e2eMangledName;
bool _isE2eEncrypted = false;
SyncJournalFileLockInfo _lockstate;
+ bool _isShared = false;
+ qint64 _lastShareStateFetchedTimestmap = 0;
};
bool OCSYNC_EXPORT
diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt
index 0258ff41b..a137916bf 100644
--- a/src/gui/CMakeLists.txt
+++ b/src/gui/CMakeLists.txt
@@ -353,11 +353,7 @@ if(Qt5Keychain_FOUND)
endif()
# add executable icon on windows and osx
-
-# UPSTREAM our ECMAddAppIcon.cmake then require that version here
-# find_package(ECM 1.7.0 REQUIRED NO_MODULE)
-# list(APPEND CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
-include(ECMAddAppIcon)
+include(GenerateIconsUtils)
# For historical reasons we can not use the application_shortname
# for ownCloud but must rather set it manually.
@@ -369,61 +365,6 @@ if(NOT DEFINED APPLICATION_FOLDER_ICON_INDEX)
set(APPLICATION_FOLDER_ICON_INDEX 0)
endif()
-# Generate png icons from svg
-find_program(SVG_CONVERTER
- NAMES inkscape inkscape.exe rsvg-convert
- REQUIRED
- HINTS "C:\\Program Files\\Inkscape\\bin" "/usr/bin" ENV SVG_CONVERTER_DIR)
-# REQUIRED keyword is only supported on CMake 3.18 and above
-if (NOT SVG_CONVERTER)
- message(FATAL_ERROR "Could not find a suitable svg converter. Set SVG_CONVERTER_DIR to the path of either the inkscape or rsvg-convert executable.")
-endif()
-
-function(generate_sized_png_from_svg icon_path size)
- set(options)
- set(oneValueArgs OUTPUT_ICON_NAME OUTPUT_ICON_FULL_NAME_WLE OUTPUT_ICON_PATH)
- set(multiValueArgs)
-
- cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
-
- get_filename_component(icon_name_dir ${icon_path} DIRECTORY)
- get_filename_component(icon_name_wle ${icon_path} NAME_WLE)
-
- if (ARG_OUTPUT_ICON_NAME)
- set(icon_name_wle ${ARG_OUTPUT_ICON_NAME})
- endif ()
-
- if (ARG_OUTPUT_ICON_PATH)
- set(icon_name_dir ${ARG_OUTPUT_ICON_PATH})
- endif ()
-
- set(output_icon_full_name_wle "${size}-${icon_name_wle}")
-
- if (ARG_OUTPUT_ICON_FULL_NAME_WLE)
- set(output_icon_full_name_wle ${ARG_OUTPUT_ICON_FULL_NAME_WLE})
- endif ()
-
- if (EXISTS "${icon_name_dir}/${output_icon_full_name_wle}.png")
- return()
- endif()
-
- set(icon_output_name "${output_icon_full_name_wle}.png")
- message(STATUS "Generate ${icon_output_name}")
- execute_process(COMMAND
- "${SVG_CONVERTER}" -w ${size} -h ${size} "${icon_path}" -o "${icon_output_name}"
- WORKING_DIRECTORY "${icon_name_dir}"
- RESULT_VARIABLE
- SVG_CONVERTER_SIDEBAR_ERROR
- OUTPUT_QUIET
- ERROR_QUIET)
-
- if (SVG_CONVERTER_SIDEBAR_ERROR)
- message(FATAL_ERROR
- "${SVG_CONVERTER} could not generate icon: ${SVG_CONVERTER_SIDEBAR_ERROR}")
- else()
- endif()
-endfunction()
-
set(STATE_ICONS_COLORS colored black white)
foreach(state_icons_color ${STATE_ICONS_COLORS})
diff --git a/src/gui/ocsjob.cpp b/src/gui/ocsjob.cpp
index ab4038c05..68c680b4d 100644
--- a/src/gui/ocsjob.cpp
+++ b/src/gui/ocsjob.cpp
@@ -40,7 +40,7 @@ void OcsJob::setVerb(const QByteArray &verb)
void OcsJob::addParam(const QString &name, const QString &value)
{
- _params.append(qMakePair(name, value));
+ _params.insert(name, value);
}
void OcsJob::addPassStatusCode(int code)
@@ -58,16 +58,21 @@ void OcsJob::addRawHeader(const QByteArray &headerName, const QByteArray &value)
_request.setRawHeader(headerName, value);
}
+QString OcsJob::getParamValue(const QString &key) const
+{
+ return _params.value(key);
+}
+
static QUrlQuery percentEncodeQueryItems(
- const QList<QPair<QString, QString>> &items)
+ const QHash<QString, QString> &items)
{
QUrlQuery result;
// Note: QUrlQuery::setQueryItems() does not fully percent encode
// the query items, see #5042
- foreach (const auto &item, items) {
+ for (auto it = std::cbegin(items); it != std::cend(items); ++it) {
result.addQueryItem(
- QUrl::toPercentEncoding(item.first),
- QUrl::toPercentEncoding(item.second));
+ QUrl::toPercentEncoding(it.key()),
+ QUrl::toPercentEncoding(it.value()));
}
return result;
}
@@ -85,13 +90,13 @@ void OcsJob::start()
} else if (_verb == "POST" || _verb == "PUT") {
// Url encode the _postParams and put them in a buffer.
QByteArray postData;
- Q_FOREACH (auto tmp, _params) {
+ for (auto it = std::cbegin(_params); it != std::cend(_params); ++it) {
if (!postData.isEmpty()) {
postData.append("&");
}
- postData.append(QUrl::toPercentEncoding(tmp.first));
+ postData.append(QUrl::toPercentEncoding(it.key()));
postData.append("=");
- postData.append(QUrl::toPercentEncoding(tmp.second));
+ postData.append(QUrl::toPercentEncoding(it.value()));
}
buffer->setData(postData);
}
diff --git a/src/gui/ocsjob.h b/src/gui/ocsjob.h
index e3b8b475d..6973d7ce0 100644
--- a/src/gui/ocsjob.h
+++ b/src/gui/ocsjob.h
@@ -19,8 +19,7 @@
#include "abstractnetworkjob.h"
#include <QVector>
-#include <QList>
-#include <QPair>
+#include <QHash>
#include <QUrl>
#define OCS_SUCCESS_STATUS_CODE 100
@@ -110,6 +109,8 @@ public:
*/
void addRawHeader(const QByteArray &headerName, const QByteArray &value);
+ [[nodiscard]] QString getParamValue(const QString &key) const;
+
protected slots:
@@ -149,7 +150,7 @@ private slots:
private:
QByteArray _verb;
- QList<QPair<QString, QString>> _params;
+ QHash<QString, QString> _params;
QVector<int> _passStatusCodes;
QNetworkRequest _request;
};
diff --git a/src/gui/ocssharejob.cpp b/src/gui/ocssharejob.cpp
index 900074dbc..13c494c4e 100644
--- a/src/gui/ocssharejob.cpp
+++ b/src/gui/ocssharejob.cpp
@@ -24,16 +24,21 @@ namespace OCC {
OcsShareJob::OcsShareJob(AccountPtr account)
: OcsJob(account)
{
- setPath("ocs/v2.php/apps/files_sharing/api/v1/shares");
+ setPath(_pathForSharesRequest);
connect(this, &OcsJob::jobFinished, this, &OcsShareJob::jobDone);
}
-void OcsShareJob::getShares(const QString &path)
+void OcsShareJob::getShares(const QString &path, const QMap<QString, QString> &params)
{
setVerb("GET");
addParam(QString::fromLatin1("path"), path);
addParam(QString::fromLatin1("reshares"), QString("true"));
+
+ for (auto it = std::cbegin(params); it != std::cend(params); ++it) {
+ addParam(it.key(), it.value());
+ }
+
addPassStatusCode(404);
start();
@@ -181,4 +186,6 @@ void OcsShareJob::jobDone(QJsonDocument reply)
{
emit shareJobFinished(reply, _value);
}
+
+QString const OcsShareJob::_pathForSharesRequest = QStringLiteral("ocs/v2.php/apps/files_sharing/api/v1/shares");
}
diff --git a/src/gui/ocssharejob.h b/src/gui/ocssharejob.h
index 064706b5c..61d8c1d26 100644
--- a/src/gui/ocssharejob.h
+++ b/src/gui/ocssharejob.h
@@ -46,7 +46,7 @@ public:
*
* @param path Path to request shares for (default all shares)
*/
- void getShares(const QString &path = "");
+ void getShares(const QString &path = "", const QMap<QString, QString> &params = {});
/**
* Delete the current Share
@@ -131,6 +131,8 @@ public:
*/
void getSharedWithMe();
+ static const QString _pathForSharesRequest;
+
signals:
/**
* Result of the OCS request
diff --git a/src/gui/shellextensionsserver.cpp b/src/gui/shellextensionsserver.cpp
index fa97a1b62..2b64c2b99 100644
--- a/src/gui/shellextensionsserver.cpp
+++ b/src/gui/shellextensionsserver.cpp
@@ -16,29 +16,58 @@
#include "account.h"
#include "accountstate.h"
#include "common/shellextensionutils.h"
+#include <libsync/vfs/cfapi/shellext/configvfscfapishellext.h>
#include "folder.h"
#include "folderman.h"
+#include "ocssharejob.h"
#include <QDir>
+#include <QJsonArray>
#include <QJsonDocument>
+#include <QJsonObject>
#include <QLocalSocket>
+namespace {
+constexpr auto isSharedInvalidationInterval = 2 * 60 * 1000; // 2 minutes, so we don't make fetch sharees requests too often
+constexpr auto folderAliasPropertyKey = "folderAlias";
+}
+
namespace OCC {
+Q_LOGGING_CATEGORY(lcShellExtServer, "nextcloud.gui.shellextensions.server", QtInfoMsg)
+
ShellExtensionsServer::ShellExtensionsServer(QObject *parent)
: QObject(parent)
{
+ _isSharedInvalidationInterval = isSharedInvalidationInterval;
_localServer.listen(VfsShellExtensions::serverNameForApplicationNameDefault());
connect(&_localServer, &QLocalServer::newConnection, this, &ShellExtensionsServer::slotNewConnection);
}
ShellExtensionsServer::~ShellExtensionsServer()
{
+ for (const auto &connection : _customStateSocketConnections) {
+ if (connection) {
+ QObject::disconnect(connection);
+ }
+ }
+ _customStateSocketConnections.clear();
+
if (!_localServer.isListening()) {
return;
}
_localServer.close();
}
+QString ShellExtensionsServer::getFetchThumbnailPath()
+{
+ return QStringLiteral("/index.php/core/preview");
+}
+
+void ShellExtensionsServer::setIsSharedInvalidationInterval(qint64 interval)
+{
+ _isSharedInvalidationInterval = interval;
+}
+
void ShellExtensionsServer::sendJsonMessageWithVersion(QLocalSocket *socket, const QVariantMap &message)
{
socket->write(VfsShellExtensions::Protocol::createJsonMessage(message));
@@ -60,6 +89,96 @@ void ShellExtensionsServer::closeSession(QLocalSocket *socket)
socket->disconnectFromServer();
}
+void ShellExtensionsServer::processCustomStateRequest(QLocalSocket *socket, const CustomStateRequestInfo &customStateRequestInfo)
+{
+ if (!customStateRequestInfo.isValid()) {
+ sendEmptyDataAndCloseSession(socket);
+ return;
+ }
+
+ const auto folder = FolderMan::instance()->folder(customStateRequestInfo.folderAlias);
+
+ if (!folder) {
+ sendEmptyDataAndCloseSession(socket);
+ return;
+ }
+ const auto filePathRelative = QString(customStateRequestInfo.path).remove(folder->path());
+
+ SyncJournalFileRecord record;
+ if (!folder->journalDb()->getFileRecord(filePathRelative, &record) || !record.isValid() || record.path().isEmpty()) {
+ qCWarning(lcShellExtServer) << "Record not found in SyncJournal for: " << filePathRelative;
+ sendEmptyDataAndCloseSession(socket);
+ return;
+ }
+
+ const auto composeMessageReplyFromRecord = [](const SyncJournalFileRecord &record) {
+ QVariantList states;
+ if (record._lockstate._locked) {
+ states.push_back(QString(CUSTOM_STATE_ICON_LOCKED_INDEX).toInt() - QString(CUSTOM_STATE_ICON_INDEX_OFFSET).toInt());
+ }
+ if (record._isShared) {
+ states.push_back(QString(CUSTOM_STATE_ICON_SHARED_INDEX).toInt() - QString(CUSTOM_STATE_ICON_INDEX_OFFSET).toInt());
+ }
+ return QVariantMap{{VfsShellExtensions::Protocol::CustomStateDataKey,
+ QVariantMap{{VfsShellExtensions::Protocol::CustomStateStatesKey, states}}}};
+ };
+
+ if (QDateTime::currentMSecsSinceEpoch() - record._lastShareStateFetchedTimestmap < _isSharedInvalidationInterval) {
+ qCInfo(lcShellExtServer) << record.path() << " record._lastShareStateFetchedTimestmap has less than " << _isSharedInvalidationInterval << " ms difference with QDateTime::currentMSecsSinceEpoch(). Returning data from SyncJournal.";
+ sendJsonMessageWithVersion(socket, composeMessageReplyFromRecord(record));
+ closeSession(socket);
+ return;
+ }
+
+ const auto job = new OcsShareJob(folder->accountState()->account());
+ job->setProperty(folderAliasPropertyKey, customStateRequestInfo.folderAlias);
+ connect(job, &OcsShareJob::shareJobFinished, this, &ShellExtensionsServer::slotSharesFetched);
+ connect(job, &OcsJob::ocsError, this, &ShellExtensionsServer::slotSharesFetchError);
+
+ {
+ _customStateSocketConnections.insert(socket->socketDescriptor(), QObject::connect(this, &ShellExtensionsServer::fetchSharesJobFinished, [this, socket, filePathRelative, composeMessageReplyFromRecord](const QString &folderAlias) {
+ {
+ const auto connection = _customStateSocketConnections[socket->socketDescriptor()];
+ if (connection) {
+ QObject::disconnect(connection);
+ }
+ _customStateSocketConnections.remove(socket->socketDescriptor());
+ }
+
+ const auto folder = FolderMan::instance()->folder(folderAlias);
+ SyncJournalFileRecord record;
+ if (!folder || !folder->journalDb()->getFileRecord(filePathRelative, &record) || !record.isValid()) {
+ qCWarning(lcShellExtServer) << "Record not found in SyncJournal for: " << filePathRelative;
+ sendEmptyDataAndCloseSession(socket);
+ return;
+ }
+
+ qCInfo(lcShellExtServer) << "Sending reply from OcsShareJob for socket: " << socket->socketDescriptor() << " and record: " << record.path();
+ sendJsonMessageWithVersion(socket, composeMessageReplyFromRecord(record));
+ closeSession(socket);
+ }));
+ }
+
+ const auto sharesPath = [&record, folder, &filePathRelative]() {
+ const auto filePathRelativeRemote = QDir(folder->remotePath()).filePath(filePathRelative);
+ // either get parent's path, or, return '/' if we are in the root folder
+ auto recordPathSplit = filePathRelativeRemote.split(QLatin1Char('/'), Qt::SkipEmptyParts);
+ if (recordPathSplit.size() > 1) {
+ recordPathSplit.removeLast();
+ return recordPathSplit.join(QLatin1Char('/'));
+ }
+ return QStringLiteral("/");
+ }();
+
+ if (!_runningFetchShareJobsForPaths.contains(sharesPath)) {
+ _runningFetchShareJobsForPaths.push_back(sharesPath);
+ qCInfo(lcShellExtServer) << "Started OcsShareJob for path: " << sharesPath;
+ job->getShares(sharesPath, {{QStringLiteral("subfiles"), QStringLiteral("true")}});
+ } else {
+ qCInfo(lcShellExtServer) << "OcsShareJob is already running for path: " << sharesPath;
+ }
+}
+
void ShellExtensionsServer::processThumbnailRequest(QLocalSocket *socket, const ThumbnailRequestInfo &thumbnailRequestInfo)
{
if (!thumbnailRequestInfo.isValid()) {
@@ -87,7 +206,7 @@ void ShellExtensionsServer::processThumbnailRequest(QLocalSocket *socket, const
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 QUrl jobUrl = Utility::concatUrlPath(folder->accountState()->account()->url(), getFetchThumbnailPath(), queryItems);
const auto job = new SimpleNetworkJob(folder->accountState()->account());
job->startRequest(QByteArrayLiteral("GET"), jobUrl);
connect(job, &SimpleNetworkJob::finishedSignal, this, [socket, this](QNetworkReply *reply) {
@@ -121,8 +240,155 @@ void ShellExtensionsServer::slotNewConnection()
return;
}
+ if (message.contains(VfsShellExtensions::Protocol::ThumbnailProviderRequestKey)) {
+ parseThumbnailRequest(socket, message);
+ return;
+ } else if (message.contains(VfsShellExtensions::Protocol::CustomStateProviderRequestKey)) {
+ parseCustomStateRequest(socket, message);
+ return;
+ }
+ qCWarning(lcShellExtServer) << "Invalid message received from shell extension: " << message;
+ sendEmptyDataAndCloseSession(socket);
+ return;
+}
+
+void ShellExtensionsServer::slotSharesFetched(const QJsonDocument &reply)
+{
+ const auto job = qobject_cast<OcsShareJob *>(sender());
+
+ Q_ASSERT(job);
+ if (!job) {
+ qCWarning(lcShellExtServer) << "ShellExtensionsServer::slotSharesFetched is not called by OcsShareJob's signal!";
+ return;
+ }
+
+ const auto sharesPath = job->getParamValue(QStringLiteral("path"));
+
+ _runningFetchShareJobsForPaths.removeAll(sharesPath);
+
+ const auto folderAlias = job->property(folderAliasPropertyKey).toString();
+
+ Q_ASSERT(!folderAlias.isEmpty());
+ if (folderAlias.isEmpty()) {
+ qCWarning(lcShellExtServer) << "No 'folderAlias' set for OcsShareJob's instance!";
+ return;
+ }
+
+ const auto folder = FolderMan::instance()->folder(folderAlias);
+
+ Q_ASSERT(folder);
+ if (!folder) {
+ qCWarning(lcShellExtServer) << "folder not found for folderAlias: " << folderAlias;
+ return;
+ }
+
+ const auto timeStamp = QDateTime::currentMSecsSinceEpoch();
+ QStringList recortPathsToResetIsSharedFlag;
+ const QByteArray pathOfSharesToResetIsSharedFlag = sharesPath == QStringLiteral("/") ? QByteArrayLiteral("") : sharesPath.toUtf8();
+ if (folder->journalDb()->listFilesInPath(pathOfSharesToResetIsSharedFlag, [&](const SyncJournalFileRecord &rec) {
+ recortPathsToResetIsSharedFlag.push_back(rec.path());
+ })) {
+ for (const auto &recordPath : recortPathsToResetIsSharedFlag) {
+ SyncJournalFileRecord record;
+ if (!folder->journalDb()->getFileRecord(recordPath, &record) || !record.isValid()) {
+ continue;
+ }
+ record._isShared = false;
+ record._lastShareStateFetchedTimestmap = timeStamp;
+ if (!folder->journalDb()->setFileRecord(record)) {
+ qCWarning(lcShellExtServer) << "Could not set file record for path: " << record._path;
+ }
+ }
+ }
+
+ const auto sharesFetched = reply.object().value(QStringLiteral("ocs")).toObject().value(QStringLiteral("data")).toArray();
+
+ for (const auto &share : sharesFetched) {
+ const auto shareData = share.toObject();
+
+ const auto sharePath = [&shareData, folder]() {
+ const auto sharePathRemote = shareData.value(QStringLiteral("path")).toString();
+
+ const auto folderPath = folder->remotePath();
+ if (folderPath != QLatin1Char('/') && sharePathRemote.startsWith(folderPath)) {
+ // shares are ruturned with absolute remote path, so, if we have our remote root set to subfolder, we need to adjust share's remote path to relative local path
+ const auto sharePathLocalRelative = sharePathRemote.midRef(folder->remotePathTrailingSlash().length());
+ return sharePathLocalRelative.toString();
+ }
+ return sharePathRemote.size() > 1 && sharePathRemote.startsWith(QLatin1Char('/'))
+ ? QString(sharePathRemote).remove(0, 1)
+ : sharePathRemote;
+ }();
+
+ SyncJournalFileRecord record;
+ if (!folder || !folder->journalDb()->getFileRecord(sharePath, &record) || !record.isValid()) {
+ continue;
+ }
+ record._isShared = true;
+ record._lastShareStateFetchedTimestmap = timeStamp;
+
+ if (!folder->journalDb()->setFileRecord(record)) {
+ qCWarning(lcShellExtServer) << "Could not set file record for path: " << record._path;
+ }
+ }
+
+ qCInfo(lcShellExtServer) << "Succeeded OcsShareJob for path: " << sharesPath;
+ emit fetchSharesJobFinished(folderAlias);
+}
+
+void ShellExtensionsServer::slotSharesFetchError(int statusCode, const QString &message)
+{
+ const auto job = qobject_cast<OcsShareJob *>(sender());
+
+ Q_ASSERT(job);
+ if (!job) {
+ qCWarning(lcShellExtServer) << "ShellExtensionsServer::slotSharesFetched is not called by OcsShareJob's signal!";
+ return;
+ }
+
+ const auto sharesPath = job->getParamValue(QStringLiteral("path"));
+
+ _runningFetchShareJobsForPaths.removeAll(sharesPath);
+
+ emit fetchSharesJobFinished(sharesPath);
+ qCWarning(lcShellExtServer) << "Failed OcsShareJob for path: " << sharesPath;
+}
+
+void ShellExtensionsServer::parseCustomStateRequest(QLocalSocket *socket, const QVariantMap &message)
+{
+ const auto customStateRequestMessage = message.value(VfsShellExtensions::Protocol::CustomStateProviderRequestKey).toMap();
+ const auto itemFilePath = QDir::fromNativeSeparators(customStateRequestMessage.value(VfsShellExtensions::Protocol::FilePathKey).toString());
+
+ if (itemFilePath.isEmpty()) {
+ sendEmptyDataAndCloseSession(socket);
+ return;
+ }
+
+ QString foundFolderAlias;
+ for (const auto folder : FolderMan::instance()->map()) {
+ if (itemFilePath.startsWith(folder->path())) {
+ foundFolderAlias = folder->alias();
+ break;
+ }
+ }
+
+ if (foundFolderAlias.isEmpty()) {
+ sendEmptyDataAndCloseSession(socket);
+ return;
+ }
+
+ const auto customStateRequestInfo = CustomStateRequestInfo {
+ itemFilePath,
+ foundFolderAlias
+ };
+
+ processCustomStateRequest(socket, customStateRequestInfo);
+}
+
+void ShellExtensionsServer::parseThumbnailRequest(QLocalSocket *socket, const QVariantMap &message)
+{
const auto thumbnailRequestMessage = message.value(VfsShellExtensions::Protocol::ThumbnailProviderRequestKey).toMap();
- const auto thumbnailFilePath = QDir::fromNativeSeparators(thumbnailRequestMessage.value(VfsShellExtensions::Protocol::ThumbnailProviderRequestFilePathKey).toString());
+ const auto thumbnailFilePath = QDir::fromNativeSeparators(thumbnailRequestMessage.value(VfsShellExtensions::Protocol::FilePathKey).toString());
const auto thumbnailFileSize = thumbnailRequestMessage.value(VfsShellExtensions::Protocol::ThumbnailProviderRequestFileSizeKey).toMap();
if (thumbnailFilePath.isEmpty() || thumbnailFileSize.isEmpty()) {
diff --git a/src/gui/shellextensionsserver.h b/src/gui/shellextensionsserver.h
index 50df905ac..491d4cfe0 100644
--- a/src/gui/shellextensionsserver.h
+++ b/src/gui/shellextensionsserver.h
@@ -16,8 +16,11 @@
#include <QObject>
#include <QLocalServer>
+#include <QMutex>
#include <QSize>
+#include <QVariant>
+class QJsonDocument;
class QLocalSocket;
namespace OCC {
@@ -32,21 +35,45 @@ class ShellExtensionsServer : public QObject
[[nodiscard]] bool isValid() const { return !path.isEmpty() && !size.isEmpty() && !folderAlias.isEmpty(); }
};
+ struct CustomStateRequestInfo
+ {
+ QString path;
+ QString folderAlias;
+
+ bool isValid() const { return !path.isEmpty() && !folderAlias.isEmpty(); }
+ };
+
Q_OBJECT
public:
ShellExtensionsServer(QObject *parent = nullptr);
~ShellExtensionsServer() override;
+ static QString getFetchThumbnailPath();
+
+ void setIsSharedInvalidationInterval(qint64 interval);
+
+signals:
+ void fetchSharesJobFinished(const QString &folderAlias);
+
private:
void sendJsonMessageWithVersion(QLocalSocket *socket, const QVariantMap &message);
void sendEmptyDataAndCloseSession(QLocalSocket *socket);
void closeSession(QLocalSocket *socket);
+ void processCustomStateRequest(QLocalSocket *socket, const CustomStateRequestInfo &customStateRequestInfo);
void processThumbnailRequest(QLocalSocket *socket, const ThumbnailRequestInfo &thumbnailRequestInfo);
+ void parseCustomStateRequest(QLocalSocket *socket, const QVariantMap &message);
+ void parseThumbnailRequest(QLocalSocket *socket, const QVariantMap &message);
+
private slots:
void slotNewConnection();
+ void slotSharesFetched(const QJsonDocument &reply);
+ void slotSharesFetchError(int statusCode, const QString &message);
private:
QLocalServer _localServer;
+ QStringList _runningFetchShareJobsForPaths;
+ QMap<qintptr, QMetaObject::Connection> _customStateSocketConnections;
+ qint64 _isSharedInvalidationInterval = 0;
};
} // namespace OCC
diff --git a/src/gui/socketapi/socketapi.cpp b/src/gui/socketapi/socketapi.cpp
index 9b14bb63e..4de3317a2 100644
--- a/src/gui/socketapi/socketapi.cpp
+++ b/src/gui/socketapi/socketapi.cpp
@@ -982,6 +982,9 @@ void SocketApi::setFileLock(const QString &localFile, const SyncFileItem::LockSt
}
shareFolder->accountState()->account()->setLockFileState(fileData.serverRelativePath, shareFolder->journalDb(), lockState);
+
+ shareFolder->journalDb()->schedulePathForRemoteDiscovery(fileData.serverRelativePath);
+ shareFolder->scheduleThisFolderSoon();
}
void SocketApi::command_V2_LIST_ACCOUNTS(const QSharedPointer<SocketApiJobV2> &job) const
diff --git a/src/libsync/bulkpropagatorjob.cpp b/src/libsync/bulkpropagatorjob.cpp
index 228180711..8afe789e3 100644
--- a/src/libsync/bulkpropagatorjob.cpp
+++ b/src/libsync/bulkpropagatorjob.cpp
@@ -393,6 +393,8 @@ void BulkPropagatorJob::slotPutFinishedOneFile(const BulkUploadItem &singleFile,
singleFile._item->_etag = etag;
singleFile._item->_fileId = getHeaderFromJsonReply(fileReply, "fileid");
singleFile._item->_remotePerm = RemotePermissions::fromServerString(getHeaderFromJsonReply(fileReply, "permissions"));
+ singleFile._item->_isShared = singleFile._item->_remotePerm.hasPermission(RemotePermissions::IsShared);
+ singleFile._item->_lastShareStateFetchedTimestmap = QDateTime::currentMSecsSinceEpoch();
if (getHeaderFromJsonReply(fileReply, "X-OC-MTime") != "accepted") {
// X-OC-MTime is supported since owncloud 5.0. But not when chunking.
diff --git a/src/libsync/discovery.cpp b/src/libsync/discovery.cpp
index 95a23c06b..ac6d0e8f8 100644
--- a/src/libsync/discovery.cpp
+++ b/src/libsync/discovery.cpp
@@ -475,6 +475,8 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo(
item->_checksumHeader = serverEntry.checksumHeader;
item->_fileId = serverEntry.fileId;
item->_remotePerm = serverEntry.remotePerm;
+ item->_isShared = serverEntry.remotePerm.hasPermission(RemotePermissions::IsShared);
+ item->_lastShareStateFetchedTimestmap = QDateTime::currentMSecsSinceEpoch();
item->_type = serverEntry.isDirectory ? ItemTypeDirectory : ItemTypeFile;
item->_etag = serverEntry.etag;
item->_directDownloadUrl = serverEntry.directDownloadUrl;
@@ -633,6 +635,8 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo(
item->_direction = SyncFileItem::Up;
item->_fileId = serverEntry.fileId;
item->_remotePerm = serverEntry.remotePerm;
+ item->_isShared = serverEntry.remotePerm.hasPermission(RemotePermissions::IsShared);
+ item->_lastShareStateFetchedTimestmap = QDateTime::currentMSecsSinceEpoch();
item->_etag = serverEntry.etag;
item->_type = serverEntry.isDirectory ? CSyncEnums::ItemTypeDirectory : CSyncEnums::ItemTypeFile;
@@ -919,6 +923,8 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
item->_remotePerm = base.isValid() ? base._remotePerm : RemotePermissions{};
item->_etag = base.isValid() ? base._etag : QByteArray{};
item->_type = base.isValid() ? base._type : localEntry.type;
+ item->_isShared = base.isValid() ? base._isShared : false;
+ item->_lastShareStateFetchedTimestmap = base.isValid() ? base._lastShareStateFetchedTimestmap : 0;
};
if (!localEntry.isValid()) {
@@ -1326,6 +1332,8 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
item->_direction = SyncFileItem::Up;
item->_fileId = base._fileId;
item->_remotePerm = base._remotePerm;
+ item->_isShared = base._isShared;
+ item->_lastShareStateFetchedTimestmap = base._lastShareStateFetchedTimestmap;
item->_etag = base._etag;
item->_type = base._type;
@@ -1451,6 +1459,8 @@ void ProcessDirectoryJob::processFileConflict(const SyncFileItemPtr &item, Proce
rec._type = item->_type;
rec._fileSize = serverEntry.size;
rec._remotePerm = serverEntry.remotePerm;
+ rec._isShared = serverEntry.remotePerm.hasPermission(RemotePermissions::IsShared);
+ rec._lastShareStateFetchedTimestmap = QDateTime::currentMSecsSinceEpoch();
rec._checksumHeader = serverEntry.checksumHeader;
const auto result = _discoveryData->_statedb->setFileRecord(rec);
if (!result) {
diff --git a/src/libsync/propagateremotemkdir.cpp b/src/libsync/propagateremotemkdir.cpp
index f95fbca01..5c4ea1e48 100644
--- a/src/libsync/propagateremotemkdir.cpp
+++ b/src/libsync/propagateremotemkdir.cpp
@@ -144,6 +144,8 @@ void PropagateRemoteMkdir::finalizeMkColJob(QNetworkReply::NetworkError err, con
connect(propfindJob, &PropfindJob::result, this, [this, jobPath](const QVariantMap &result){
propagator()->_activeJobList.removeOne(this);
_item->_remotePerm = RemotePermissions::fromServerString(result.value(QStringLiteral("permissions")).toString());
+ _item->_isShared = _item->_remotePerm.hasPermission(RemotePermissions::IsShared);
+ _item->_lastShareStateFetchedTimestmap = QDateTime::currentMSecsSinceEpoch();
if (!_uploadEncryptedHelper && !_item->_isEncrypted) {
success();
diff --git a/src/libsync/syncfileitem.cpp b/src/libsync/syncfileitem.cpp
index 07202d41f..97971d29f 100644
--- a/src/libsync/syncfileitem.cpp
+++ b/src/libsync/syncfileitem.cpp
@@ -41,6 +41,8 @@ SyncJournalFileRecord SyncFileItem::toSyncJournalFileRecordWithInode(const QStri
rec._fileId = _fileId;
rec._fileSize = _size;
rec._remotePerm = _remotePerm;
+ rec._isShared = _isShared;
+ rec._lastShareStateFetchedTimestmap = _lastShareStateFetchedTimestmap;
rec._serverHasIgnoredFiles = _serverHasIgnoredFiles;
rec._checksumHeader = _checksumHeader;
rec._e2eMangledName = _encryptedFileName.toUtf8();
@@ -89,6 +91,8 @@ SyncFileItemPtr SyncFileItem::fromSyncJournalFileRecord(const SyncJournalFileRec
item->_lockEditorApp = rec._lockstate._lockEditorApp;
item->_lockTime = rec._lockstate._lockTime;
item->_lockTimeout = rec._lockstate._lockTimeout;
+ item->_isShared = rec._isShared;
+ item->_lastShareStateFetchedTimestmap = rec._lastShareStateFetchedTimestmap;
return item;
}
diff --git a/src/libsync/syncfileitem.h b/src/libsync/syncfileitem.h
index bda583e76..1e134eee1 100644
--- a/src/libsync/syncfileitem.h
+++ b/src/libsync/syncfileitem.h
@@ -308,6 +308,9 @@ public:
QString _lockEditorApp;
qint64 _lockTime = 0;
qint64 _lockTimeout = 0;
+
+ bool _isShared = false;
+ time_t _lastShareStateFetchedTimestmap = 0;
};
inline bool operator<(const SyncFileItemPtr &item1, const SyncFileItemPtr &item2)
diff --git a/src/libsync/vfs/cfapi/cfapiwrapper.cpp b/src/libsync/vfs/cfapi/cfapiwrapper.cpp
index 4975ca881..e98d02792 100644
--- a/src/libsync/vfs/cfapi/cfapiwrapper.cpp
+++ b/src/libsync/vfs/cfapi/cfapiwrapper.cpp
@@ -442,7 +442,8 @@ 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("CustomStateHandler"), REG_SZ, CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID_REG},
{ providerSyncRootIdRegistryKey, QStringLiteral("ThumbnailProvider"), REG_SZ, CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG},
{ providerSyncRootIdRegistryKey, QStringLiteral("NamespaceCLSID"), REG_SZ, QString(navigationPaneClsid)}
};
@@ -550,6 +551,7 @@ void unregisterSyncRootShellExtensions(const QString &providerName, const QStrin
const QString providerSyncRootIdRegistryKey = syncRootManagerRegKey + QStringLiteral("\\") + syncRootId;
OCC::Utility::registryDeleteKeyValue(HKEY_LOCAL_MACHINE, providerSyncRootIdRegistryKey, QStringLiteral("ThumbnailProvider"));
+ OCC::Utility::registryDeleteKeyValue(HKEY_LOCAL_MACHINE, providerSyncRootIdRegistryKey, QStringLiteral("CustomStateHandler"));
qCInfo(lcCfApiWrapper) << "Successfully unregistered SyncRoot Shell Extensions!";
}
diff --git a/src/libsync/vfs/cfapi/shellext/CMakeLists.txt b/src/libsync/vfs/cfapi/shellext/CMakeLists.txt
index 966ae60e0..067289ee4 100644
--- a/src/libsync/vfs/cfapi/shellext/CMakeLists.txt
+++ b/src/libsync/vfs/cfapi/shellext/CMakeLists.txt
@@ -1,18 +1,197 @@
+include(GenerateIconsUtils)
+
+# generate custom states icons
+set(theme_dir ${CMAKE_SOURCE_DIR}/theme)
+set(custom_state_icons_path "${theme_dir}/cfapishellext_custom_states")
+set(CUSTOM_STATE_ICON_LOCKED_PATH "${custom_state_icons_path}/0-locked.svg")
+set(CUSTOM_STATE_ICON_SHARED_PATH "${custom_state_icons_path}/1-shared.svg")
+
+foreach(size IN ITEMS 24;32;40;48;64;128;256;512;1024)
+ get_filename_component(output_icon_name_custom_state_locked ${CUSTOM_STATE_ICON_LOCKED_PATH} NAME_WLE)
+ generate_sized_png_from_svg(${CUSTOM_STATE_ICON_LOCKED_PATH} ${size} OUTPUT_ICON_NAME ${output_icon_name_custom_state_locked} OUTPUT_ICON_PATH "${custom_state_icons_path}/")
+endforeach()
+
+foreach(size IN ITEMS 24;32;40;48;64;128;256;512;1024)
+ get_filename_component(output_icon_name_custom_state_shared ${CUSTOM_STATE_ICON_SHARED_PATH} NAME_WLE)
+ generate_sized_png_from_svg(${CUSTOM_STATE_ICON_SHARED_PATH} ${size} OUTPUT_ICON_NAME ${output_icon_name_custom_state_shared} OUTPUT_ICON_PATH "${custom_state_icons_path}/")
+endforeach()
+
+# offset is used for referencing icon within the binary's resources (indexing start with 0, while IDI_ICON{i} 'i' starts with 1)
+if(NOT DEFINED CUSTOM_STATE_ICON_INDEX_OFFSET)
+ set(CUSTOM_STATE_ICON_INDEX_OFFSET 1)
+endif()
+
+# indeces used for referencing icon within the binary's resources and .rc file's IDI_ICON{i} entries 'i'
+if(NOT DEFINED CUSTOM_STATE_ICON_LOCKED_INDEX)
+ set(CUSTOM_STATE_ICON_LOCKED_INDEX 1)
+endif()
+if(NOT DEFINED CUSTOM_STATE_ICON_SHARED_INDEX)
+ set(CUSTOM_STATE_ICON_SHARED_INDEX 2)
+endif()
+
+file(GLOB_RECURSE CUSTOM_STATE_ICONS_LOCKED "${custom_state_icons_path}/*-locked.png*")
+get_filename_component(CUSTOM_STATE_ICON_LOCKED_NAME ${CUSTOM_STATE_ICON_LOCKED_PATH} NAME_WLE)
+ecm_add_app_icon(CUSTOM_STATE_ICON_LOCKED_OUT ICONS "${CUSTOM_STATE_ICONS_LOCKED}" OUTFILE_BASENAME "${CUSTOM_STATE_ICON_LOCKED_NAME}" DO_NOT_GENERATE_RC_FILE TRUE)
+
+file(GLOB_RECURSE CUSTOM_STATE_ICONS_SHARED "${custom_state_icons_path}/*-shared.png*")
+get_filename_component(CUSTOM_STATE_ICON_SHARED_NAME ${CUSTOM_STATE_ICON_SHARED_PATH} NAME_WLE)
+ecm_add_app_icon(CUSTOM_STATE_ICON_SHARED_OUT ICONS "${CUSTOM_STATE_ICONS_SHARED}" OUTFILE_BASENAME "${CUSTOM_STATE_ICON_SHARED_NAME}" DO_NOT_GENERATE_RC_FILE TRUE)
+
+file(REMOVE "${CMAKE_CURRENT_BINARY_DIR}/${CFAPI_SHELL_EXTENSIONS_LIB_NAME}.rc.in")
+
+file(APPEND "${CMAKE_CURRENT_BINARY_DIR}/${CFAPI_SHELL_EXTENSIONS_LIB_NAME}.rc.in" "IDI_ICON${CUSTOM_STATE_ICON_LOCKED_INDEX} ICON DISCARDABLE \"${CMAKE_CURRENT_BINARY_DIR}/${CUSTOM_STATE_ICON_LOCKED_NAME}.ico\"\n")
+file(APPEND "${CMAKE_CURRENT_BINARY_DIR}/${CFAPI_SHELL_EXTENSIONS_LIB_NAME}.rc.in" "IDI_ICON${CUSTOM_STATE_ICON_SHARED_INDEX} ICON DISCARDABLE \"${CMAKE_CURRENT_BINARY_DIR}/${CUSTOM_STATE_ICON_SHARED_NAME}.ico\"\n")
+
+add_custom_command(
+ OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${CFAPI_SHELL_EXTENSIONS_LIB_NAME}.rc"
+ COMMAND ${CMAKE_COMMAND}
+ ARGS -E copy "${CMAKE_CURRENT_BINARY_DIR}/${CFAPI_SHELL_EXTENSIONS_LIB_NAME}.rc.in" "${CMAKE_CURRENT_BINARY_DIR}/${CFAPI_SHELL_EXTENSIONS_LIB_NAME}.rc"
+ DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/${CUSTOM_STATE_ICON_LOCKED_NAME}.ico" "${CMAKE_CURRENT_BINARY_DIR}/${CUSTOM_STATE_ICON_SHARED_NAME}.ico"
+ WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
+)
+message("CUSTOM_STATE_ICON_LOCKED_OUT: ${CUSTOM_STATE_ICON_LOCKED_OUT}")
+message("CUSTOM_STATE_ICON_SHARED_OUT: ${CUSTOM_STATE_ICON_SHARED_OUT}")
+#
+
+# Windows SDK command-line tools require native paths
+file(TO_NATIVE_PATH "${CMAKE_CURRENT_SOURCE_DIR}" MidleFileFolder)
+set(GeneratedFilesPath "${CMAKE_CURRENT_BINARY_DIR}\\Generated")
+set(MidlOutputPathHeader "${GeneratedFilesPath}\\CustomStateProvider.g.h")
+set(MidlOutputPathTlb "${GeneratedFilesPath}\\CustomStateProvider.tlb")
+set(MidlOutputPathWinmd "${GeneratedFilesPath}\\CustomStateProvider.winmd")
+
+add_custom_target(CustomStateProviderImpl
+ DEPENDS ${MidlOutputPathHeader}
+)
+
+if(NOT DEFINED ENV{WindowsSdkDir})
+ message("Getting WindowsSdkDir from Registry")
+ get_filename_component(WindowsSdkDir "[HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows Kits\\Installed Roots;KitsRoot10]" ABSOLUTE)
+else()
+ set(WindowsSdkDir $ENV{WindowsSdkDir})
+ message("Setting WindowsSdkDir from ENV{WindowsSdkDir")
+endif()
+
+# we need cmake path to work with subfolders
+file(TO_CMAKE_PATH "${WindowsSdkDir}" WindowsSdkDir)
+
+MACRO(SUBDIRLIST result curdir)
+ FILE(GLOB children RELATIVE ${curdir} ${curdir}/*)
+ SET(dirlist "")
+ FOREACH(child ${children})
+ IF(IS_DIRECTORY ${curdir}/${child})
+ LIST(APPEND dirlist ${child})
+ ENDIF()
+ ENDFOREACH()
+ SET(${result} ${dirlist})
+ENDMACRO()
+
+SUBDIRLIST(WindowsSdkList "${WindowsSdkDir}/bin")
+
+# pick only dirs that start with 10.0
+list(FILTER WindowsSdkList INCLUDE REGEX "10.0.")
+# sort the list of subdirs and choose the latest
+list(SORT WindowsSdkList ORDER ASCENDING)
+list(GET WindowsSdkList -1 WindowsSdkLatest)
+message("WindowsSdkLatest has been set to: ${WindowsSdkLatest}")
+
+if(NOT WindowsSdkLatest)
+ message( FATAL_ERROR "Windows SDK not found")
+endif()
+
+SUBDIRLIST(listFoundationContracts "${WindowsSdkDir}/References/${WindowsSdkLatest}/Windows.Foundation.FoundationContract")
+list(FILTER listFoundationContracts INCLUDE REGEX "[0-9]+\.")
+list(SORT listFoundationContracts ORDER ASCENDING)
+list(GET listFoundationContracts -1 WindowsFoundationContractVersion)
+message("WindowsFoundationContractVersion has been set to: ${WindowsFoundationContractVersion}")
+
+if(NOT WindowsFoundationContractVersion)
+ message( FATAL_ERROR "Windows Foundation Contract is not found in ${WindowsSdkLatest} SDK.")
+endif()
+
+SUBDIRLIST(listCloudFilesContracts "${WindowsSdkDir}/References/${WindowsSdkLatest}/Windows.Storage.Provider.CloudFilesContract")
+list(FILTER listCloudFilesContracts INCLUDE REGEX "[0-9]+\.")
+list(SORT listCloudFilesContracts ORDER ASCENDING)
+list(GET listCloudFilesContracts -1 WindowsStorageProviderCloudFilesContractVersion)
+message("WindowsStorageProviderCloudFilesContractVersion has been set to: ${WindowsStorageProviderCloudFilesContractVersion}")
+
+if(NOT WindowsStorageProviderCloudFilesContractVersion)
+ message( FATAL_ERROR "Windows Storage Provider Cloud Files Contract is not found in ${WindowsSdkLatest} SDK.")
+endif()
+
+# we no longer need to work with sub folders, so convert the WindowsSdkDir to native path
+file(TO_NATIVE_PATH ${WindowsSdkDir} WindowsSdkDir)
+message("WindowsSdkDir has been set to: ${WindowsSdkDir}")
+message("WindowsSdkList has been set to: ${WindowsSdkList}")
+message("WindowsSdkLatest has been set to: ${WindowsSdkLatest}")
+
+set(TargetPlatform "x64")
+if(CMAKE_SIZEOF_VOID_P EQUAL 8)
+ set(TargetPlatform "x64")
+elseif(CMAKE_SIZEOF_VOID_P EQUAL 4)
+ set(TargetPlatform "x86")
+endif()
+
+set(WindowsSDKReferencesPath "${WindowsSdkDir}\\References\\${WindowsSdkLatest}")
+set(WindowsSDKBinPathForTools "${WindowsSdkDir}\\bin\\${WindowsSdkLatest}\\${TargetPlatform}")
+set(WindowsSDKMetadataDirectory "${WindowsSdkDir}\\UnionMetadata\\${WindowsSdkLatest}")
+
+IF(NOT EXISTS "${WindowsSDKReferencesPath}" OR NOT IS_DIRECTORY "${WindowsSDKReferencesPath}")
+ message( FATAL_ERROR "Please install Windows SDK ${WindowsSdkLatest}")
+ENDIF()
+IF(NOT EXISTS "${WindowsSDKBinPathForTools}" OR NOT IS_DIRECTORY "${WindowsSDKBinPathForTools}")
+ message( FATAL_ERROR "Please install Windows SDK ${WindowsSdkLatest}")
+ENDIF()
+IF(NOT EXISTS "${WindowsSDKMetadataDirectory}" OR NOT IS_DIRECTORY "${WindowsSDKMetadataDirectory}")
+ message( FATAL_ERROR "Please install Windows SDK ${WindowsSdkLatest}")
+ENDIF()
+set(midlExe "${WindowsSDKBinPathForTools}\\midl.exe")
+set(cppWinRtExe "${WindowsSDKBinPathForTools}\\cppwinrt.exe")
+
+message("cppWinRtExe: ${cppWinRtExe}")
+message("midlExe: ${midlExe}")
+
+# use midl.exe and cppwinrt.exe to generate files for CustomStateProvider (WinRT class)
+add_custom_command(OUTPUT ${MidlOutputPathHeader}
+ COMMAND ${midlExe} /winrt /h nul /tlb ${MidlOutputPathTlb} /winmd ${MidlOutputPathWinmd} /metadata_dir "${WindowsSDKReferencesPath}\\Windows.Foundation.FoundationContract\\${WindowsFoundationContractVersion}" /nomidl /reference "${WindowsSDKReferencesPath}\\Windows.Foundation.FoundationContract\\${WindowsFoundationContractVersion}\\Windows.Foundation.FoundationContract.winmd" /reference "${WindowsSDKReferencesPath}\\Windows.Storage.Provider.CloudFilesContract\\${WindowsStorageProviderCloudFilesContractVersion}\\Windows.Storage.Provider.CloudFilesContract.winmd" /I ${MidleFileFolder} customstateprovider.idl
+ COMMAND ${cppWinRtExe} -in ${MidlOutputPathWinmd} -comp ${GeneratedFilesPath} -pch pch.h -ref ${WindowsSDKMetadataDirectory} -out ${GeneratedFilesPath} -verbose
+ COMMENT "Creating generated files from customstateprovider.idl"
+)
+
add_library(CfApiShellExtensions MODULE
dllmain.cpp
cfapishellintegrationclassfactory.cpp
+ customstateprovideripc.cpp
+ ipccommon.cpp
thumbnailprovider.cpp
thumbnailprovideripc.cpp
${CMAKE_SOURCE_DIR}/src/common/shellextensionutils.cpp
+ customstateprovider.cpp
CfApiShellIntegration.def
)
-target_link_libraries(CfApiShellExtensions shlwapi Gdiplus Nextcloud::csync Qt5::Core Qt5::Network)
+message("CUSTOM_STATE_ICON_LOCKED_OUT: ${CUSTOM_STATE_ICON_LOCKED_OUT}")
+message("CUSTOM_STATE_ICON_SHARED_OUT: ${CUSTOM_STATE_ICON_SHARED_OUT}")
+
+if (CUSTOM_STATE_ICON_LOCKED_OUT AND CUSTOM_STATE_ICON_SHARED_OUT)
+ message("Adding ${CMAKE_CURRENT_BINARY_DIR}/${CFAPI_SHELL_EXTENSIONS_LIB_NAME}.rc...")
+ target_sources(CfApiShellExtensions PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/${CFAPI_SHELL_EXTENSIONS_LIB_NAME}.rc")
+else()
+ message(WARNING "Could not add ${CMAKE_CURRENT_BINARY_DIR}/${CFAPI_SHELL_EXTENSIONS_LIB_NAME}.rc to CfApiShellExtensions. Custom states for Windows Virtual Files won't work.")
+endif()
+
+add_dependencies(CfApiShellExtensions CustomStateProviderImpl)
+
+target_link_libraries(CfApiShellExtensions shlwapi Gdiplus onecoreuap Nextcloud::csync Qt5::Core Qt5::Network)
+
+target_include_directories(CfApiShellExtensions PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_include_directories(CfApiShellExtensions PRIVATE ${GeneratedFilesPath})
target_include_directories(CfApiShellExtensions PRIVATE ${CMAKE_SOURCE_DIR})
+target_compile_features(CfApiShellExtensions PRIVATE cxx_std_17)
+
set_target_properties(CfApiShellExtensions
PROPERTIES
LIBRARY_OUTPUT_NAME
@@ -29,3 +208,5 @@ install(TARGETS CfApiShellExtensions
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_BINDIR}
)
+
+configure_file(${CMAKE_CURRENT_SOURCE_DIR}/configvfscfapishellext.h.in ${CMAKE_CURRENT_BINARY_DIR}/configvfscfapishellext.h)
diff --git a/src/libsync/vfs/cfapi/shellext/CustomStateProvider.idl b/src/libsync/vfs/cfapi/shellext/CustomStateProvider.idl
new file mode 100644
index 000000000..174b828a9
--- /dev/null
+++ b/src/libsync/vfs/cfapi/shellext/CustomStateProvider.idl
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+namespace CfApiShellExtensions
+{
+ runtimeclass CustomStateProvider : [default] Windows.Storage.Provider.IStorageProviderItemPropertySource
+ {
+ CustomStateProvider();
+ }
+}
diff --git a/src/libsync/vfs/cfapi/shellext/configvfscfapishellext.h.in b/src/libsync/vfs/cfapi/shellext/configvfscfapishellext.h.in
new file mode 100644
index 000000000..4f0b193a7
--- /dev/null
+++ b/src/libsync/vfs/cfapi/shellext/configvfscfapishellext.h.in
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+#ifndef CONFIG_VFS_CFAPI_SHELLEXT_H
+#define CONFIG_VFS_CFAPI_SHELLEXT_H
+#cmakedefine CUSTOM_STATE_ICON_LOCKED_INDEX "@CUSTOM_STATE_ICON_LOCKED_INDEX@"
+#cmakedefine CUSTOM_STATE_ICON_SHARED_INDEX "@CUSTOM_STATE_ICON_SHARED_INDEX@"
+#cmakedefine CUSTOM_STATE_ICON_INDEX_OFFSET "@CUSTOM_STATE_ICON_INDEX_OFFSET@"
+#endif
diff --git a/src/libsync/vfs/cfapi/shellext/customstateprovider.cpp b/src/libsync/vfs/cfapi/shellext/customstateprovider.cpp
new file mode 100644
index 000000000..8f7a2266f
--- /dev/null
+++ b/src/libsync/vfs/cfapi/shellext/customstateprovider.cpp
@@ -0,0 +1,104 @@
+/*
+ * 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 "customstateprovider.h"
+#include "customstateprovideripc.h"
+#include <Shlguid.h>
+
+extern long dllObjectsCount;
+
+namespace winrt::CfApiShellExtensions::implementation {
+
+CustomStateProvider::CustomStateProvider()
+{
+ InterlockedIncrement(&dllObjectsCount);
+}
+
+CustomStateProvider::~CustomStateProvider()
+{
+ InterlockedDecrement(&dllObjectsCount);
+}
+
+winrt::Windows::Foundation::Collections::IIterable<winrt::Windows::Storage::Provider::StorageProviderItemProperty>
+CustomStateProvider::GetItemProperties(hstring const &itemPath)
+{
+ std::vector<winrt::Windows::Storage::Provider::StorageProviderItemProperty> properties;
+
+ if (_dllFilePath.isEmpty()) {
+ return winrt::single_threaded_vector(std::move(properties));
+ }
+
+ const auto itemPathString = QString::fromStdString(winrt::to_string(itemPath));
+
+ const auto isItemPathValid = [&itemPathString]() {
+ if (itemPathString.isEmpty()) {
+ return false;
+ }
+
+ const auto itemPathSplit = itemPathString.split(QStringLiteral("\\"), Qt::SkipEmptyParts);
+
+ if (itemPathSplit.size() > 0) {
+ const auto itemName = itemPathSplit.last();
+ return !itemName.startsWith(QStringLiteral(".sync_")) && !itemName.startsWith(QStringLiteral(".owncloudsync.log"));
+ }
+
+ return true;
+ }();
+
+ if (!isItemPathValid) {
+ return winrt::single_threaded_vector(std::move(properties));
+ }
+
+ VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
+
+ const auto states = customStateProviderIpc.fetchCustomStatesForFile(itemPathString);
+
+ for (const auto &state : states) {
+ const auto stateValue = state.canConvert<int>() ? state.toInt() : -1;
+
+ if (stateValue >= 0) {
+ auto foundAvalability = _stateIconsAvailibility.constFind(stateValue);
+ if (foundAvalability == std::cend(_stateIconsAvailibility)) {
+ const auto hIcon = ExtractIcon(NULL, _dllFilePath.toStdWString().c_str(), stateValue);
+ _stateIconsAvailibility[stateValue] = hIcon != NULL;
+ if (hIcon) {
+ DestroyIcon(hIcon);
+ }
+ foundAvalability = _stateIconsAvailibility.constFind(stateValue);
+ }
+
+ if (!foundAvalability.value()) {
+ continue;
+ }
+
+ winrt::Windows::Storage::Provider::StorageProviderItemProperty itemProperty;
+ itemProperty.Id(stateValue);
+ itemProperty.Value(QString("Value%1").arg(stateValue).toStdWString());
+ itemProperty.IconResource(QString(_dllFilePath + QString(",%1").arg(QString::number(stateValue))).toStdWString());
+ properties.push_back(std::move(itemProperty));
+ }
+ }
+
+ return winrt::single_threaded_vector(std::move(properties));
+}
+void CustomStateProvider::setDllFilePath(LPCTSTR dllFilePath)
+{
+ _dllFilePath = QString::fromWCharArray(dllFilePath);
+ if (!_dllFilePath.endsWith(QStringLiteral(".dll"))) {
+ _dllFilePath.clear();
+ }
+}
+
+QString CustomStateProvider::_dllFilePath;
+}
diff --git a/src/libsync/vfs/cfapi/shellext/customstateprovider.h b/src/libsync/vfs/cfapi/shellext/customstateprovider.h
new file mode 100644
index 000000000..30c2045e9
--- /dev/null
+++ b/src/libsync/vfs/cfapi/shellext/customstateprovider.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#pragma once
+#include "Generated/CfApiShellExtensions/customstateprovider.g.h"
+#include "config.h"
+#include <winrt/windows.foundation.collections.h>
+#include <windows.storage.provider.h>
+#include <QString>
+#include <QMap>
+
+namespace winrt::CfApiShellExtensions::implementation {
+class __declspec(uuid(CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID)) CustomStateProvider
+ : public CustomStateProviderT<CustomStateProvider>
+{
+public:
+ CustomStateProvider();
+ virtual ~CustomStateProvider();
+ Windows::Foundation::Collections::IIterable<Windows::Storage::Provider::StorageProviderItemProperty>
+ GetItemProperties(_In_ hstring const &itemPath);
+
+ static void setDllFilePath(LPCTSTR dllFilePath);
+
+private:
+ static QString _dllFilePath;
+ static HINSTANCE _dllhInstance;
+ QMap<int, bool> _stateIconsAvailibility;
+};
+}
+
+namespace winrt::CfApiShellExtensions::factory_implementation {
+struct CustomStateProvider : CustomStateProviderT<CustomStateProvider, implementation::CustomStateProvider>
+{
+};
+}
diff --git a/src/libsync/vfs/cfapi/shellext/customstateprovideripc.cpp b/src/libsync/vfs/cfapi/shellext/customstateprovideripc.cpp
new file mode 100644
index 000000000..50939faec
--- /dev/null
+++ b/src/libsync/vfs/cfapi/shellext/customstateprovideripc.cpp
@@ -0,0 +1,104 @@
+/*
+ * 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 "customstateprovideripc.h"
+#include "common/shellextensionutils.h"
+#include "ipccommon.h"
+#include <QJsonDocument>
+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 {
+
+CustomStateProviderIpc::~CustomStateProviderIpc()
+{
+ disconnectSocketFromServer();
+}
+
+QVariantList CustomStateProviderIpc::fetchCustomStatesForFile(const QString &filePath)
+{
+ 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 {};
+ }
+
+ // #1 Connect to the local server
+ if (!connectSocketToServer(mainServerName)) {
+ return {};
+ }
+
+ auto messageRequestCustomStatesForFile = QVariantMap {
+ {
+ VfsShellExtensions::Protocol::CustomStateProviderRequestKey,
+ QVariantMap {
+ { VfsShellExtensions::Protocol::FilePathKey, filePath }
+ }
+ }
+ };
+
+ // #2 Request custom states for a 'filePath'
+ if (!sendMessageAndReadyRead(messageRequestCustomStatesForFile)) {
+ return {};
+ }
+
+ // #3 Receive custom states as JSON
+ const auto message = QJsonDocument::fromJson(_localSocket.readAll()).toVariant().toMap();
+ if (!VfsShellExtensions::Protocol::validateProtocolVersion(message) || !message.contains(VfsShellExtensions::Protocol::CustomStateDataKey)) {
+ return {};
+ }
+ const auto customStates = message.value(VfsShellExtensions::Protocol::CustomStateDataKey).toMap().value(VfsShellExtensions::Protocol::CustomStateStatesKey).toList();
+ disconnectSocketFromServer();
+
+ return customStates;
+}
+
+bool CustomStateProviderIpc::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 CustomStateProviderIpc::getServerNameForPath(const QString &filePath)
+{
+ if (!overrideServerName.isEmpty()) {
+ return overrideServerName;
+ }
+
+ return findServerNameForPath(filePath);
+}
+
+bool CustomStateProviderIpc::connectSocketToServer(const QString &serverName)
+{
+ if (!disconnectSocketFromServer()) {
+ return false;
+ }
+ _localSocket.setServerName(serverName);
+ _localSocket.connectToServer();
+ return _localSocket.state() == QLocalSocket::ConnectedState || _localSocket.waitForConnected(socketTimeoutMs);
+}
+QString CustomStateProviderIpc::overrideServerName = {};
+}
diff --git a/src/libsync/vfs/cfapi/shellext/customstateprovideripc.h b/src/libsync/vfs/cfapi/shellext/customstateprovideripc.h
new file mode 100644
index 000000000..bf21d91f7
--- /dev/null
+++ b/src/libsync/vfs/cfapi/shellext/customstateprovideripc.h
@@ -0,0 +1,43 @@
+/*
+ * 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 <QtNetwork/QLocalSocket>
+#include <QString>
+#include <QVariant>
+
+namespace VfsShellExtensions {
+class CustomStateProviderIpc
+{
+public:
+ CustomStateProviderIpc() = default;
+ ~CustomStateProviderIpc();
+
+ QVariantList fetchCustomStatesForFile(const QString &filePath);
+
+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:
+ QLocalSocket _localSocket;
+};
+}
diff --git a/src/libsync/vfs/cfapi/shellext/dllmain.cpp b/src/libsync/vfs/cfapi/shellext/dllmain.cpp
index c9edff1dd..07a8dea7f 100644
--- a/src/libsync/vfs/cfapi/shellext/dllmain.cpp
+++ b/src/libsync/vfs/cfapi/shellext/dllmain.cpp
@@ -13,16 +13,20 @@
*/
#include "cfapishellintegrationclassfactory.h"
+#include "customstateprovider.h"
#include "thumbnailprovider.h"
#include <comdef.h>
long dllReferenceCount = 0;
+long dllObjectsCount = 0;
HINSTANCE instanceHandle = NULL;
+HRESULT CustomStateProvider_CreateInstance(REFIID riid, void **ppv);
HRESULT ThumbnailProvider_CreateInstance(REFIID riid, void **ppv);
const VfsShellExtensions::ClassObjectInit listClassesSupported[] = {
+ {&__uuidof(winrt::CfApiShellExtensions::implementation::CustomStateProvider), CustomStateProvider_CreateInstance},
{&__uuidof(VfsShellExtensions::ThumbnailProvider), ThumbnailProvider_CreateInstance}
};
@@ -30,6 +34,9 @@ STDAPI_(BOOL) DllMain(HINSTANCE hInstance, DWORD dwReason, void *)
{
if (dwReason == DLL_PROCESS_ATTACH) {
instanceHandle = hInstance;
+ wchar_t dllFilePath[_MAX_PATH] = {0};
+ ::GetModuleFileName(instanceHandle, dllFilePath, _MAX_PATH);
+ winrt::CfApiShellExtensions::implementation::CustomStateProvider::setDllFilePath(dllFilePath);
DisableThreadLibraryCalls(hInstance);
}
@@ -38,7 +45,7 @@ STDAPI_(BOOL) DllMain(HINSTANCE hInstance, DWORD dwReason, void *)
STDAPI DllCanUnloadNow()
{
- return dllReferenceCount == 0 ? S_OK : S_FALSE;
+ return (dllReferenceCount == 0 && dllObjectsCount == 0) ? S_OK : S_FALSE;
}
STDAPI DllGetClassObject(REFCLSID clsid, REFIID riid, void **ppv)
@@ -46,6 +53,16 @@ STDAPI DllGetClassObject(REFCLSID clsid, REFIID riid, void **ppv)
return VfsShellExtensions::CfApiShellIntegrationClassFactory::CreateInstance(clsid, listClassesSupported, ARRAYSIZE(listClassesSupported), riid, ppv);
}
+HRESULT CustomStateProvider_CreateInstance(REFIID riid, void **ppv)
+{
+ try {
+ const auto customStateProvider = winrt::make_self<winrt::CfApiShellExtensions::implementation::CustomStateProvider>();
+ return customStateProvider->QueryInterface(riid, ppv);
+ } catch (_com_error exc) {
+ return exc.Error();
+ }
+}
+
HRESULT ThumbnailProvider_CreateInstance(REFIID riid, void **ppv)
{
auto *thumbnailProvider = new (std::nothrow) VfsShellExtensions::ThumbnailProvider();
diff --git a/src/libsync/vfs/cfapi/shellext/ipccommon.cpp b/src/libsync/vfs/cfapi/shellext/ipccommon.cpp
new file mode 100644
index 000000000..a86b06b9d
--- /dev/null
+++ b/src/libsync/vfs/cfapi/shellext/ipccommon.cpp
@@ -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.
+ */
+
+#include "ipccommon.h"
+#include "common/shellextensionutils.h"
+#include "common/utility.h"
+#include <QDir>
+
+namespace VfsShellExtensions {
+QString findServerNameForPath(const QString &filePath)
+{
+ // 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;
+}
+}
diff --git a/src/libsync/vfs/cfapi/shellext/ipccommon.h b/src/libsync/vfs/cfapi/shellext/ipccommon.h
new file mode 100644
index 000000000..9b78787e3
--- /dev/null
+++ b/src/libsync/vfs/cfapi/shellext/ipccommon.h
@@ -0,0 +1,21 @@
+/*
+ * 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 <QString>
+
+namespace VfsShellExtensions {
+QString findServerNameForPath(const QString &filePath);
+}
diff --git a/src/libsync/vfs/cfapi/shellext/thumbnailprovider.cpp b/src/libsync/vfs/cfapi/shellext/thumbnailprovider.cpp
index 18c0ba905..f3eb9de24 100644
--- a/src/libsync/vfs/cfapi/shellext/thumbnailprovider.cpp
+++ b/src/libsync/vfs/cfapi/shellext/thumbnailprovider.cpp
@@ -48,6 +48,8 @@
#include <shlwapi.h>
#include <QSize>
+extern long dllObjectsCount;
+
namespace VfsShellExtensions {
std::pair<HBITMAP, WTS_ALPHATYPE> hBitmapAndAlphaTypeFromData(const QByteArray &thumbnailData)
@@ -93,8 +95,13 @@ std::pair<HBITMAP, WTS_ALPHATYPE> hBitmapAndAlphaTypeFromData(const QByteArray &
ThumbnailProvider::ThumbnailProvider()
: _referenceCount(1)
{
+ InterlockedIncrement(&dllObjectsCount);
}
+ThumbnailProvider::~ThumbnailProvider()
+{
+ InterlockedDecrement(&dllObjectsCount);
+}
IFACEMETHODIMP ThumbnailProvider::QueryInterface(REFIID riid, void **ppv)
{
static const QITAB qit[] = {
diff --git a/src/libsync/vfs/cfapi/shellext/thumbnailprovider.h b/src/libsync/vfs/cfapi/shellext/thumbnailprovider.h
index 66256c3a0..3e5e7f85f 100644
--- a/src/libsync/vfs/cfapi/shellext/thumbnailprovider.h
+++ b/src/libsync/vfs/cfapi/shellext/thumbnailprovider.h
@@ -30,7 +30,7 @@ class __declspec(uuid(CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID)) ThumbnailProvi
public:
ThumbnailProvider();
- virtual ~ThumbnailProvider() = default;
+ virtual ~ThumbnailProvider();
IFACEMETHODIMP QueryInterface(REFIID riid, void **ppv);
diff --git a/src/libsync/vfs/cfapi/shellext/thumbnailprovideripc.cpp b/src/libsync/vfs/cfapi/shellext/thumbnailprovideripc.cpp
index 8a6d057b5..2ee66d81c 100644
--- a/src/libsync/vfs/cfapi/shellext/thumbnailprovideripc.cpp
+++ b/src/libsync/vfs/cfapi/shellext/thumbnailprovideripc.cpp
@@ -14,14 +14,11 @@
#include "thumbnailprovideripc.h"
#include "common/shellextensionutils.h"
-#include "common/utility.h"
+#include "ipccommon.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;
@@ -61,7 +58,7 @@ QByteArray ThumbnailProviderIpc::fetchThumbnailForFile(const QString &filePath,
{
VfsShellExtensions::Protocol::ThumbnailProviderRequestKey,
QVariantMap {
- {VfsShellExtensions::Protocol::ThumbnailProviderRequestFilePathKey, filePath},
+ {VfsShellExtensions::Protocol::FilePathKey, filePath},
{VfsShellExtensions::Protocol::ThumbnailProviderRequestFileSizeKey, QVariantMap{{QStringLiteral("width"), size.width()}, {QStringLiteral("height"), size.height()}}}
}
}
@@ -99,26 +96,8 @@ 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;
+ return findServerNameForPath(filePath);
}
bool ThumbnailProviderIpc::connectSocketToServer(const QString &serverName)
diff --git a/src/libsync/vfs/cfapi/vfs_cfapi.cpp b/src/libsync/vfs/cfapi/vfs_cfapi.cpp
index 3893f6393..fda83f77b 100644
--- a/src/libsync/vfs/cfapi/vfs_cfapi.cpp
+++ b/src/libsync/vfs/cfapi/vfs_cfapi.cpp
@@ -40,12 +40,16 @@ const auto rootKey = HKEY_CURRENT_USER;
bool registerShellExtension()
{
+ const QList<QPair<QString, QString>> listExtensions = {
+ {CFAPI_SHELLEXT_THUMBNAIL_HANDLER_DISPLAY_NAME, CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG},
+ {CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_DISPLAY_NAME, CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID_REG}
+ };
+ // assume CFAPI_SHELL_EXTENSIONS_LIB_NAME is always in the same folder as the main executable
// 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();
+ qCWarning(lcCfApi) << "Register CfAPI shell extensions failed. Dll does not exist in " << QCoreApplication::applicationDirPath();
return false;
}
@@ -57,20 +61,22 @@ bool registerShellExtension()
return false;
}
- const QString clsidPath = QString() % clsIdRegKey % CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG;
- const QString clsidServerPath = clsidPath % R"(\InprocServer32)";
+ for (const auto extension : listExtensions) {
+ const QString clsidPath = QString() % clsIdRegKey % extension.second;
+ 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;
+ if (!OCC::Utility::registrySetKeyValue(rootKey, clsidPath, QStringLiteral("AppID"), REG_SZ, CFAPI_SHELLEXT_APPID_REG)) {
+ return false;
+ }
+ if (!OCC::Utility::registrySetKeyValue(rootKey, clsidPath, {}, REG_SZ, extension.first)) {
+ 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;
@@ -83,9 +89,16 @@ void unregisterShellExtensions()
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);
+ const QStringList listExtensions = {
+ CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID_REG,
+ CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG
+ };
+
+ for (const auto extension : listExtensions) {
+ const QString clsidPath = QString() % clsIdRegKey % extension;
+ if (OCC::Utility::registryKeyExists(rootKey, clsidPath)) {
+ OCC::Utility::registryDeleteKeyTree(rootKey, clsidPath);
+ }
}
}