瀏覽代碼

Merge pull request #4942 from nextcloud/feature/vfs-windows-sharing-and-lock-state

Feature/vfs windows sharing and lock state
allexzander 3 年之前
父節點
當前提交
de27a2ffd7
共有 59 個文件被更改,包括 1404 次插入186 次删除
  1. 4 0
      CMakeLists.txt
  2. 9 3
      admin/win/msi/RegistryCleanup.vbs.in
  3. 16 12
      cmake/modules/ECMAddAppIcon.cmake
  4. 58 0
      cmake/modules/GenerateIconsUtils.cmake
  5. 4 0
      config.h.in
  6. 0 1
      src/common/shellextensionutils.cpp
  7. 5 2
      src/common/shellextensionutils.h
  8. 18 8
      src/common/syncjournaldb.cpp
  9. 2 0
      src/common/syncjournalfilerecord.h
  10. 1 60
      src/gui/CMakeLists.txt
  11. 13 8
      src/gui/ocsjob.cpp
  12. 4 3
      src/gui/ocsjob.h
  13. 9 2
      src/gui/ocssharejob.cpp
  14. 3 1
      src/gui/ocssharejob.h
  15. 268 2
      src/gui/shellextensionsserver.cpp
  16. 27 0
      src/gui/shellextensionsserver.h
  17. 3 0
      src/gui/socketapi/socketapi.cpp
  18. 2 0
      src/libsync/bulkpropagatorjob.cpp
  19. 10 0
      src/libsync/discovery.cpp
  20. 2 0
      src/libsync/propagateremotemkdir.cpp
  21. 4 0
      src/libsync/syncfileitem.cpp
  22. 3 0
      src/libsync/syncfileitem.h
  23. 3 1
      src/libsync/vfs/cfapi/cfapiwrapper.cpp
  24. 155 0
      src/libsync/vfs/cfapi/shellext/CMakeLists.txt
  25. 21 0
      src/libsync/vfs/cfapi/shellext/CustomStateProvider.idl
  26. 20 0
      src/libsync/vfs/cfapi/shellext/configvfscfapishellext.h.in
  27. 104 0
      src/libsync/vfs/cfapi/shellext/customstateprovider.cpp
  28. 46 0
      src/libsync/vfs/cfapi/shellext/customstateprovider.h
  29. 104 0
      src/libsync/vfs/cfapi/shellext/customstateprovideripc.cpp
  30. 43 0
      src/libsync/vfs/cfapi/shellext/customstateprovideripc.h
  31. 18 1
      src/libsync/vfs/cfapi/shellext/dllmain.cpp
  32. 50 0
      src/libsync/vfs/cfapi/shellext/ipccommon.cpp
  33. 21 0
      src/libsync/vfs/cfapi/shellext/ipccommon.h
  34. 7 0
      src/libsync/vfs/cfapi/shellext/thumbnailprovider.cpp
  35. 1 1
      src/libsync/vfs/cfapi/shellext/thumbnailprovider.h
  36. 3 24
      src/libsync/vfs/cfapi/shellext/thumbnailprovideripc.cpp
  37. 31 18
      src/libsync/vfs/cfapi/vfs_cfapi.cpp
  38. 1 1
      test/CMakeLists.txt
  39. 309 38
      test/testcfapishellextensionsipc.cpp
  40. 1 0
      theme/cfapishellext_custom_states/0-locked.svg
  41. 1 0
      theme/cfapishellext_custom_states/1-shared.svg
  42. 二進制
      theme/cfapishellext_custom_states/1024-0-locked.png
  43. 二進制
      theme/cfapishellext_custom_states/1024-1-shared.png
  44. 二進制
      theme/cfapishellext_custom_states/128-0-locked.png
  45. 二進制
      theme/cfapishellext_custom_states/128-1-shared.png
  46. 二進制
      theme/cfapishellext_custom_states/24-0-locked.png
  47. 二進制
      theme/cfapishellext_custom_states/24-1-shared.png
  48. 二進制
      theme/cfapishellext_custom_states/256-0-locked.png
  49. 二進制
      theme/cfapishellext_custom_states/256-1-shared.png
  50. 二進制
      theme/cfapishellext_custom_states/32-0-locked.png
  51. 二進制
      theme/cfapishellext_custom_states/32-1-shared.png
  52. 二進制
      theme/cfapishellext_custom_states/40-0-locked.png
  53. 二進制
      theme/cfapishellext_custom_states/40-1-shared.png
  54. 二進制
      theme/cfapishellext_custom_states/48-0-locked.png
  55. 二進制
      theme/cfapishellext_custom_states/48-1-shared.png
  56. 二進制
      theme/cfapishellext_custom_states/512-0-locked.png
  57. 二進制
      theme/cfapishellext_custom_states/512-1-shared.png
  58. 二進制
      theme/cfapishellext_custom_states/64-0-locked.png
  59. 二進制
      theme/cfapishellext_custom_states/64-1-shared.png

+ 4 - 0
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" )

+ 9 - 3
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()

+ 16 - 12
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)

+ 58 - 0
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()

+ 4 - 0
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@"

+ 0 - 1
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;
     }
 }

+ 5 - 2
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);

+ 18 - 8
src/common/syncjournaldb.cpp

@@ -49,7 +49,7 @@ Q_LOGGING_CATEGORY(lcDb, "nextcloud.sync.database", QtInfoMsg)
 #define GET_FILE_RECORD_QUERY \
         "SELECT path, inode, modtime, type, md5, fileid, remotePerm, filesize," \
         "  ignoredChildrenRemote, contentchecksumtype.name || ':' || contentChecksum, e2eMangledName, isE2eEncrypted, " \
-        "  lock, lockOwnerDisplayName, lockOwnerId, lockType, lockOwnerEditor, lockTime, lockTimeout " \
+        "  lock, lockOwnerDisplayName, lockOwnerId, lockType, lockOwnerEditor, lockTime, lockTimeout, isShared, lastShareStateFetchedTimestmap " \
         " FROM metadata" \
         "  LEFT JOIN checksumtype as contentchecksumtype ON metadata.contentChecksumTypeId == contentchecksumtype.id"
 
@@ -74,6 +74,8 @@ static void fillFileRecordFromGetQuery(SyncJournalFileRecord &rec, SqlQuery &que
     rec._lockstate._lockEditorApp = query.stringValue(16);
     rec._lockstate._lockTime = query.int64Value(17);
     rec._lockstate._lockTimeout = query.int64Value(18);
+    rec._isShared = query.intValue(19) > 0;
+    rec._lastShareStateFetchedTimestmap = query.int64Value(20);
 }
 
 static QByteArray defaultJournalMode(const QString &dbPath)
@@ -727,6 +729,8 @@ bool SyncJournalDb::updateMetadataTableStructure()
     addColumn(QStringLiteral("contentChecksumTypeId"), QStringLiteral("INTEGER"));
     addColumn(QStringLiteral("e2eMangledName"), QStringLiteral("TEXT"));
     addColumn(QStringLiteral("isE2eEncrypted"), QStringLiteral("INTEGER"));
+    addColumn(QStringLiteral("isShared"), QStringLiteral("INTEGER"));
+    addColumn(QStringLiteral("lastShareStateFetchedTimestmap"), QStringLiteral("INTEGER"));
 
     auto uploadInfoColumns = tableColumns("uploadinfo");
     if (uploadInfoColumns.isEmpty())
@@ -881,13 +885,17 @@ Result<void, QString> SyncJournalDb::setFileRecord(const SyncJournalFileRecord &
     }
 
     qCInfo(lcDb) << "Updating file record for path:" << record.path() << "inode:" << record._inode
-                 << "modtime:" << record._modtime << "type:" << record._type
-                 << "etag:" << record._etag << "fileId:" << record._fileId << "remotePerm:" << record._remotePerm.toString()
+                 << "modtime:" << record._modtime << "type:" << record._type << "etag:" << record._etag
+                 << "fileId:" << record._fileId << "remotePerm:" << record._remotePerm.toString()
                  << "fileSize:" << record._fileSize << "checksum:" << record._checksumHeader
                  << "e2eMangledName:" << record.e2eMangledName() << "isE2eEncrypted:" << record._isE2eEncrypted
