diff --git a/CMakeLists.txt b/CMakeLists.txt index a8d41aeea..23f292d44 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,10 @@ set( CFAPI_SHELL_EXTENSIONS_LIB_NAME CfApiShellExtensions ) set( CFAPI_SHELLEXT_APPID_REG "{E314A650-DCA4-416E-974E-18EA37C213EA}") set( CFAPI_SHELLEXT_APPID_DISPLAY_NAME "${APPLICATION_NAME} CfApi Shell Extensions" ) +set( CFAPI_SHELLEXT_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" ) diff --git a/admin/win/msi/RegistryCleanup.vbs.in b/admin/win/msi/RegistryCleanup.vbs.in index 35c3b7651..0eeb1a4ed 100644 --- a/admin/win/msi/RegistryCleanup.vbs.in +++ b/admin/win/msi/RegistryCleanup.vbs.in @@ -53,19 +53,25 @@ End Function 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() diff --git a/cmake/modules/ECMAddAppIcon.cmake b/cmake/modules/ECMAddAppIcon.cmake index 61dba6c42..bd27b0590 100644 --- a/cmake/modules/ECMAddAppIcon.cmake +++ b/cmake/modules/ECMAddAppIcon.cmake @@ -102,11 +102,13 @@ include(CMakeParseArguments) 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) @@ -211,15 +213,17 @@ function(ecm_add_app_icon appsources) 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) diff --git a/cmake/modules/GenerateIconsUtils.cmake b/cmake/modules/GenerateIconsUtils.cmake new file mode 100644 index 000000000..e37b04cfe --- /dev/null +++ b/cmake/modules/GenerateIconsUtils.cmake @@ -0,0 +1,58 @@ +# 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() diff --git a/config.h.in b/config.h.in index 9ea359863..1d821d2b4 100644 --- a/config.h.in +++ b/config.h.in @@ -48,6 +48,10 @@ #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@" 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 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 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 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> &items) + const QHash &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 -#include -#include +#include #include #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> _params; + QHash _params; QVector _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 ¶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(); @@ -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 ¶ms = {}); /** * 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 #include "folder.h" #include "folderman.h" +#include "ocssharejob.h" #include +#include #include +#include #include +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(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(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 #include +#include #include +#include +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 _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 &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 + * + * 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 + * + * 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 + * + * 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 + +extern long dllObjectsCount; + +namespace winrt::CfApiShellExtensions::implementation { + +CustomStateProvider::CustomStateProvider() +{ + InterlockedIncrement(&dllObjectsCount); +} + +CustomStateProvider::~CustomStateProvider() +{ + InterlockedDecrement(&dllObjectsCount); +} + +winrt::Windows::Foundation::Collections::IIterable +CustomStateProvider::GetItemProperties(hstring const &itemPath) +{ + std::vector 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() ? 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 + * + * 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 +#include +#include +#include + +namespace winrt::CfApiShellExtensions::implementation { +class __declspec(uuid(CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID)) CustomStateProvider + : public CustomStateProviderT +{ +public: + CustomStateProvider(); + virtual ~CustomStateProvider(); + Windows::Foundation::Collections::IIterable + GetItemProperties(_In_ hstring const &itemPath); + + static void setDllFilePath(LPCTSTR dllFilePath); + +private: + static QString _dllFilePath; + static HINSTANCE _dllhInstance; + QMap _stateIconsAvailibility; +}; +} + +namespace winrt::CfApiShellExtensions::factory_implementation { +struct CustomStateProvider : CustomStateProviderT +{ +}; +} 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 + * + * 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 +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 + * + * 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 +#include +#include + +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 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(); + 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 + * + * 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 + +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 + * + * 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 + +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 #include +extern long dllObjectsCount; + namespace VfsShellExtensions { std::pair hBitmapAndAlphaTypeFromData(const QByteArray &thumbnailData) @@ -93,8 +95,13 @@ std::pair 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 #include #include #include -#include -#include -#include 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> 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); + } } } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d7ff56515..c3fe37a99 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -76,7 +76,7 @@ if (WIN32) 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() diff --git a/test/testcfapishellextensionsipc.cpp b/test/testcfapishellextensionsipc.cpp index d9fe3cc0d..4aec97b57 100644 --- a/test/testcfapishellextensionsipc.cpp +++ b/test/testcfapishellextensionsipc.cpp @@ -5,22 +5,121 @@ * */ +#include +#include +#include +#include +#include +#include "config.h" +#include +#include +#include +#include +#include +#include "syncenginetestutils.h" +#include "testhelper.h" +#include +#include #include #include #include -#include "syncenginetestutils.h" -#include "common/vfs.h" -#include "common/shellextensionutils.h" -#include "config.h" -#include -#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; @@ -38,21 +137,39 @@ class TestCfApiShellExtensionsIPC : public QObject QScopedPointer _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 dummyImages; QString currentImage; + struct FileStates + { + bool _isShared = false; + bool _isLocked = false; + }; + + const QMap 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(); @@ -68,6 +185,16 @@ private slots: 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()}); @@ -86,31 +213,43 @@ private slots: 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 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 additionalHeaders = { + {QNetworkRequest::KnownHeaders::ContentTypeHeader, "image/jpeg"}}; + fakePayloadReply->_additionalHeaders = additionalHeaders; + + reply = fakePayloadReply; + } + } else { + reply = new FakePayloadReply(op, req, {}, nullptr); } return reply; @@ -126,6 +265,7 @@ private slots: folder->setVirtualFilesEnabled(true); + QVERIFY(fakeFolder.syncOnce()); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); ItemCompletedSpy completeSpy(fakeFolder); @@ -135,8 +275,6 @@ private slots: 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); } @@ -198,6 +336,137 @@ private slots: 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(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(); @@ -212,5 +481,7 @@ private slots: } }; +bool TestCfApiShellExtensionsIPC::replyWithNoShares = false; + QTEST_GUILESS_MAIN(TestCfApiShellExtensionsIPC) #include "testcfapishellextensionsipc.moc" diff --git a/theme/cfapishellext_custom_states/0-locked.svg b/theme/cfapishellext_custom_states/0-locked.svg new file mode 100644 index 000000000..e0722f326 --- /dev/null +++ b/theme/cfapishellext_custom_states/0-locked.svg @@ -0,0 +1 @@ + diff --git a/theme/cfapishellext_custom_states/1-shared.svg b/theme/cfapishellext_custom_states/1-shared.svg new file mode 100644 index 000000000..014392d5a --- /dev/null +++ b/theme/cfapishellext_custom_states/1-shared.svg @@ -0,0 +1 @@ + diff --git a/theme/cfapishellext_custom_states/1024-0-locked.png b/theme/cfapishellext_custom_states/1024-0-locked.png new file mode 100644 index 000000000..073bb7a20 Binary files /dev/null and b/theme/cfapishellext_custom_states/1024-0-locked.png differ diff --git a/theme/cfapishellext_custom_states/1024-1-shared.png b/theme/cfapishellext_custom_states/1024-1-shared.png new file mode 100644 index 000000000..637548aff Binary files /dev/null and b/theme/cfapishellext_custom_states/1024-1-shared.png differ diff --git a/theme/cfapishellext_custom_states/128-0-locked.png b/theme/cfapishellext_custom_states/128-0-locked.png new file mode 100644 index 000000000..3297f9206 Binary files /dev/null and b/theme/cfapishellext_custom_states/128-0-locked.png differ diff --git a/theme/cfapishellext_custom_states/128-1-shared.png b/theme/cfapishellext_custom_states/128-1-shared.png new file mode 100644 index 000000000..4d4f7561e Binary files /dev/null and b/theme/cfapishellext_custom_states/128-1-shared.png differ diff --git a/theme/cfapishellext_custom_states/24-0-locked.png b/theme/cfapishellext_custom_states/24-0-locked.png new file mode 100644 index 000000000..d38071689 Binary files /dev/null and b/theme/cfapishellext_custom_states/24-0-locked.png differ diff --git a/theme/cfapishellext_custom_states/24-1-shared.png b/theme/cfapishellext_custom_states/24-1-shared.png new file mode 100644 index 000000000..ebeea1710 Binary files /dev/null and b/theme/cfapishellext_custom_states/24-1-shared.png differ diff --git a/theme/cfapishellext_custom_states/256-0-locked.png b/theme/cfapishellext_custom_states/256-0-locked.png new file mode 100644 index 000000000..4d24d7478 Binary files /dev/null and b/theme/cfapishellext_custom_states/256-0-locked.png differ diff --git a/theme/cfapishellext_custom_states/256-1-shared.png b/theme/cfapishellext_custom_states/256-1-shared.png new file mode 100644 index 000000000..8dc623e4e Binary files /dev/null and b/theme/cfapishellext_custom_states/256-1-shared.png differ diff --git a/theme/cfapishellext_custom_states/32-0-locked.png b/theme/cfapishellext_custom_states/32-0-locked.png new file mode 100644 index 000000000..2effbd667 Binary files /dev/null and b/theme/cfapishellext_custom_states/32-0-locked.png differ diff --git a/theme/cfapishellext_custom_states/32-1-shared.png b/theme/cfapishellext_custom_states/32-1-shared.png new file mode 100644 index 000000000..835bebd7d Binary files /dev/null and b/theme/cfapishellext_custom_states/32-1-shared.png differ diff --git a/theme/cfapishellext_custom_states/40-0-locked.png b/theme/cfapishellext_custom_states/40-0-locked.png new file mode 100644 index 000000000..93cdd5e2f Binary files /dev/null and b/theme/cfapishellext_custom_states/40-0-locked.png differ diff --git a/theme/cfapishellext_custom_states/40-1-shared.png b/theme/cfapishellext_custom_states/40-1-shared.png new file mode 100644 index 000000000..1485ad363 Binary files /dev/null and b/theme/cfapishellext_custom_states/40-1-shared.png differ diff --git a/theme/cfapishellext_custom_states/48-0-locked.png b/theme/cfapishellext_custom_states/48-0-locked.png new file mode 100644 index 000000000..3b3e1f200 Binary files /dev/null and b/theme/cfapishellext_custom_states/48-0-locked.png differ diff --git a/theme/cfapishellext_custom_states/48-1-shared.png b/theme/cfapishellext_custom_states/48-1-shared.png new file mode 100644 index 000000000..f58d721e4 Binary files /dev/null and b/theme/cfapishellext_custom_states/48-1-shared.png differ diff --git a/theme/cfapishellext_custom_states/512-0-locked.png b/theme/cfapishellext_custom_states/512-0-locked.png new file mode 100644 index 000000000..4da18008d Binary files /dev/null and b/theme/cfapishellext_custom_states/512-0-locked.png differ diff --git a/theme/cfapishellext_custom_states/512-1-shared.png b/theme/cfapishellext_custom_states/512-1-shared.png new file mode 100644 index 000000000..ad006e238 Binary files /dev/null and b/theme/cfapishellext_custom_states/512-1-shared.png differ diff --git a/theme/cfapishellext_custom_states/64-0-locked.png b/theme/cfapishellext_custom_states/64-0-locked.png new file mode 100644 index 000000000..5cb27a958 Binary files /dev/null and b/theme/cfapishellext_custom_states/64-0-locked.png differ diff --git a/theme/cfapishellext_custom_states/64-1-shared.png b/theme/cfapishellext_custom_states/64-1-shared.png new file mode 100644 index 000000000..a840bd1bb Binary files /dev/null and b/theme/cfapishellext_custom_states/64-1-shared.png differ