set( CFAPI_SHELLEXT_APPID_REG "{E314A650-DCA4-416E-974E-18EA37C213EA}")
set( CFAPI_SHELLEXT_APPID_DISPLAY_NAME "${APPLICATION_NAME} CfApi Shell Extensions" )
+set( CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID "1E62D59A-6EA4-476C-B707-4A32E88ED822" )
+set( CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID_REG "{${CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID}}" )
+set( CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_DISPLAY_NAME "${APPLICATION_NAME} Custom State Handler" )
+
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" )
Function RegistryCleanupCfApiShellExtensions()
Set objRegistry = GetObject(strObjRegistry)
- strShellExtThumbnailHandlerAppId = "Software\Classes\AppID\@CFAPI_SHELLEXT_APPID_REG@"
+ strShellExtAppId = "Software\Classes\AppID\@CFAPI_SHELLEXT_APPID_REG@"
+
strShellExtThumbnailHandlerClsId = "Software\Classes\CLSID\@CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG@"
+ strShellExtCustomStateHandlerClsId = "Software\Classes\CLSID\@CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID_REG@"
rootKey = HKEY_CURRENT_USER
- If objRegistry.EnumKey(rootKey, strShellExtThumbnailHandlerAppId, arrSubKeys) = 0 Then
- RegistryDeleteKeyRecursive rootKey, strShellExtThumbnailHandlerAppId
+ If objRegistry.EnumKey(rootKey, strShellExtAppId, arrSubKeys) = 0 Then
+ RegistryDeleteKeyRecursive rootKey, strShellExtAppId
End If
If objRegistry.EnumKey(rootKey, strShellExtThumbnailHandlerClsId, arrSubKeys) = 0 Then
RegistryDeleteKeyRecursive rootKey, strShellExtThumbnailHandlerClsId
End If
+ If objRegistry.EnumKey(rootKey, strShellExtCustomStateHandlerClsId, arrSubKeys) = 0 Then
+ RegistryDeleteKeyRecursive rootKey, strShellExtCustomStateHandlerClsId
+ End If
+
End Function
Function RegistryCleanup()
function(ecm_add_app_icon appsources)
set(options)
- set(oneValueArgs OUTFILE_BASENAME ICON_INDEX)
+ set(oneValueArgs OUTFILE_BASENAME ICON_INDEX DO_NOT_GENERATE_RC_FILE)
set(multiValueArgs ICONS SIDEBAR_ICONS RC_DEPENDENCIES)
cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
- if (NOT ARG_ICON_INDEX)
- set(ARG_ICON_INDEX 1)
+ if (ARG_DO_NOT_GENERATE_RC_FILE)
+ set (_do_not_generate_rc_file TRUE)
+ else()
+ set (_do_not_generate_rc_file FALSE)
endif()
if(NOT ARG_ICONS)
DEPENDS ${deps}
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
)
- # this bit's a little hacky to make the dependency stuff work
- file(WRITE "${_outfilename}.rc.in" "IDI_ICON${ARG_ICON_INDEX} ICON DISCARDABLE \"${_outfilename}.ico\"\n")
- add_custom_command(
- OUTPUT "${_outfilename}.rc"
- COMMAND ${CMAKE_COMMAND}
- ARGS -E copy "${_outfilename}.rc.in" "${_outfilename}.rc"
- DEPENDS ${ARG_RC_DEPENDENCIES} "${_outfilename}.ico"
- WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
- )
+ if (NOT _do_not_generate_rc_file)
+ # this bit's a little hacky to make the dependency stuff work
+ file(WRITE "${_outfilename}.rc.in" "IDI_ICON${ARG_ICON_INDEX} ICON DISCARDABLE \"${_outfilename}.ico\"\n")
+ add_custom_command(
+ OUTPUT "${_outfilename}.rc"
+ COMMAND ${CMAKE_COMMAND}
+ ARGS -E copy "${_outfilename}.rc.in" "${_outfilename}.rc"
+ DEPENDS ${ARG_RC_DEPENDENCIES} "${_outfilename}.ico"
+ WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
+ )
+ endif()
endfunction()
if (IcoTool_FOUND)
--- /dev/null
+# 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)
+
+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()
#cmakedefine CFAPI_SHELLEXT_APPID_REG "@CFAPI_SHELLEXT_APPID_REG@"
#cmakedefine CFAPI_SHELLEXT_APPID_DISPLAY_NAME "@CFAPI_SHELLEXT_APPID_DISPLAY_NAME@"
+#cmakedefine CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID "@CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID@"
+#cmakedefine CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID_REG "@CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID_REG@"
+#cmakedefine CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_DISPLAY_NAME "@CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_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@"
if (!valid) {
qCWarning(lcShellExtensionUtils) << "Invalid shell extensions IPC protocol: " << message.value(QStringLiteral("version")) << " vs " << Version;
}
- Q_ASSERT(valid);
return valid;
}
}
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);
#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"
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)
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())
}
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()) {
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();
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();
QByteArray _e2eMangledName;
bool _isE2eEncrypted = false;
SyncJournalFileLockInfo _lockstate;
+ bool _isShared = false;
+ qint64 _lastShareStateFetchedTimestmap = 0;
};
bool OCSYNC_EXPORT
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.
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})
void OcsJob::addParam(const QString &name, const QString &value)
{
- _params.append(qMakePair(name, value));
+ _params.insert(name, value);
}
void OcsJob::addPassStatusCode(int code)
_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;
}
} 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);
}
#include "abstractnetworkjob.h"
#include <QVector>
-#include <QList>
-#include <QPair>
+#include <QHash>
#include <QUrl>
#define OCS_SUCCESS_STATUS_CODE 100
*/
void addRawHeader(const QByteArray &headerName, const QByteArray &value);
+ [[nodiscard]] QString getParamValue(const QString &key) const;
+
protected slots:
private:
QByteArray _verb;
- QList<QPair<QString, QString>> _params;
+ QHash<QString, QString> _params;
QVector<int> _passStatusCodes;
QNetworkRequest _request;
};
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> ¶ms)
{
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();
{
emit shareJobFinished(reply, _value);
}
+
+QString const OcsShareJob::_pathForSharesRequest = QStringLiteral("ocs/v2.php/apps/files_sharing/api/v1/shares");
}
*
* @param path Path to request shares for (default all shares)
*/
- void getShares(const QString &path = "");
+ void getShares(const QString &path = "", const QMap<QString, QString> ¶ms = {});
/**
* Delete the current Share
*/
void getSharedWithMe();
+ static const QString _pathForSharesRequest;
+
signals:
/**
* Result of the OCS request
#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));
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()) {
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) {
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()) {
#include <QObject>
#include <QLocalServer>
+#include <QMutex>
#include <QSize>
+#include <QVariant>
+class QJsonDocument;
class QLocalSocket;
namespace OCC {
[[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
}
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
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.
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;
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;
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()) {
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;
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) {
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();
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();
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;
}
QString _lockEditorApp;
qint64 _lockTime = 0;
qint64 _lockTimeout = 0;
+
+ bool _isShared = false;
+ time_t _lastShareStateFetchedTimestmap = 0;
};
inline bool operator<(const SyncFileItemPtr &item1, const SyncFileItemPtr &item2)
{ 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)}
};
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!";
}
+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
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)
--- /dev/null
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+namespace CfApiShellExtensions
+{
+ runtimeclass CustomStateProvider : [default] Windows.Storage.Provider.IStorageProviderItemPropertySource
+ {
+ CustomStateProvider();
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#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
--- /dev/null
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#include "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;
+}
--- /dev/null
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#pragma once
+#include "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>
+{
+};
+}
--- /dev/null
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#include "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 = {};
+}
--- /dev/null
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#pragma once
+
+#include <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;
+};
+}
*/
#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}
};
{
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);
}
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)
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();
--- /dev/null
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#include "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;
+}
+}
--- /dev/null
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#pragma once
+
+#include <QString>
+
+namespace VfsShellExtensions {
+QString findServerNameForPath(const QString &filePath);
+}
#include <shlwapi.h>
#include <QSize>
+extern long dllObjectsCount;
+
namespace VfsShellExtensions {
std::pair<HBITMAP, WTS_ALPHATYPE> hBitmapAndAlphaTypeFromData(const QByteArray &thumbnailData)
ThumbnailProvider::ThumbnailProvider()
: _referenceCount(1)
{
+ InterlockedIncrement(&dllObjectsCount);
}
+ThumbnailProvider::~ThumbnailProvider()
+{
+ InterlockedDecrement(&dllObjectsCount);
+}
IFACEMETHODIMP ThumbnailProvider::QueryInterface(REFIID riid, void **ppv)
{
static const QITAB qit[] = {
public:
ThumbnailProvider();
- virtual ~ThumbnailProvider() = default;
+ virtual ~ThumbnailProvider();
IFACEMETHODIMP QueryInterface(REFIID riid, void **ppv);
#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;
{
VfsShellExtensions::Protocol::ThumbnailProviderRequestKey,
QVariantMap {
- {VfsShellExtensions::Protocol::ThumbnailProviderRequestFilePathKey, filePath},
+ {VfsShellExtensions::Protocol::FilePathKey, filePath},
{VfsShellExtensions::Protocol::ThumbnailProviderRequestFileSizeKey, QVariantMap{{QStringLiteral("width"), size.width()}, {QStringLiteral("height"), size.height()}}}
}
}
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)
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;
}
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;
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);
+ }
}
}
nextcloud_add_test(SyncCfApi)
nextcloud_add_test(CfApiShellExtensionsIPC)
- target_sources(CfApiShellExtensionsIPCTest PRIVATE "${CMAKE_SOURCE_DIR}/src/libsync/vfs/cfapi/shellext/thumbnailprovideripc.cpp")
+ target_sources(CfApiShellExtensionsIPCTest PRIVATE "${CMAKE_SOURCE_DIR}/src/libsync/vfs/cfapi/shellext/thumbnailprovideripc.cpp" "${CMAKE_SOURCE_DIR}/src/libsync/vfs/cfapi/shellext/customstateprovideripc.cpp" "${CMAKE_SOURCE_DIR}/src/libsync/vfs/cfapi/shellext/ipccommon.cpp")
elseif(LINUX) # elseif(LINUX OR APPLE)
nextcloud_add_test(SyncXAttr)
endif()
*
*/
+#include <account.h>
+#include <accountstate.h>
+#include <accountmanager.h>
+#include <common/vfs.h>
+#include <common/shellextensionutils.h>
+#include "config.h"
+#include <folderman.h>
+#include <libsync/vfs/cfapi/shellext/configvfscfapishellext.h>
+#include <ocssharejob.h>
+#include <shellextensionsserver.h>
+#include <syncengine.h>
+#include "syncenginetestutils.h"
+#include "testhelper.h"
+#include <vfs/cfapi/shellext/customstateprovideripc.h>
+#include <vfs/cfapi/shellext/thumbnailprovideripc.h>
#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"
+namespace {
+static constexpr auto roootFolderName = "A";
+static constexpr auto imagesFolderName = "photos";
+static constexpr auto filesFolderName = "files";
+
+static const QByteArray fakeNoSharesResponse = R"({"ocs":{"data":[],"meta":{"message":"OK","status":"ok","statuscode":200}}})";
+
+static const QByteArray fakeSharedFilesResponse = R"({"ocs":{"data":[{
+ "attributes": null,
+ "can_delete": true,
+ "can_edit": true,
+ "displayname_file_owner": "admin",
+ "displayname_owner": "admin",
+ "expiration": null,
+ "file_parent": 2981,
+ "file_source": 3538,
+ "file_target": "/test_shared_file.txt",
+ "has_preview": true,
+ "hide_download": 0,
+ "id": "36",
+ "item_source": 3538,
+ "item_type": "file",
+ "label": null,
+ "mail_send": 0,
+ "mimetype": "text/plain",
+ "note": "",
+ "parent": null,
+ "path": "A/files/test_shared_file.txt",
+ "permissions": 19,
+ "share_type": 0,
+ "share_with": "newstandard",
+ "share_with_displayname": "newstandard",
+ "share_with_displayname_unique": "newstandard",
+ "status": {
+ "clearAt": null,
+ "icon": null,
+ "message": null,
+ "status": "offline"
+ },
+ "stime": 1662995777,
+ "storage": 2,
+ "storage_id": "home::admin",
+ "token": null,
+ "uid_file_owner": "admin",
+ "uid_owner": "admin"
+ },
+ {
+ "attributes": null,
+ "can_delete": true,
+ "can_edit": true,
+ "displayname_file_owner": "admin",
+ "displayname_owner": "admin",
+ "expiration": null,
+ "file_parent": 2981,
+ "file_source": 3538,
+ "file_target": "/test_shared_and_locked_file.txt",
+ "has_preview": true,
+ "hide_download": 0,
+ "id": "36",
+ "item_source": 3538,
+ "item_type": "file",
+ "label": null,
+ "mail_send": 0,
+ "mimetype": "text/plain",
+ "note": "",
+ "parent": null,
+ "path": "A/files/test_shared_and_locked_file.txt",
+ "permissions": 19,
+ "share_type": 0,
+ "share_with": "newstandard",
+ "share_with_displayname": "newstandard",
+ "share_with_displayname_unique": "newstandard",
+ "status": {
+ "clearAt": null,
+ "icon": null,
+ "message": null,
+ "status": "offline"
+ },
+ "stime": 1662995777,
+ "storage": 2,
+ "storage_id": "home::admin",
+ "token": null,
+ "uid_file_owner": "admin",
+ "uid_owner": "admin"
+ }
+ ],
+ "meta": {
+ "message": "OK",
+ "status": "ok",
+ "statuscode": 200
+ }
+ }
+})";
+
+static constexpr auto shellExtensionServerOverrideIntervalMs = 1000LL * 2LL;
+}
using namespace OCC;
QScopedPointer<ShellExtensionsServer> _shellExtensionsServer;
- QStringList dummmyImageNames = {
- "A/photos/imageJpg.jpg",
- "A/photos/imagePng.png",
- "A/photos/imagePng.bmp",
+ const QStringList dummmyImageNames = {
+ { QString(QString(roootFolderName) + QLatin1Char('/') + QString(imagesFolderName) + QLatin1Char('/') + QStringLiteral("imageJpg.jpg")) },
+ { QString(QString(roootFolderName) + QLatin1Char('/') + QString(imagesFolderName) + QLatin1Char('/') + QStringLiteral("imagePng.png")) },
+ { QString(QString(roootFolderName) + QLatin1Char('/') + QString(imagesFolderName) + QLatin1Char('/') + QStringLiteral("imagePng.bmp")) }
};
QMap<QString, QByteArray> dummyImages;
QString currentImage;
+ struct FileStates
+ {
+ bool _isShared = false;
+ bool _isLocked = false;
+ };
+
+ const QMap<QString, FileStates> dummyFileStates = {
+ { QString(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName) + QLatin1Char('/') + QStringLiteral("test_locked_file.txt")), { false, true } },
+ { QString(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName) + QLatin1Char('/') + QStringLiteral("test_shared_file.txt")), { true, false } },
+ { QString(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName) + QLatin1Char('/') + QStringLiteral("test_shared_and_locked_file.txt")), { true, true }},
+ { QString(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName) + QLatin1Char('/') + QStringLiteral("test_non_shared_and_non_locked_file.txt")), { false, false }}
+ };
+
+public:
+ static bool replyWithNoShares;
+
private slots:
void initTestCase()
{
VfsShellExtensions::ThumbnailProviderIpc::overrideServerName = VfsShellExtensions::serverNameForApplicationNameDefault();
+ VfsShellExtensions::CustomStateProviderIpc::overrideServerName = VfsShellExtensions::serverNameForApplicationNameDefault();
_shellExtensionsServer.reset(new ShellExtensionsServer);
+ _shellExtensionsServer->setIsSharedInvalidationInterval(shellExtensionServerOverrideIntervalMs);
for (const auto &dummyImageName : dummmyImageNames) {
const auto extension = dummyImageName.split(".").last();
dummyImages.insert(dummyImageName, byteArray);
}
+ fakeFolder.remoteModifier().mkdir(roootFolderName);
+
+ fakeFolder.remoteModifier().mkdir(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName));
+
+ fakeFolder.remoteModifier().mkdir(QString(roootFolderName) + QLatin1Char('/') + QString(imagesFolderName));
+
+ for (const auto &fileStateKey : dummyFileStates.keys()) {
+ fakeFolder.remoteModifier().insert(fileStateKey, 256);
+ }
+
fakeQnam.reset(new FakeQNAM({}));
account = OCC::Account::create();
account->setCredentials(new FakeCredentials{fakeQnam.data()});
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);
-
+ if (path.endsWith(OCC::OcsShareJob::_pathForSharesRequest)) {
+ const auto jsonReply = TestCfApiShellExtensionsIPC::replyWithNoShares ? fakeNoSharesResponse : fakeSharedFilesResponse;
+ TestCfApiShellExtensionsIPC::replyWithNoShares = false;
+ auto fakePayloadReply = new FakePayloadReply(op, req, jsonReply, nullptr);
QMap<QNetworkRequest::KnownHeaders, QByteArray> additionalHeaders = {
- {QNetworkRequest::KnownHeaders::ContentTypeHeader, "image/jpeg"}};
+ {QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"}};
fakePayloadReply->_additionalHeaders = additionalHeaders;
-
reply = fakePayloadReply;
+ } else if (path.endsWith(ShellExtensionsServer::getFetchThumbnailPath())) {
+ 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();
+ 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;
+ }
+ } else {
+ reply = new FakePayloadReply(op, req, {}, nullptr);
}
return reply;
folder->setVirtualFilesEnabled(true);
+ QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
ItemCompletedSpy completeSpy(fakeFolder);
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(thumbnailReplyData.isEmpty());
}
+ void testRequestCustomStates()
+ {
+ FolderMan *folderman = FolderMan::instance();
+ QVERIFY(folderman);
+ auto folder = FolderMan::instance()->folderForPath(fakeFolder.localPath());
+ QVERIFY(folder);
+
+ folder->setVirtualFilesEnabled(true);
+
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+ // 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 (auto it = std::begin(dummyFileStates); it != std::end(dummyFileStates); ++it) {
+ if (fakeFolder.syncJournal().getFileRecord(it.key(), &record)) {
+ record._isShared = it.value()._isShared;
+ if (record._isShared) {
+ record._remotePerm.setPermission(OCC::RemotePermissions::Permissions::IsShared);
+ }
+ record._lockstate._locked = it.value()._isLocked;
+ if (record._lockstate._locked) {
+ record._lockstate._lockOwnerId = "admin@example.cloud.com";
+ record._lockstate._lockOwnerDisplayName = "Admin";
+ record._lockstate._lockOwnerType = static_cast<int>(SyncFileItem::LockOwnerType::UserLock);
+ record._lockstate._lockTime = QDateTime::currentMSecsSinceEpoch();
+ record._lockstate._lockTimeout = 1000 * 60 * 60;
+ }
+ QVERIFY(fakeFolder.syncJournal().setFileRecord(record));
+ QVERIFY(realFolder->journalDb()->setFileRecord(record));
+ }
+ }
+
+ // #1 Test every file's states fetching. Everything must succeed.
+ for (auto it = std::cbegin(dummyFileStates); it != std::cend(dummyFileStates); ++it) {
+ QEventLoop loop;
+ QVariantList customStates;
+ std::thread t([&] {
+ VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
+ customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + it.key());
+ QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
+ });
+ loop.exec();
+ t.detach();
+ QVERIFY(!customStates.isEmpty() || (!it.value()._isLocked && !it.value()._isShared));
+ }
+
+ // #2 Test wrong file's states fetching. It must fail.
+ QEventLoop loop;
+ QVariantList customStates;
+ std::thread t1([&] {
+ VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
+ customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + QStringLiteral("A/files/wrong.jpg"));
+ QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
+ });
+ loop.exec();
+ t1.detach();
+ QVERIFY(customStates.isEmpty());
+
+ // #3 Test wrong file states fetching. It must fail.
+ customStates.clear();
+ std::thread t2([&] {
+ VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
+ customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + QStringLiteral("A/files/test_non_shared_and_non_locked_file.txt"));
+ QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
+ });
+ loop.exec();
+ t2.detach();
+ QVERIFY(customStates.isEmpty());
+
+ // reset all share states to make sure we'll get new states when fetching
+ for (auto it = std::begin(dummyFileStates); it != std::end(dummyFileStates); ++it) {
+ if (fakeFolder.syncJournal().getFileRecord(it.key(), &record)) {
+ record._remotePerm.unsetPermission(OCC::RemotePermissions::Permissions::IsShared);
+ record._isShared = false;
+ QVERIFY(fakeFolder.syncJournal().setFileRecord(record));
+ QVERIFY(realFolder->journalDb()->setFileRecord(record));
+ }
+ }
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ //
+
+ // wait enough time to make shares' state invalid
+ QTest::qWait(shellExtensionServerOverrideIntervalMs + 1000);
+
+ // #4 Test every file's states fetching. Everything must succeed.
+ for (auto it = std::cbegin(dummyFileStates); it != std::cend(dummyFileStates); ++it) {
+ QEventLoop loop;
+ QVariantList customStates;
+ std::thread t([&] {
+ VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
+ customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + it.key());
+ QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
+ });
+ loop.exec();
+ t.detach();
+ QVERIFY(!customStates.isEmpty() || (!it.value()._isLocked && !it.value()._isShared));
+
+ if (!customStates.isEmpty()) {
+ const auto lockedIndex = QString(CUSTOM_STATE_ICON_LOCKED_INDEX).toInt() - QString(CUSTOM_STATE_ICON_INDEX_OFFSET).toInt();
+ const auto sharedIndex = QString(CUSTOM_STATE_ICON_SHARED_INDEX).toInt() - QString(CUSTOM_STATE_ICON_INDEX_OFFSET).toInt();
+
+ if (customStates.contains(lockedIndex) && customStates.contains(sharedIndex)) {
+ QVERIFY(it.value()._isLocked && it.value()._isShared);
+ }
+ if (customStates.contains(lockedIndex)) {
+ QVERIFY(it.value()._isLocked);
+ }
+ if (customStates.contains(sharedIndex)) {
+ QVERIFY(it.value()._isShared);
+ }
+ }
+ }
+
+ // #5 Test no shares response for a file
+ QTest::qWait(shellExtensionServerOverrideIntervalMs + 1000);
+ TestCfApiShellExtensionsIPC::replyWithNoShares = true;
+ customStates.clear();
+ std::thread t3([&] {
+ VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
+ customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + QStringLiteral("A/files/test_non_shared_and_non_locked_file.txt"));
+ QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
+ });
+ loop.exec();
+ t3.detach();
+ QVERIFY(customStates.isEmpty());
+ }
+
void cleanupTestCase()
{
VfsShellExtensions::ThumbnailProviderIpc::overrideServerName.clear();
}
};
+bool TestCfApiShellExtensionsIPC::replyWithNoShares = false;
+
QTEST_GUILESS_MAIN(TestCfApiShellExtensionsIPC)
#include "testcfapishellextensionsipc.moc"
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 16 16" width="16" height="16"><path d="m8 1c-2.319 0-3.967 1.8644-4 4v2.5h-1.5v7.5h11v-7.5h-1.5v-2.5c0-2.27-1.8-3.9735-4-4zm0 2c1.25 0 2 0.963 2 2v2.5h-4v-2.5c0-1.174 0.747-2 2-2z" fill="#000"/></svg>
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" viewBox="0 0 16 16"><circle cx="3.5" cy="8" r="2.5"/><circle cy="12.5" cx="12.5" r="2.5"/><circle cx="12.5" cy="3.5" r="2.5"/><path d="m3.5 8 9 4.5m-9-4.5 9-4.5" stroke="#000" stroke-width="2" fill="none"/></svg>