-                 << "lock:" << (record._lockstate._locked ? "true" : "false") << "lock owner type:" << record._lockstate._lockOwnerType
-                 << "lock owner:" << record._lockstate._lockOwnerDisplayName << "lock owner id:" << record._lockstate._lockOwnerId
-                 << "lock editor:" << record._lockstate._lockEditorApp;
+                 << "lock:" << (record._lockstate._locked ? "true" : "false")
+                 << "lock owner type:" << record._lockstate._lockOwnerType
+                 << "lock owner:" << record._lockstate._lockOwnerDisplayName
+                 << "lock owner id:" << record._lockstate._lockOwnerId
+                 << "lock editor:" << record._lockstate._lockEditorApp
+                 << "isShared:" << record._isShared
+                 << "lastShareStateFetchedTimestmap:" << record._lastShareStateFetchedTimestmap;
 
     const qint64 phash = getPHash(record._path);
     if (!checkConnect()) {
@@ -913,8 +921,8 @@ Result<void, QString> SyncJournalDb::setFileRecord(const SyncJournalFileRecord &
     const auto query = _queryManager.get(PreparedSqlQueryManager::SetFileRecordQuery, QByteArrayLiteral("INSERT OR REPLACE INTO metadata "
                                                                                                         "(phash, pathlen, path, inode, uid, gid, mode, modtime, type, md5, fileid, remotePerm, filesize, ignoredChildrenRemote, "
                                                                                                         "contentChecksum, contentChecksumTypeId, e2eMangledName, isE2eEncrypted, lock, lockType, lockOwnerDisplayName, lockOwnerId, "
-                                                                                                        "lockOwnerEditor, lockTime, lockTimeout) "
-                                                                                                        "VALUES (?1 , ?2, ?3 , ?4 , ?5 , ?6 , ?7,  ?8 , ?9 , ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25);"),
+                                                                                                        "lockOwnerEditor, lockTime, lockTimeout, isShared, lastShareStateFetchedTimestmap) "
+                                                                                                        "VALUES (?1 , ?2, ?3 , ?4 , ?5 , ?6 , ?7,  ?8 , ?9 , ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27);"),
         _db);
     if (!query) {
         return query->error();
@@ -945,6 +953,8 @@ Result<void, QString> SyncJournalDb::setFileRecord(const SyncJournalFileRecord &
     query->bindValue(23, record._lockstate._lockEditorApp);
     query->bindValue(24, record._lockstate._lockTime);
     query->bindValue(25, record._lockstate._lockTimeout);
+    query->bindValue(26, record._isShared);
+    query->bindValue(27, record._lastShareStateFetchedTimestmap);
 
     if (!query->exec()) {
         return query->error();

+ 2 - 0
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

+ 1 - 60
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})

+ 13 - 8
src/gui/ocsjob.cpp

@@ -40,7 +40,7 @@ void OcsJob::setVerb(const QByteArray &verb)
 
 void OcsJob::addParam(const QString &name, const QString &value)
 {
-    _params.append(qMakePair(name, value));
+    _params.insert(name, value);
 }
 
 void OcsJob::addPassStatusCode(int code)
@@ -58,16 +58,21 @@ void OcsJob::addRawHeader(const QByteArray &headerName, const QByteArray &value)
     _request.setRawHeader(headerName, value);
 }
 
+QString OcsJob::getParamValue(const QString &key) const
+{
+    return _params.value(key);
+}
+
 static QUrlQuery percentEncodeQueryItems(
-    const QList<QPair<QString, QString>> &items)
+    const QHash<QString, QString> &items)
 {
     QUrlQuery result;
     // Note: QUrlQuery::setQueryItems() does not fully percent encode
     // the query items, see #5042
-    foreach (const auto &item, items) {
+    for (auto it = std::cbegin(items); it != std::cend(items); ++it) {
         result.addQueryItem(
-            QUrl::toPercentEncoding(item.first),
-            QUrl::toPercentEncoding(item.second));
+            QUrl::toPercentEncoding(it.key()),
+            QUrl::toPercentEncoding(it.value()));
     }
     return result;
 }
@@ -85,13 +90,13 @@ void OcsJob::start()
     } else if (_verb == "POST" || _verb == "PUT") {
         // Url encode the _postParams and put them in a buffer.
         QByteArray postData;
-        Q_FOREACH (auto tmp, _params) {
+        for (auto it = std::cbegin(_params); it != std::cend(_params); ++it) {
             if (!postData.isEmpty()) {
                 postData.append("&");
             }
-            postData.append(QUrl::toPercentEncoding(tmp.first));
+            postData.append(QUrl::toPercentEncoding(it.key()));
             postData.append("=");
-            postData.append(QUrl::toPercentEncoding(tmp.second));
+            postData.append(QUrl::toPercentEncoding(it.value()));
         }
         buffer->setData(postData);
     }

+ 4 - 3
src/gui/ocsjob.h

@@ -19,8 +19,7 @@
 #include "abstractnetworkjob.h"
 
 #include <QVector>
-#include <QList>
-#include <QPair>
+#include <QHash>
 #include <QUrl>
 
 #define OCS_SUCCESS_STATUS_CODE 100
@@ -110,6 +109,8 @@ public:
      */
     void addRawHeader(const QByteArray &headerName, const QByteArray &value);
 
+    [[nodiscard]] QString getParamValue(const QString &key) const;
+
 
 protected slots:
 
@@ -149,7 +150,7 @@ private slots:
 
 private:
     QByteArray _verb;
-    QList<QPair<QString, QString>> _params;
+    QHash<QString, QString> _params;
     QVector<int> _passStatusCodes;
     QNetworkRequest _request;
 };

+ 9 - 2
src/gui/ocssharejob.cpp

@@ -24,16 +24,21 @@ namespace OCC {
 OcsShareJob::OcsShareJob(AccountPtr account)
     : OcsJob(account)
 {
-    setPath("ocs/v2.php/apps/files_sharing/api/v1/shares");
+    setPath(_pathForSharesRequest);
     connect(this, &OcsJob::jobFinished, this, &OcsShareJob::jobDone);
 }
 
-void OcsShareJob::getShares(const QString &path)
+void OcsShareJob::getShares(const QString &path, const QMap<QString, QString> &params)
 {
     setVerb("GET");
 
     addParam(QString::fromLatin1("path"), path);
     addParam(QString::fromLatin1("reshares"), QString("true"));
+
+    for (auto it = std::cbegin(params); it != std::cend(params); ++it) {
+        addParam(it.key(), it.value());
+    }
+
     addPassStatusCode(404);
 
     start();
@@ -181,4 +186,6 @@ void OcsShareJob::jobDone(QJsonDocument reply)
 {
     emit shareJobFinished(reply, _value);
 }
+
+QString const OcsShareJob::_pathForSharesRequest = QStringLiteral("ocs/v2.php/apps/files_sharing/api/v1/shares");
 }

+ 3 - 1
src/gui/ocssharejob.h

@@ -46,7 +46,7 @@ public:
      *
      * @param path Path to request shares for (default all shares)
      */
-    void getShares(const QString &path = "");
+    void getShares(const QString &path = "", const QMap<QString, QString> &params = {});
 
     /**
      * Delete the current Share
@@ -131,6 +131,8 @@ public:
      */
     void getSharedWithMe();
 
+    static const QString _pathForSharesRequest;
+
 signals:
     /**
      * Result of the OCS request

+ 268 - 2
src/gui/shellextensionsserver.cpp

@@ -16,29 +16,58 @@
 #include "account.h"
 #include "accountstate.h"
 #include "common/shellextensionutils.h"
+#include <libsync/vfs/cfapi/shellext/configvfscfapishellext.h>
 #include "folder.h"
 #include "folderman.h"
+#include "ocssharejob.h"
 #include <QDir>
+#include <QJsonArray>
 #include <QJsonDocument>
+#include <QJsonObject>
 #include <QLocalSocket>
 
+namespace {
+constexpr auto isSharedInvalidationInterval = 2 * 60 * 1000; // 2 minutes, so we don't make fetch sharees requests too often
+constexpr auto folderAliasPropertyKey = "folderAlias";
+}
+
 namespace OCC {
 
+Q_LOGGING_CATEGORY(lcShellExtServer, "nextcloud.gui.shellextensions.server", QtInfoMsg)
+
 ShellExtensionsServer::ShellExtensionsServer(QObject *parent)
     : QObject(parent)
 {
+    _isSharedInvalidationInterval = isSharedInvalidationInterval;
     _localServer.listen(VfsShellExtensions::serverNameForApplicationNameDefault());
     connect(&_localServer, &QLocalServer::newConnection, this, &ShellExtensionsServer::slotNewConnection);
 }
 
 ShellExtensionsServer::~ShellExtensionsServer()
 {
+    for (const auto &connection : _customStateSocketConnections) {
+        if (connection) {
+            QObject::disconnect(connection);
+        }
+    }
+    _customStateSocketConnections.clear();
+
     if (!_localServer.isListening()) {
         return;
     }
     _localServer.close();
 }
 
+QString ShellExtensionsServer::getFetchThumbnailPath()
+{
+    return QStringLiteral("/index.php/core/preview");
+}
+
+void ShellExtensionsServer::setIsSharedInvalidationInterval(qint64 interval)
+{
+    _isSharedInvalidationInterval = interval;
+}
+
 void ShellExtensionsServer::sendJsonMessageWithVersion(QLocalSocket *socket, const QVariantMap &message)
 {
     socket->write(VfsShellExtensions::Protocol::createJsonMessage(message));
@@ -60,6 +89,96 @@ void ShellExtensionsServer::closeSession(QLocalSocket *socket)
     socket->disconnectFromServer();
 }
 
+void ShellExtensionsServer::processCustomStateRequest(QLocalSocket *socket, const CustomStateRequestInfo &customStateRequestInfo)
+{
+    if (!customStateRequestInfo.isValid()) {
+        sendEmptyDataAndCloseSession(socket);
+        return;
+    }
+
+    const auto folder = FolderMan::instance()->folder(customStateRequestInfo.folderAlias);
+
+    if (!folder) {
+        sendEmptyDataAndCloseSession(socket);
+        return;
+    }
+    const auto filePathRelative = QString(customStateRequestInfo.path).remove(folder->path());
+
+    SyncJournalFileRecord record;
+    if (!folder->journalDb()->getFileRecord(filePathRelative, &record) || !record.isValid() || record.path().isEmpty()) {
+        qCWarning(lcShellExtServer) << "Record not found in SyncJournal for: " << filePathRelative;
+        sendEmptyDataAndCloseSession(socket);
+        return;
+    }
+
+    const auto composeMessageReplyFromRecord = [](const SyncJournalFileRecord &record) {
+        QVariantList states;
+        if (record._lockstate._locked) {
+            states.push_back(QString(CUSTOM_STATE_ICON_LOCKED_INDEX).toInt() - QString(CUSTOM_STATE_ICON_INDEX_OFFSET).toInt());
+        }
+        if (record._isShared) {
+            states.push_back(QString(CUSTOM_STATE_ICON_SHARED_INDEX).toInt() - QString(CUSTOM_STATE_ICON_INDEX_OFFSET).toInt());
+        }
+        return QVariantMap{{VfsShellExtensions::Protocol::CustomStateDataKey,
+            QVariantMap{{VfsShellExtensions::Protocol::CustomStateStatesKey, states}}}};
+    };
+
+    if (QDateTime::currentMSecsSinceEpoch() - record._lastShareStateFetchedTimestmap < _isSharedInvalidationInterval) {
+        qCInfo(lcShellExtServer) << record.path() << " record._lastShareStateFetchedTimestmap has less than " << _isSharedInvalidationInterval << " ms difference with QDateTime::currentMSecsSinceEpoch(). Returning data from SyncJournal.";
+        sendJsonMessageWithVersion(socket, composeMessageReplyFromRecord(record));
+        closeSession(socket);
+        return;
+    }
+
+    const auto job = new OcsShareJob(folder->accountState()->account());
+    job->setProperty(folderAliasPropertyKey, customStateRequestInfo.folderAlias);
+    connect(job, &OcsShareJob::shareJobFinished, this, &ShellExtensionsServer::slotSharesFetched);
+    connect(job, &OcsJob::ocsError, this, &ShellExtensionsServer::slotSharesFetchError);
+
+    {
+        _customStateSocketConnections.insert(socket->socketDescriptor(), QObject::connect(this, &ShellExtensionsServer::fetchSharesJobFinished, [this, socket, filePathRelative, composeMessageReplyFromRecord](const QString &folderAlias) {
+            {
+                const auto connection = _customStateSocketConnections[socket->socketDescriptor()];
+                if (connection) {
+                    QObject::disconnect(connection);
+                }
+                _customStateSocketConnections.remove(socket->socketDescriptor());
+            }
+            
+            const auto folder = FolderMan::instance()->folder(folderAlias);
+            SyncJournalFileRecord record;
+            if (!folder || !folder->journalDb()->getFileRecord(filePathRelative, &record) || !record.isValid()) {
+                qCWarning(lcShellExtServer) << "Record not found in SyncJournal for: " << filePathRelative;
+                sendEmptyDataAndCloseSession(socket);
+                return;
+            }
+            
+            qCInfo(lcShellExtServer) << "Sending reply from OcsShareJob for socket: " << socket->socketDescriptor() << " and record: " << record.path();
+            sendJsonMessageWithVersion(socket, composeMessageReplyFromRecord(record));
+            closeSession(socket);
+        }));
+    }
+
+    const auto sharesPath = [&record, folder, &filePathRelative]() {
+        const auto filePathRelativeRemote = QDir(folder->remotePath()).filePath(filePathRelative);
+        // either get parent's path, or, return '/' if we are in the root folder
+        auto recordPathSplit = filePathRelativeRemote.split(QLatin1Char('/'), Qt::SkipEmptyParts);
+        if (recordPathSplit.size() > 1) {
+            recordPathSplit.removeLast();
+            return recordPathSplit.join(QLatin1Char('/'));
+        }
+        return QStringLiteral("/");
+    }();
+
+    if (!_runningFetchShareJobsForPaths.contains(sharesPath)) {
+        _runningFetchShareJobsForPaths.push_back(sharesPath);
+        qCInfo(lcShellExtServer) << "Started OcsShareJob for path: " << sharesPath;
+        job->getShares(sharesPath, {{QStringLiteral("subfiles"), QStringLiteral("true")}});
+    } else {
+        qCInfo(lcShellExtServer) << "OcsShareJob is already running for path: " << sharesPath;
+    }
+}
+
 void ShellExtensionsServer::processThumbnailRequest(QLocalSocket *socket, const ThumbnailRequestInfo &thumbnailRequestInfo)
 {
     if (!thumbnailRequestInfo.isValid()) {
@@ -87,7 +206,7 @@ void ShellExtensionsServer::processThumbnailRequest(QLocalSocket *socket, const
     queryItems.addQueryItem(QStringLiteral("fileId"), record._fileId);
     queryItems.addQueryItem(QStringLiteral("x"), QString::number(thumbnailRequestInfo.size.width()));
     queryItems.addQueryItem(QStringLiteral("y"), QString::number(thumbnailRequestInfo.size.height()));
-    const QUrl jobUrl = Utility::concatUrlPath(folder->accountState()->account()->url(), QStringLiteral("/index.php/core/preview"), queryItems);
+    const QUrl jobUrl = Utility::concatUrlPath(folder->accountState()->account()->url(), getFetchThumbnailPath(), queryItems);
     const auto job = new SimpleNetworkJob(folder->accountState()->account());
     job->startRequest(QByteArrayLiteral("GET"), jobUrl);
     connect(job, &SimpleNetworkJob::finishedSignal, this, [socket, this](QNetworkReply *reply) {
@@ -121,8 +240,155 @@ void ShellExtensionsServer::slotNewConnection()
         return;
     }
 
+    if (message.contains(VfsShellExtensions::Protocol::ThumbnailProviderRequestKey)) {
+        parseThumbnailRequest(socket, message);
+        return;
+    } else if (message.contains(VfsShellExtensions::Protocol::CustomStateProviderRequestKey)) {
+        parseCustomStateRequest(socket, message);
+        return;
+    }
+    qCWarning(lcShellExtServer) << "Invalid message received from shell extension: " << message;
+    sendEmptyDataAndCloseSession(socket);
+    return;
+}
+
+void ShellExtensionsServer::slotSharesFetched(const QJsonDocument &reply)
+{
+    const auto job = qobject_cast<OcsShareJob *>(sender());
+
+    Q_ASSERT(job);
+    if (!job) {
+        qCWarning(lcShellExtServer) << "ShellExtensionsServer::slotSharesFetched is not called by OcsShareJob's signal!";
+        return;
+    }
+
+    const auto sharesPath = job->getParamValue(QStringLiteral("path"));
+
+    _runningFetchShareJobsForPaths.removeAll(sharesPath);
+
+    const auto folderAlias = job->property(folderAliasPropertyKey).toString();
+
+    Q_ASSERT(!folderAlias.isEmpty());
+    if (folderAlias.isEmpty()) {
+        qCWarning(lcShellExtServer) << "No 'folderAlias' set for OcsShareJob's instance!";
+        return;
+    }
+
+    const auto folder = FolderMan::instance()->folder(folderAlias);
+
+    Q_ASSERT(folder);
+    if (!folder) {
+        qCWarning(lcShellExtServer) << "folder not found for folderAlias: " << folderAlias;
+        return;
+    }
+
+    const auto timeStamp = QDateTime::currentMSecsSinceEpoch();
+    QStringList recortPathsToResetIsSharedFlag;
+    const QByteArray pathOfSharesToResetIsSharedFlag = sharesPath == QStringLiteral("/") ? QByteArrayLiteral("") : sharesPath.toUtf8();
+    if (folder->journalDb()->listFilesInPath(pathOfSharesToResetIsSharedFlag, [&](const SyncJournalFileRecord &rec) {
+        recortPathsToResetIsSharedFlag.push_back(rec.path());
+    })) {
+        for (const auto &recordPath : recortPathsToResetIsSharedFlag) {
+            SyncJournalFileRecord record;
+            if (!folder->journalDb()->getFileRecord(recordPath, &record) || !record.isValid()) {
+                continue;
+            }
+            record._isShared = false;
+            record._lastShareStateFetchedTimestmap = timeStamp;
+            if (!folder->journalDb()->setFileRecord(record)) {
+                qCWarning(lcShellExtServer) << "Could not set file record for path: " << record._path;
+            }
+        }
+    }
+
+    const auto sharesFetched = reply.object().value(QStringLiteral("ocs")).toObject().value(QStringLiteral("data")).toArray();
+
+    for (const auto &share : sharesFetched) {
+        const auto shareData = share.toObject();
+
+        const auto sharePath = [&shareData, folder]() { 
+            const auto sharePathRemote = shareData.value(QStringLiteral("path")).toString();
+
+            const auto folderPath = folder->remotePath();
+            if (folderPath != QLatin1Char('/') && sharePathRemote.startsWith(folderPath)) {
+                // shares are ruturned with absolute remote path, so, if we have our remote root set to subfolder, we need to adjust share's remote path to relative local path
+                const auto sharePathLocalRelative = sharePathRemote.midRef(folder->remotePathTrailingSlash().length());
+                return sharePathLocalRelative.toString();
+            }
+            return sharePathRemote.size() > 1 && sharePathRemote.startsWith(QLatin1Char('/'))
+                ? QString(sharePathRemote).remove(0, 1)
+                : sharePathRemote;
+        }();
+
+        SyncJournalFileRecord record;
+        if (!folder || !folder->journalDb()->getFileRecord(sharePath, &record) || !record.isValid()) {
+            continue;
+        }
+        record._isShared = true;
+        record._lastShareStateFetchedTimestmap = timeStamp;
+
+        if (!folder->journalDb()->setFileRecord(record)) {
+            qCWarning(lcShellExtServer) << "Could not set file record for path: " << record._path;
+        }
+    }
+
+    qCInfo(lcShellExtServer) << "Succeeded OcsShareJob for path: " << sharesPath;
+    emit fetchSharesJobFinished(folderAlias);
+}
+
+void ShellExtensionsServer::slotSharesFetchError(int statusCode, const QString &message)
+{
+    const auto job = qobject_cast<OcsShareJob *>(sender());
+
+    Q_ASSERT(job);
+    if (!job) {
+        qCWarning(lcShellExtServer) << "ShellExtensionsServer::slotSharesFetched is not called by OcsShareJob's signal!";
+        return;
+    }
+
+    const auto sharesPath = job->getParamValue(QStringLiteral("path"));
+
+    _runningFetchShareJobsForPaths.removeAll(sharesPath);
+
+    emit fetchSharesJobFinished(sharesPath);
+    qCWarning(lcShellExtServer) << "Failed OcsShareJob for path: " << sharesPath;
+}
+
+void ShellExtensionsServer::parseCustomStateRequest(QLocalSocket *socket, const QVariantMap &message)
+{
+    const auto customStateRequestMessage = message.value(VfsShellExtensions::Protocol::CustomStateProviderRequestKey).toMap();
+    const auto itemFilePath = QDir::fromNativeSeparators(customStateRequestMessage.value(VfsShellExtensions::Protocol::FilePathKey).toString());
+
+    if (itemFilePath.isEmpty()) {
+        sendEmptyDataAndCloseSession(socket);
+        return;
+    }
+
+    QString foundFolderAlias;
+    for (const auto folder : FolderMan::instance()->map()) {
+        if (itemFilePath.startsWith(folder->path())) {
+            foundFolderAlias = folder->alias();
+            break;
+        }
+    }
+
+    if (foundFolderAlias.isEmpty()) {
+        sendEmptyDataAndCloseSession(socket);
+        return;
+    }
+    
+    const auto customStateRequestInfo = CustomStateRequestInfo {
+        itemFilePath,
+        foundFolderAlias
+    };
+
+    processCustomStateRequest(socket, customStateRequestInfo);
+}
+
+void ShellExtensionsServer::parseThumbnailRequest(QLocalSocket *socket, const QVariantMap &message)
+{
     const auto thumbnailRequestMessage = message.value(VfsShellExtensions::Protocol::ThumbnailProviderRequestKey).toMap();
-    const auto thumbnailFilePath = QDir::fromNativeSeparators(thumbnailRequestMessage.value(VfsShellExtensions::Protocol::ThumbnailProviderRequestFilePathKey).toString());
+    const auto thumbnailFilePath = QDir::fromNativeSeparators(thumbnailRequestMessage.value(VfsShellExtensions::Protocol::FilePathKey).toString());
     const auto thumbnailFileSize = thumbnailRequestMessage.value(VfsShellExtensions::Protocol::ThumbnailProviderRequestFileSizeKey).toMap();
 
     if (thumbnailFilePath.isEmpty() || thumbnailFileSize.isEmpty()) {

+ 27 - 0
src/gui/shellextensionsserver.h

@@ -16,8 +16,11 @@
 
 #include <QObject>
 #include <QLocalServer>
+#include <QMutex>
 #include <QSize>
+#include <QVariant>
 
+class QJsonDocument;
 class QLocalSocket;
 
 namespace OCC {
@@ -32,21 +35,45 @@ class ShellExtensionsServer : public QObject
         [[nodiscard]] bool isValid() const { return !path.isEmpty() && !size.isEmpty() && !folderAlias.isEmpty(); }
     };
 
+    struct CustomStateRequestInfo
+    {
+        QString path;
+        QString folderAlias;
+
+        bool isValid() const { return !path.isEmpty() && !folderAlias.isEmpty(); }
+    };
+
     Q_OBJECT
 public:
     ShellExtensionsServer(QObject *parent = nullptr);
     ~ShellExtensionsServer() override;
 
+    static QString getFetchThumbnailPath();
+
+    void setIsSharedInvalidationInterval(qint64 interval);
+
+signals:
+    void fetchSharesJobFinished(const QString &folderAlias);
+
 private:
     void sendJsonMessageWithVersion(QLocalSocket *socket, const QVariantMap &message);
     void sendEmptyDataAndCloseSession(QLocalSocket *socket);
     void closeSession(QLocalSocket *socket);
+    void processCustomStateRequest(QLocalSocket *socket, const CustomStateRequestInfo &customStateRequestInfo);
     void processThumbnailRequest(QLocalSocket *socket, const ThumbnailRequestInfo &thumbnailRequestInfo);
 
+    void parseCustomStateRequest(QLocalSocket *socket, const QVariantMap &message);
+    void parseThumbnailRequest(QLocalSocket *socket, const QVariantMap &message);
+
 private slots:
     void slotNewConnection();
+    void slotSharesFetched(const QJsonDocument &reply);
+    void slotSharesFetchError(int statusCode, const QString &message);
 
 private:
     QLocalServer _localServer;
+    QStringList _runningFetchShareJobsForPaths;
+    QMap<qintptr, QMetaObject::Connection> _customStateSocketConnections;
+    qint64 _isSharedInvalidationInterval = 0;
 };
 } // namespace OCC

+ 3 - 0
src/gui/socketapi/socketapi.cpp

@@ -982,6 +982,9 @@ void SocketApi::setFileLock(const QString &localFile, const SyncFileItem::LockSt
     }
 
     shareFolder->accountState()->account()->setLockFileState(fileData.serverRelativePath, shareFolder->journalDb(), lockState);
+
+    shareFolder->journalDb()->schedulePathForRemoteDiscovery(fileData.serverRelativePath);
+    shareFolder->scheduleThisFolderSoon();
 }
 
 void SocketApi::command_V2_LIST_ACCOUNTS(const QSharedPointer<SocketApiJobV2> &job) const

+ 2 - 0
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.

+ 10 - 0
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) {

+ 2 - 0
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();

+ 4 - 0
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;
 }
 

+ 3 - 0
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)

+ 3 - 1
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!";
 }

文件差異過大導致無法顯示
+ 155 - 0
src/libsync/vfs/cfapi/shellext/CMakeLists.txt


+ 21 - 0
src/libsync/vfs/cfapi/shellext/CustomStateProvider.idl

@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+namespace CfApiShellExtensions
+{
+    runtimeclass CustomStateProvider : [default] Windows.Storage.Provider.IStorageProviderItemPropertySource
+    {
+        CustomStateProvider();
+    }
+}

+ 20 - 0
src/libsync/vfs/cfapi/shellext/configvfscfapishellext.h.in

@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#ifndef CONFIG_VFS_CFAPI_SHELLEXT_H
+#define CONFIG_VFS_CFAPI_SHELLEXT_H
+#cmakedefine CUSTOM_STATE_ICON_LOCKED_INDEX "@CUSTOM_STATE_ICON_LOCKED_INDEX@"
+#cmakedefine CUSTOM_STATE_ICON_SHARED_INDEX "@CUSTOM_STATE_ICON_SHARED_INDEX@"
+#cmakedefine CUSTOM_STATE_ICON_INDEX_OFFSET "@CUSTOM_STATE_ICON_INDEX_OFFSET@"
+#endif

+ 104 - 0
src/libsync/vfs/cfapi/shellext/customstateprovider.cpp

@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#include "customstateprovider.h"
+#include "customstateprovideripc.h"
+#include <Shlguid.h>
+
+extern long dllObjectsCount;
+
+namespace winrt::CfApiShellExtensions::implementation {
+
+CustomStateProvider::CustomStateProvider()
+{
+    InterlockedIncrement(&dllObjectsCount);
+}
+
+CustomStateProvider::~CustomStateProvider()
+{
+    InterlockedDecrement(&dllObjectsCount);
+}
+
+winrt::Windows::Foundation::Collections::IIterable<winrt::Windows::Storage::Provider::StorageProviderItemProperty>
+CustomStateProvider::GetItemProperties(hstring const &itemPath)
+{
+    std::vector<winrt::Windows::Storage::Provider::StorageProviderItemProperty> properties;
+
+    if (_dllFilePath.isEmpty()) {
+        return winrt::single_threaded_vector(std::move(properties));
+    }
+
+    const auto itemPathString = QString::fromStdString(winrt::to_string(itemPath));
+
+    const auto isItemPathValid = [&itemPathString]() {
+        if (itemPathString.isEmpty()) {
+            return false;
+        }
+
+        const auto itemPathSplit = itemPathString.split(QStringLiteral("\\"), Qt::SkipEmptyParts);
+
+        if (itemPathSplit.size() > 0) {
+            const auto itemName = itemPathSplit.last();
+            return !itemName.startsWith(QStringLiteral(".sync_")) && !itemName.startsWith(QStringLiteral(".owncloudsync.log"));
+        }
+
+        return true;
+    }();
+
+    if (!isItemPathValid) {
+        return winrt::single_threaded_vector(std::move(properties));
+    }
+
+    VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
+
+    const auto states = customStateProviderIpc.fetchCustomStatesForFile(itemPathString);
+
+    for (const auto &state : states) {
+        const auto stateValue = state.canConvert<int>() ? state.toInt() : -1;
+
+        if (stateValue >= 0) {
+            auto foundAvalability = _stateIconsAvailibility.constFind(stateValue);
+            if (foundAvalability == std::cend(_stateIconsAvailibility)) {
+                const auto hIcon = ExtractIcon(NULL, _dllFilePath.toStdWString().c_str(), stateValue);
+                _stateIconsAvailibility[stateValue] = hIcon != NULL;
+                if (hIcon) {
+                    DestroyIcon(hIcon); 
+                }
+                foundAvalability = _stateIconsAvailibility.constFind(stateValue);
+            }
+
+            if (!foundAvalability.value()) {
+                continue;
+            }
+
+            winrt::Windows::Storage::Provider::StorageProviderItemProperty itemProperty;
+            itemProperty.Id(stateValue);
+            itemProperty.Value(QString("Value%1").arg(stateValue).toStdWString());
+            itemProperty.IconResource(QString(_dllFilePath + QString(",%1").arg(QString::number(stateValue))).toStdWString());
+            properties.push_back(std::move(itemProperty));
+        }
+    }
+
+    return winrt::single_threaded_vector(std::move(properties));
+}
+void CustomStateProvider::setDllFilePath(LPCTSTR dllFilePath)
+{
+    _dllFilePath = QString::fromWCharArray(dllFilePath);
+    if (!_dllFilePath.endsWith(QStringLiteral(".dll"))) {
+        _dllFilePath.clear();
+    }
+}
+
+QString CustomStateProvider::_dllFilePath;
+}

+ 46 - 0
src/libsync/vfs/cfapi/shellext/customstateprovider.h

@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#pragma once
+#include "Generated/CfApiShellExtensions/customstateprovider.g.h"
+#include "config.h"
+#include <winrt/windows.foundation.collections.h>
+#include <windows.storage.provider.h>
+#include <QString>
+#include <QMap>
+
+namespace winrt::CfApiShellExtensions::implementation {
+class __declspec(uuid(CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID)) CustomStateProvider
+    : public CustomStateProviderT<CustomStateProvider>
+{
+public:
+    CustomStateProvider();
+    virtual ~CustomStateProvider();
+    Windows::Foundation::Collections::IIterable<Windows::Storage::Provider::StorageProviderItemProperty>
+    GetItemProperties(_In_ hstring const &itemPath);
+
+    static void setDllFilePath(LPCTSTR dllFilePath);
+
+private:
+    static QString _dllFilePath;
+    static HINSTANCE _dllhInstance;
+    QMap<int, bool> _stateIconsAvailibility;
+};
+}
+
+namespace winrt::CfApiShellExtensions::factory_implementation {
+struct CustomStateProvider : CustomStateProviderT<CustomStateProvider, implementation::CustomStateProvider>
+{
+};
+}

+ 104 - 0
src/libsync/vfs/cfapi/shellext/customstateprovideripc.cpp

@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#include "customstateprovideripc.h"
+#include "common/shellextensionutils.h"
+#include "ipccommon.h"
+#include <QJsonDocument>
+namespace {
+// we don't want to block the Explorer for too long (default is 30K, so we'd keep it at 10K, except QLocalSocket::waitForDisconnected())
+constexpr auto socketTimeoutMs = 10000;
+}
+
+namespace VfsShellExtensions {
+
+CustomStateProviderIpc::~CustomStateProviderIpc()
+{
+    disconnectSocketFromServer();
+}
+
+QVariantList CustomStateProviderIpc::fetchCustomStatesForFile(const QString &filePath)
+{
+    const auto sendMessageAndReadyRead = [this](QVariantMap &message) {
+        _localSocket.write(VfsShellExtensions::Protocol::createJsonMessage(message));
+        return _localSocket.waitForBytesWritten(socketTimeoutMs) && _localSocket.waitForReadyRead(socketTimeoutMs);
+    };
+
+    const auto mainServerName = getServerNameForPath(filePath);
+
+    if (mainServerName.isEmpty()) {
+        return {};
+    }
+
+    // #1 Connect to the local server
+    if (!connectSocketToServer(mainServerName)) {
+        return {};
+    }
+
+    auto messageRequestCustomStatesForFile = QVariantMap {
+        {
+            VfsShellExtensions::Protocol::CustomStateProviderRequestKey,
+            QVariantMap {
+                { VfsShellExtensions::Protocol::FilePathKey, filePath }
+            }
+        }
+    };
+
+    // #2 Request custom states for a 'filePath'
+    if (!sendMessageAndReadyRead(messageRequestCustomStatesForFile)) {
+        return {};
+    }
+
+    // #3 Receive custom states as JSON
+    const auto message = QJsonDocument::fromJson(_localSocket.readAll()).toVariant().toMap();
+    if (!VfsShellExtensions::Protocol::validateProtocolVersion(message) || !message.contains(VfsShellExtensions::Protocol::CustomStateDataKey)) {
+        return {};
+    }
+    const auto customStates = message.value(VfsShellExtensions::Protocol::CustomStateDataKey).toMap().value(VfsShellExtensions::Protocol::CustomStateStatesKey).toList();
+    disconnectSocketFromServer();
+
+    return customStates;
+}
+
+bool CustomStateProviderIpc::disconnectSocketFromServer()
+{
+    const auto isConnectedOrConnecting = _localSocket.state() == QLocalSocket::ConnectedState || _localSocket.state() == QLocalSocket::ConnectingState;
+    if (isConnectedOrConnecting) {
+        _localSocket.disconnectFromServer();
+        const auto isNotConnected = _localSocket.state() == QLocalSocket::UnconnectedState || _localSocket.state() == QLocalSocket::ClosingState;
+        return isNotConnected || _localSocket.waitForDisconnected();
+    }
+    return true;
+}
+
+QString CustomStateProviderIpc::getServerNameForPath(const QString &filePath)
+{
+    if (!overrideServerName.isEmpty()) {
+        return overrideServerName;
+    }
+
+    return findServerNameForPath(filePath);
+}
+
+bool CustomStateProviderIpc::connectSocketToServer(const QString &serverName)
+{
+    if (!disconnectSocketFromServer()) {
+        return false;
+    }
+    _localSocket.setServerName(serverName);
+    _localSocket.connectToServer();
+    return _localSocket.state() == QLocalSocket::ConnectedState || _localSocket.waitForConnected(socketTimeoutMs);
+}
+QString CustomStateProviderIpc::overrideServerName = {};
+}

+ 43 - 0
src/libsync/vfs/cfapi/shellext/customstateprovideripc.h

@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#pragma once
+
+#include <QtNetwork/QLocalSocket>
+#include <QString>
+#include <QVariant>
+
+namespace VfsShellExtensions {
+class CustomStateProviderIpc
+{
+public:
+    CustomStateProviderIpc() = default;
+    ~CustomStateProviderIpc();
+
+    QVariantList fetchCustomStatesForFile(const QString &filePath);
+
+private:
+    bool connectSocketToServer(const QString &serverName);
+    bool disconnectSocketFromServer();
+
+    static QString getServerNameForPath(const QString &filePath);
+
+public:
+    // for unit tests (as Registry does not work on a CI VM)
+    static QString overrideServerName;
+
+private:
+    QLocalSocket _localSocket;
+};
+}

+ 18 - 1
src/libsync/vfs/cfapi/shellext/dllmain.cpp

@@ -13,16 +13,20 @@
  */
 
 #include "cfapishellintegrationclassfactory.h"
+#include "customstateprovider.h"
 #include "thumbnailprovider.h"
 #include <comdef.h>
 
 long dllReferenceCount = 0;
+long dllObjectsCount = 0;
 
 HINSTANCE instanceHandle = NULL;
 
+HRESULT CustomStateProvider_CreateInstance(REFIID riid, void **ppv);
 HRESULT ThumbnailProvider_CreateInstance(REFIID riid, void **ppv);
 
 const VfsShellExtensions::ClassObjectInit listClassesSupported[] = {
+    {&__uuidof(winrt::CfApiShellExtensions::implementation::CustomStateProvider), CustomStateProvider_CreateInstance},
     {&__uuidof(VfsShellExtensions::ThumbnailProvider), ThumbnailProvider_CreateInstance}
 };
 
@@ -30,6 +34,9 @@ STDAPI_(BOOL) DllMain(HINSTANCE hInstance, DWORD dwReason, void *)
 {
     if (dwReason == DLL_PROCESS_ATTACH) {
         instanceHandle = hInstance;
+        wchar_t dllFilePath[_MAX_PATH] = {0};
+        ::GetModuleFileName(instanceHandle, dllFilePath, _MAX_PATH);
+        winrt::CfApiShellExtensions::implementation::CustomStateProvider::setDllFilePath(dllFilePath);
         DisableThreadLibraryCalls(hInstance);
     }
 
@@ -38,7 +45,7 @@ STDAPI_(BOOL) DllMain(HINSTANCE hInstance, DWORD dwReason, void *)
 
 STDAPI DllCanUnloadNow()
 {
-    return dllReferenceCount == 0 ? S_OK : S_FALSE;
+    return (dllReferenceCount == 0 && dllObjectsCount == 0) ? S_OK : S_FALSE;
 }
 
 STDAPI DllGetClassObject(REFCLSID clsid, REFIID riid, void **ppv)
@@ -46,6 +53,16 @@ STDAPI DllGetClassObject(REFCLSID clsid, REFIID riid, void **ppv)
     return VfsShellExtensions::CfApiShellIntegrationClassFactory::CreateInstance(clsid, listClassesSupported, ARRAYSIZE(listClassesSupported), riid, ppv);
 }
 
+HRESULT CustomStateProvider_CreateInstance(REFIID riid, void **ppv)
+{
+    try {
+        const auto customStateProvider = winrt::make_self<winrt::CfApiShellExtensions::implementation::CustomStateProvider>();
+        return customStateProvider->QueryInterface(riid, ppv);
+    } catch (_com_error exc) {
+        return exc.Error();
+    }
+}
+
 HRESULT ThumbnailProvider_CreateInstance(REFIID riid, void **ppv)
 {
     auto *thumbnailProvider = new (std::nothrow) VfsShellExtensions::ThumbnailProvider();

+ 50 - 0
src/libsync/vfs/cfapi/shellext/ipccommon.cpp

@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#include "ipccommon.h"
+#include "common/shellextensionutils.h"
+#include "common/utility.h"
+#include <QDir>
+
+namespace VfsShellExtensions {
+QString findServerNameForPath(const QString &filePath)
+{
+    // SyncRootManager Registry key contains all registered folders for Cf API. It will give us the correct name of the
+    // current app based on the folder path
+    QString serverName;
+    constexpr auto syncRootManagerRegKey = R"(SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\SyncRootManager)";
+
+    if (OCC::Utility::registryKeyExists(HKEY_LOCAL_MACHINE, syncRootManagerRegKey)) {
+        OCC::Utility::registryWalkSubKeys(
+            HKEY_LOCAL_MACHINE, syncRootManagerRegKey, [&](HKEY, const QString &syncRootId) {
+                const QString syncRootIdUserSyncRootsRegistryKey =
+                    syncRootManagerRegKey + QStringLiteral("\\") + syncRootId + QStringLiteral(R"(\UserSyncRoots\)");
+                OCC::Utility::registryWalkValues(HKEY_LOCAL_MACHINE, syncRootIdUserSyncRootsRegistryKey,
+                    [&](const QString &userSyncRootName, bool *done) {
+                        const auto userSyncRootValue = QDir::fromNativeSeparators(OCC::Utility::registryGetKeyValue(
+                            HKEY_LOCAL_MACHINE, syncRootIdUserSyncRootsRegistryKey, userSyncRootName)
+                                                                                      .toString());
+                        if (QDir::fromNativeSeparators(filePath).startsWith(userSyncRootValue)) {
+                            const auto syncRootIdSplit = syncRootId.split(QLatin1Char('!'), Qt::SkipEmptyParts);
+                            if (!syncRootIdSplit.isEmpty()) {
+                                serverName = VfsShellExtensions::serverNameForApplicationName(syncRootIdSplit.first());
+                                *done = true;
+                            }
+                        }
+                    });
+            });
+    }
+    return serverName;
+}
+}

+ 21 - 0
src/libsync/vfs/cfapi/shellext/ipccommon.h

@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#pragma once
+
+#include <QString>
+
+namespace VfsShellExtensions {
+QString findServerNameForPath(const QString &filePath);
+}

+ 7 - 0
src/libsync/vfs/cfapi/shellext/thumbnailprovider.cpp

@@ -48,6 +48,8 @@
 #include <shlwapi.h>
 #include <QSize>
 
+extern long dllObjectsCount;
+
 namespace VfsShellExtensions {
 
 std::pair<HBITMAP, WTS_ALPHATYPE> hBitmapAndAlphaTypeFromData(const QByteArray &thumbnailData)
@@ -93,8 +95,13 @@ std::pair<HBITMAP, WTS_ALPHATYPE> hBitmapAndAlphaTypeFromData(const QByteArray &
 ThumbnailProvider::ThumbnailProvider()
     : _referenceCount(1)
 {
+    InterlockedIncrement(&dllObjectsCount);
 }
 
+ThumbnailProvider::~ThumbnailProvider()
+{
+    InterlockedDecrement(&dllObjectsCount);
+}
 IFACEMETHODIMP ThumbnailProvider::QueryInterface(REFIID riid, void **ppv)
 {
     static const QITAB qit[] = {

+ 1 - 1
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);
 

+ 3 - 24
src/libsync/vfs/cfapi/shellext/thumbnailprovideripc.cpp

@@ -14,14 +14,11 @@
 
 #include "thumbnailprovideripc.h"
 #include "common/shellextensionutils.h"
-#include "common/utility.h"
+#include "ipccommon.h"
 #include <QString>
 #include <QSize>
 #include <QtNetwork/QLocalSocket>
 #include <QJsonDocument>
-#include <QObject>
-#include <QDir>
-#include <Windows.h>
 namespace {
 // we don't want to block the Explorer for too long (default is 30K, so we'd keep it at 10K, except QLocalSocket::waitForDisconnected())
 constexpr auto socketTimeoutMs = 10000;
@@ -61,7 +58,7 @@ QByteArray ThumbnailProviderIpc::fetchThumbnailForFile(const QString &filePath,
         {
             VfsShellExtensions::Protocol::ThumbnailProviderRequestKey,
             QVariantMap {
-                {VfsShellExtensions::Protocol::ThumbnailProviderRequestFilePathKey, filePath},
+                {VfsShellExtensions::Protocol::FilePathKey, filePath},
                 {VfsShellExtensions::Protocol::ThumbnailProviderRequestFileSizeKey, QVariantMap{{QStringLiteral("width"), size.width()}, {QStringLiteral("height"), size.height()}}}
             }
         }
@@ -99,26 +96,8 @@ QString ThumbnailProviderIpc::getServerNameForPath(const QString &filePath)
     if (!overrideServerName.isEmpty()) {
         return overrideServerName;
     }
-    // SyncRootManager Registry key contains all registered folders for Cf API. It will give us the correct name of the current app based on the folder path
-    QString serverName;
-    constexpr auto syncRootManagerRegKey = R"(SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\SyncRootManager)";
 
-    if (OCC::Utility::registryKeyExists(HKEY_LOCAL_MACHINE, syncRootManagerRegKey)) {
-        OCC::Utility::registryWalkSubKeys(HKEY_LOCAL_MACHINE, syncRootManagerRegKey, [&](HKEY, const QString &syncRootId) {
-            const QString syncRootIdUserSyncRootsRegistryKey = syncRootManagerRegKey + QStringLiteral("\\") + syncRootId + QStringLiteral(R"(\UserSyncRoots\)");
-            OCC::Utility::registryWalkValues(HKEY_LOCAL_MACHINE, syncRootIdUserSyncRootsRegistryKey, [&](const QString &userSyncRootName, bool *done) {
-                const auto userSyncRootValue = QDir::fromNativeSeparators(OCC::Utility::registryGetKeyValue(HKEY_LOCAL_MACHINE, syncRootIdUserSyncRootsRegistryKey, userSyncRootName).toString());
-                if (QDir::fromNativeSeparators(filePath).startsWith(userSyncRootValue)) {
-                    const auto syncRootIdSplit = syncRootId.split(QLatin1Char('!'), Qt::SkipEmptyParts);
-                    if (!syncRootIdSplit.isEmpty()) {
-                        serverName = VfsShellExtensions::serverNameForApplicationName(syncRootIdSplit.first());
-                        *done = true;
-                    }
-                }
-            });
-        });
-    }
-    return serverName;
+    return findServerNameForPath(filePath);
 }
 
 bool ThumbnailProviderIpc::connectSocketToServer(const QString &serverName)

+ 31 - 18
src/libsync/vfs/cfapi/vfs_cfapi.cpp

@@ -40,12 +40,16 @@ const auto rootKey = HKEY_CURRENT_USER;
 
 bool registerShellExtension()
 {
+    const QList<QPair<QString, QString>> listExtensions = {
+        {CFAPI_SHELLEXT_THUMBNAIL_HANDLER_DISPLAY_NAME, CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG},
+        {CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_DISPLAY_NAME, CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID_REG}
+    };
+    // assume CFAPI_SHELL_EXTENSIONS_LIB_NAME is always in the same folder as the main executable
     // assume CFAPI_SHELL_EXTENSIONS_LIB_NAME is always in the same folder as the main executable
     const auto shellExtensionDllPath = QDir::toNativeSeparators(QString(QCoreApplication::applicationDirPath() + QStringLiteral("/") + CFAPI_SHELL_EXTENSIONS_LIB_NAME + QStringLiteral(".dll")));
     if (!QFileInfo::exists(shellExtensionDllPath)) {
         Q_ASSERT(false);
-        qCWarning(lcCfApi) << "Register CfAPI shell extensions failed. Dll does not exist in "
-                           << QCoreApplication::applicationDirPath();
+        qCWarning(lcCfApi) << "Register CfAPI shell extensions failed. Dll does not exist in " << QCoreApplication::applicationDirPath();
         return false;
     }
 
@@ -57,20 +61,22 @@ bool registerShellExtension()
         return false;
     }
 
-    const QString clsidPath = QString() % clsIdRegKey % CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG;
-    const QString clsidServerPath = clsidPath % R"(\InprocServer32)";
+    for (const auto extension : listExtensions) {
+        const QString clsidPath = QString() % clsIdRegKey % extension.second;
+        const QString clsidServerPath = clsidPath % R"(\InprocServer32)";
 
-    if (!OCC::Utility::registrySetKeyValue(rootKey, clsidPath, QStringLiteral("AppID"), REG_SZ, CFAPI_SHELLEXT_APPID_REG)) {
-        return false;
-    }
-    if (!OCC::Utility::registrySetKeyValue(rootKey, clsidPath, {}, REG_SZ, CFAPI_SHELLEXT_THUMBNAIL_HANDLER_DISPLAY_NAME)) {
-        return false;
-    }
-    if (!OCC::Utility::registrySetKeyValue(rootKey, clsidServerPath, {}, REG_SZ, shellExtensionDllPath)) {
-        return false;
-    }
-    if (!OCC::Utility::registrySetKeyValue(rootKey, clsidServerPath, QStringLiteral("ThreadingModel"), REG_SZ, QStringLiteral("Apartment"))) {
-        return false;
+        if (!OCC::Utility::registrySetKeyValue(rootKey, clsidPath, QStringLiteral("AppID"), REG_SZ, CFAPI_SHELLEXT_APPID_REG)) {
+            return false;
+        }
+        if (!OCC::Utility::registrySetKeyValue(rootKey, clsidPath, {}, REG_SZ, extension.first)) {
+            return false;
+        }
+        if (!OCC::Utility::registrySetKeyValue(rootKey, clsidServerPath, {}, REG_SZ, shellExtensionDllPath)) {
+            return false;
+        }
+        if (!OCC::Utility::registrySetKeyValue(rootKey, clsidServerPath, QStringLiteral("ThreadingModel"), REG_SZ, QStringLiteral("Apartment"))) {
+            return false;
+        }
     }
 
     return true;
@@ -83,9 +89,16 @@ void unregisterShellExtensions()
         OCC::Utility::registryDeleteKeyTree(rootKey, appIdPath);
     }
 
-    const QString clsidPath = QString() % clsIdRegKey % CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG;
-    if (OCC::Utility::registryKeyExists(rootKey, clsidPath)) {
-        OCC::Utility::registryDeleteKeyTree(rootKey, clsidPath);
+    const QStringList listExtensions = {
+        CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID_REG,
+        CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG
+    };
+
+    for (const auto extension : listExtensions) {
+        const QString clsidPath = QString() % clsIdRegKey % extension;
+        if (OCC::Utility::registryKeyExists(rootKey, clsidPath)) {
+            OCC::Utility::registryDeleteKeyTree(rootKey, clsidPath);
+        }
     }
 }
 

+ 1 - 1
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()

+ 309 - 38
test/testcfapishellextensionsipc.cpp

@@ -5,22 +5,121 @@
  *
  */
 
+#include <account.h>
+#include <accountstate.h>
+#include <accountmanager.h>
+#include <common/vfs.h>
+#include <common/shellextensionutils.h>
+#include "config.h"
+#include <folderman.h>
+#include <libsync/vfs/cfapi/shellext/configvfscfapishellext.h>
+#include <ocssharejob.h>
+#include <shellextensionsserver.h>
+#include <syncengine.h>
+#include "syncenginetestutils.h"
+#include "testhelper.h"
+#include <vfs/cfapi/shellext/customstateprovideripc.h>
+#include <vfs/cfapi/shellext/thumbnailprovideripc.h>
 #include <QtTest>
 #include <QImage>
 #include <QPainter>
-#include "syncenginetestutils.h"
-#include "common/vfs.h"
-#include "common/shellextensionutils.h"
-#include "config.h"
-#include <syncengine.h>
 
-#include "folderman.h"
-#include "account.h"
-#include "accountstate.h"
-#include "accountmanager.h"
-#include "testhelper.h"
-#include "vfs/cfapi/shellext/thumbnailprovideripc.h"
-#include "shellextensionsserver.h"
+namespace {
+static constexpr auto roootFolderName = "A";
+static constexpr auto imagesFolderName = "photos";
+static constexpr auto filesFolderName = "files";
+
+static const QByteArray fakeNoSharesResponse = R"({"ocs":{"data":[],"meta":{"message":"OK","status":"ok","statuscode":200}}})";
+
+static const QByteArray fakeSharedFilesResponse = R"({"ocs":{"data":[{
+                "attributes": null,
+                "can_delete": true,
+                "can_edit": true,
+                "displayname_file_owner": "admin",
+                "displayname_owner": "admin",
+                "expiration": null,
+                "file_parent": 2981,
+                "file_source": 3538,
+                "file_target": "/test_shared_file.txt",
+                "has_preview": true,
+                "hide_download": 0,
+                "id": "36",
+                "item_source": 3538,
+                "item_type": "file",
+                "label": null,
+                "mail_send": 0,
+                "mimetype": "text/plain",
+                "note": "",
+                "parent": null,
+                "path": "A/files/test_shared_file.txt",
+                "permissions": 19,
+                "share_type": 0,
+                "share_with": "newstandard",
+                "share_with_displayname": "newstandard",
+                "share_with_displayname_unique": "newstandard",
+                "status": {
+                    "clearAt": null,
+                    "icon": null,
+                    "message": null,
+                    "status": "offline"
+                },
+                "stime": 1662995777,
+                "storage": 2,
+                "storage_id": "home::admin",
+                "token": null,
+                "uid_file_owner": "admin",
+                "uid_owner": "admin"
+            },
+            {
+                "attributes": null,
+                "can_delete": true,
+                "can_edit": true,
+                "displayname_file_owner": "admin",
+                "displayname_owner": "admin",
+                "expiration": null,
+                "file_parent": 2981,
+                "file_source": 3538,
+                "file_target": "/test_shared_and_locked_file.txt",
+                "has_preview": true,
+                "hide_download": 0,
+                "id": "36",
+                "item_source": 3538,
+                "item_type": "file",
+                "label": null,
+                "mail_send": 0,
+                "mimetype": "text/plain",
+                "note": "",
+                "parent": null,
+                "path": "A/files/test_shared_and_locked_file.txt",
+                "permissions": 19,
+                "share_type": 0,
+                "share_with": "newstandard",
+                "share_with_displayname": "newstandard",
+                "share_with_displayname_unique": "newstandard",
+                "status": {
+                    "clearAt": null,
+                    "icon": null,
+                    "message": null,
+                    "status": "offline"
+                },
+                "stime": 1662995777,
+                "storage": 2,
+                "storage_id": "home::admin",
+                "token": null,
+                "uid_file_owner": "admin",
+                "uid_owner": "admin"
+            }
+        ],
+        "meta": {
+            "message": "OK",
+            "status": "ok",
+            "statuscode": 200
+        }
+    }
+})";
+
+static constexpr auto shellExtensionServerOverrideIntervalMs = 1000LL * 2LL;
+}
 
 using namespace OCC;
 
@@ -38,21 +137,39 @@ class TestCfApiShellExtensionsIPC : public QObject
 
     QScopedPointer<ShellExtensionsServer> _shellExtensionsServer;
 
-    QStringList dummmyImageNames = {
-        "A/photos/imageJpg.jpg",
-        "A/photos/imagePng.png",
-        "A/photos/imagePng.bmp",
+    const QStringList dummmyImageNames = {
+        { QString(QString(roootFolderName) + QLatin1Char('/') + QString(imagesFolderName) + QLatin1Char('/') + QStringLiteral("imageJpg.jpg")) },
+        { QString(QString(roootFolderName) + QLatin1Char('/') + QString(imagesFolderName) + QLatin1Char('/') + QStringLiteral("imagePng.png")) },
+        { QString(QString(roootFolderName) + QLatin1Char('/') + QString(imagesFolderName) + QLatin1Char('/') + QStringLiteral("imagePng.bmp")) }
     };
     QMap<QString, QByteArray> dummyImages;
 
     QString currentImage;
 
+    struct FileStates
+    {
+        bool _isShared = false;
+        bool _isLocked = false;
+    };
+
+    const QMap<QString, FileStates> dummyFileStates = {
+        { QString(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName) + QLatin1Char('/') + QStringLiteral("test_locked_file.txt")), { false, true } },
+        { QString(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName) + QLatin1Char('/') + QStringLiteral("test_shared_file.txt")), { true, false } },
+        { QString(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName) + QLatin1Char('/') + QStringLiteral("test_shared_and_locked_file.txt")), { true, true }},
+        { QString(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName) + QLatin1Char('/') + QStringLiteral("test_non_shared_and_non_locked_file.txt")), { false, false }}
+    };
+
+public:
+    static bool replyWithNoShares;
+
 private slots:
     void initTestCase()
     {
         VfsShellExtensions::ThumbnailProviderIpc::overrideServerName = VfsShellExtensions::serverNameForApplicationNameDefault();
+        VfsShellExtensions::CustomStateProviderIpc::overrideServerName = VfsShellExtensions::serverNameForApplicationNameDefault();
 
         _shellExtensionsServer.reset(new ShellExtensionsServer);
+        _shellExtensionsServer->setIsSharedInvalidationInterval(shellExtensionServerOverrideIntervalMs);
 
         for (const auto &dummyImageName : dummmyImageNames) {
             const auto extension = dummyImageName.split(".").last();
@@ -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<QNetworkRequest::KnownHeaders, QByteArray> additionalHeaders = {
-                        {QNetworkRequest::KnownHeaders::ContentTypeHeader, "image/jpeg"}};
+                        {QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"}};
                     fakePayloadReply->_additionalHeaders = additionalHeaders;
-
                     reply = fakePayloadReply;
+                } else if (path.endsWith(ShellExtensionsServer::getFetchThumbnailPath())) {
+                    const auto urlQuery = QUrlQuery(req.url());
+                    const auto fileId = urlQuery.queryItemValue(QStringLiteral("fileId"));
+                    const auto x = urlQuery.queryItemValue(QStringLiteral("x")).toInt();
+                    const auto y = urlQuery.queryItemValue(QStringLiteral("y")).toInt();
+                    if (fileId.isEmpty() || x <= 0 || y <= 0) {
+                        reply = new FakePayloadReply(op, req, {}, nullptr);
+                    } else {
+                        const auto foundImageIt = dummyImages.find(currentImage);
+
+                        QByteArray byteArray;
+                        if (foundImageIt != dummyImages.end()) {
+                            byteArray = foundImageIt.value();
+                        }
+
+                        currentImage.clear();
+
+                        auto fakePayloadReply = new FakePayloadReply(op, req, byteArray, nullptr);
+
+                        QMap<QNetworkRequest::KnownHeaders, QByteArray> additionalHeaders = {
+                            {QNetworkRequest::KnownHeaders::ContentTypeHeader, "image/jpeg"}};
+                        fakePayloadReply->_additionalHeaders = additionalHeaders;
+
+                        reply = fakePayloadReply;
+                    }
+                } else {
+                    reply = new FakePayloadReply(op, req, {}, nullptr);
                 }
                 
                 return reply;
@@ -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<int>(SyncFileItem::LockOwnerType::UserLock);
+                    record._lockstate._lockTime = QDateTime::currentMSecsSinceEpoch();
+                    record._lockstate._lockTimeout = 1000 * 60 * 60;
+                }
+                QVERIFY(fakeFolder.syncJournal().setFileRecord(record));
+                QVERIFY(realFolder->journalDb()->setFileRecord(record));
+            }
+        }
+
+        // #1 Test every file's states fetching. Everything must succeed.
+        for (auto it = std::cbegin(dummyFileStates); it != std::cend(dummyFileStates); ++it) {
+            QEventLoop loop;
+            QVariantList customStates;
+            std::thread t([&] {
+                VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
+                customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + it.key());
+                QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
+            });
+            loop.exec();
+            t.detach();
+            QVERIFY(!customStates.isEmpty() || (!it.value()._isLocked && !it.value()._isShared));
+        }
+
+        // #2 Test wrong file's states fetching. It must fail.
+        QEventLoop loop;
+        QVariantList customStates;
+        std::thread t1([&] {
+            VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
+            customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + QStringLiteral("A/files/wrong.jpg"));
+            QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
+        });
+        loop.exec();
+        t1.detach();
+        QVERIFY(customStates.isEmpty());
+
+        // #3 Test wrong file states fetching. It must fail.
+        customStates.clear();
+        std::thread t2([&] {
+            VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
+            customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + QStringLiteral("A/files/test_non_shared_and_non_locked_file.txt"));
+            QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
+        });
+        loop.exec();
+        t2.detach();
+        QVERIFY(customStates.isEmpty());
+
+        // reset all share states to make sure we'll get new states when fetching
+        for (auto it = std::begin(dummyFileStates); it != std::end(dummyFileStates); ++it) {
+            if (fakeFolder.syncJournal().getFileRecord(it.key(), &record)) {
+                record._remotePerm.unsetPermission(OCC::RemotePermissions::Permissions::IsShared);
+                record._isShared = false;
+                QVERIFY(fakeFolder.syncJournal().setFileRecord(record));
+                QVERIFY(realFolder->journalDb()->setFileRecord(record));
+            }
+        }
+        QVERIFY(fakeFolder.syncOnce());
+        QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+        //
+
+        // wait enough time to make shares' state invalid
+        QTest::qWait(shellExtensionServerOverrideIntervalMs + 1000);
+
+        // #4 Test every file's states fetching. Everything must succeed.
+        for (auto it = std::cbegin(dummyFileStates); it != std::cend(dummyFileStates); ++it) {
+            QEventLoop loop;
+            QVariantList customStates;
+            std::thread t([&] {
+                VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
+                customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + it.key());
+                QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
+            });
+            loop.exec();
+            t.detach();
+            QVERIFY(!customStates.isEmpty() || (!it.value()._isLocked && !it.value()._isShared));
+
+            if (!customStates.isEmpty()) {
+                const auto lockedIndex = QString(CUSTOM_STATE_ICON_LOCKED_INDEX).toInt() - QString(CUSTOM_STATE_ICON_INDEX_OFFSET).toInt();
+                const auto sharedIndex = QString(CUSTOM_STATE_ICON_SHARED_INDEX).toInt() - QString(CUSTOM_STATE_ICON_INDEX_OFFSET).toInt();
+
+                if (customStates.contains(lockedIndex) && customStates.contains(sharedIndex)) {
+                    QVERIFY(it.value()._isLocked && it.value()._isShared);
+                }
+                if (customStates.contains(lockedIndex)) {
+                    QVERIFY(it.value()._isLocked);
+                }
+                if (customStates.contains(sharedIndex)) {
+                    QVERIFY(it.value()._isShared);
+                }
+            }
+        }
+
+        // #5 Test no shares response for a file
+        QTest::qWait(shellExtensionServerOverrideIntervalMs + 1000);
+        TestCfApiShellExtensionsIPC::replyWithNoShares = true;
+        customStates.clear();
+        std::thread t3([&] {
+            VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
+            customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + QStringLiteral("A/files/test_non_shared_and_non_locked_file.txt"));
+            QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
+        });
+        loop.exec();
+        t3.detach();
+        QVERIFY(customStates.isEmpty());
+    }
+
     void cleanupTestCase()
     {
         VfsShellExtensions::ThumbnailProviderIpc::overrideServerName.clear();
@@ -212,5 +481,7 @@ private slots:
     }
 };
 
+bool TestCfApiShellExtensionsIPC::replyWithNoShares = false;
+
 QTEST_GUILESS_MAIN(TestCfApiShellExtensionsIPC)
 #include "testcfapishellextensionsipc.moc"

+ 1 - 0
theme/cfapishellext_custom_states/0-locked.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 16 16" width="16" height="16"><path d="m8 1c-2.319 0-3.967 1.8644-4 4v2.5h-1.5v7.5h11v-7.5h-1.5v-2.5c0-2.27-1.8-3.9735-4-4zm0 2c1.25 0 2 0.963 2 2v2.5h-4v-2.5c0-1.174 0.747-2 2-2z" fill="#000"/></svg>

+ 1 - 0
theme/cfapishellext_custom_states/1-shared.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" viewBox="0 0 16 16"><circle cx="3.5" cy="8" r="2.5"/><circle cy="12.5" cx="12.5" r="2.5"/><circle cx="12.5" cy="3.5" r="2.5"/><path d="m3.5 8 9 4.5m-9-4.5 9-4.5" stroke="#000" stroke-width="2" fill="none"/></svg>

二進制
theme/cfapishellext_custom_states/1024-0-locked.png


二進制
theme/cfapishellext_custom_states/1024-1-shared.png


二進制
theme/cfapishellext_custom_states/128-0-locked.png


二進制
theme/cfapishellext_custom_states/128-1-shared.png


二進制
theme/cfapishellext_custom_states/24-0-locked.png


二進制
theme/cfapishellext_custom_states/24-1-shared.png


二進制
theme/cfapishellext_custom_states/256-0-locked.png


二進制
theme/cfapishellext_custom_states/256-1-shared.png


二進制
theme/cfapishellext_custom_states/32-0-locked.png


二進制
theme/cfapishellext_custom_states/32-1-shared.png


二進制
theme/cfapishellext_custom_states/40-0-locked.png


二進制
theme/cfapishellext_custom_states/40-1-shared.png


二進制
theme/cfapishellext_custom_states/48-0-locked.png


二進制
theme/cfapishellext_custom_states/48-1-shared.png


二進制
theme/cfapishellext_custom_states/512-0-locked.png


二進制
theme/cfapishellext_custom_states/512-1-shared.png


二進制
theme/cfapishellext_custom_states/64-0-locked.png


二進制
theme/cfapishellext_custom_states/64-1-shared.png


部分文件因文件數量過多而無法顯示