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

Feature/vfs windows sharing and lock state
This commit is contained in:
allexzander 2022-10-04 22:40:15 +03:00 committed by GitHub
commit de27a2ffd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 1431 additions and 187 deletions

View File

@ -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" )

View File

@ -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()

View File

@ -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)

View File

@ -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()

View File

@ -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@"

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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();

View File

@ -81,6 +81,8 @@ public:
QByteArray _e2eMangledName;
bool _isE2eEncrypted = false;
SyncJournalFileLockInfo _lockstate;
bool _isShared = false;
qint64 _lastShareStateFetchedTimestmap = 0;
};
bool OCSYNC_EXPORT

View File

@ -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})

View File

@ -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);
}

View File

@ -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;
};

View File

@ -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");
}

View File

@ -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

View File

@ -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()) {

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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) {

View File

@ -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();

View File

@ -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;
}

View File

@ -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)

View File

@ -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!";
}

View File

@ -1,18 +1,197 @@
include(GenerateIconsUtils)
# generate custom states icons
set(theme_dir ${CMAKE_SOURCE_DIR}/theme)
set(custom_state_icons_path "${theme_dir}/cfapishellext_custom_states")
set(CUSTOM_STATE_ICON_LOCKED_PATH "${custom_state_icons_path}/0-locked.svg")
set(CUSTOM_STATE_ICON_SHARED_PATH "${custom_state_icons_path}/1-shared.svg")
foreach(size IN ITEMS 24;32;40;48;64;128;256;512;1024)
get_filename_component(output_icon_name_custom_state_locked ${CUSTOM_STATE_ICON_LOCKED_PATH} NAME_WLE)
generate_sized_png_from_svg(${CUSTOM_STATE_ICON_LOCKED_PATH} ${size} OUTPUT_ICON_NAME ${output_icon_name_custom_state_locked} OUTPUT_ICON_PATH "${custom_state_icons_path}/")
endforeach()
foreach(size IN ITEMS 24;32;40;48;64;128;256;512;1024)
get_filename_component(output_icon_name_custom_state_shared ${CUSTOM_STATE_ICON_SHARED_PATH} NAME_WLE)
generate_sized_png_from_svg(${CUSTOM_STATE_ICON_SHARED_PATH} ${size} OUTPUT_ICON_NAME ${output_icon_name_custom_state_shared} OUTPUT_ICON_PATH "${custom_state_icons_path}/")
endforeach()
# offset is used for referencing icon within the binary's resources (indexing start with 0, while IDI_ICON{i} 'i' starts with 1)
if(NOT DEFINED CUSTOM_STATE_ICON_INDEX_OFFSET)
set(CUSTOM_STATE_ICON_INDEX_OFFSET 1)
endif()
# indeces used for referencing icon within the binary's resources and .rc file's IDI_ICON{i} entries 'i'
if(NOT DEFINED CUSTOM_STATE_ICON_LOCKED_INDEX)
set(CUSTOM_STATE_ICON_LOCKED_INDEX 1)
endif()
if(NOT DEFINED CUSTOM_STATE_ICON_SHARED_INDEX)
set(CUSTOM_STATE_ICON_SHARED_INDEX 2)
endif()
file(GLOB_RECURSE CUSTOM_STATE_ICONS_LOCKED "${custom_state_icons_path}/*-locked.png*")
get_filename_component(CUSTOM_STATE_ICON_LOCKED_NAME ${CUSTOM_STATE_ICON_LOCKED_PATH} NAME_WLE)
ecm_add_app_icon(CUSTOM_STATE_ICON_LOCKED_OUT ICONS "${CUSTOM_STATE_ICONS_LOCKED}" OUTFILE_BASENAME "${CUSTOM_STATE_ICON_LOCKED_NAME}" DO_NOT_GENERATE_RC_FILE TRUE)
file(GLOB_RECURSE CUSTOM_STATE_ICONS_SHARED "${custom_state_icons_path}/*-shared.png*")
get_filename_component(CUSTOM_STATE_ICON_SHARED_NAME ${CUSTOM_STATE_ICON_SHARED_PATH} NAME_WLE)
ecm_add_app_icon(CUSTOM_STATE_ICON_SHARED_OUT ICONS "${CUSTOM_STATE_ICONS_SHARED}" OUTFILE_BASENAME "${CUSTOM_STATE_ICON_SHARED_NAME}" DO_NOT_GENERATE_RC_FILE TRUE)
file(REMOVE "${CMAKE_CURRENT_BINARY_DIR}/${CFAPI_SHELL_EXTENSIONS_LIB_NAME}.rc.in")
file(APPEND "${CMAKE_CURRENT_BINARY_DIR}/${CFAPI_SHELL_EXTENSIONS_LIB_NAME}.rc.in" "IDI_ICON${CUSTOM_STATE_ICON_LOCKED_INDEX} ICON DISCARDABLE \"${CMAKE_CURRENT_BINARY_DIR}/${CUSTOM_STATE_ICON_LOCKED_NAME}.ico\"\n")
file(APPEND "${CMAKE_CURRENT_BINARY_DIR}/${CFAPI_SHELL_EXTENSIONS_LIB_NAME}.rc.in" "IDI_ICON${CUSTOM_STATE_ICON_SHARED_INDEX} ICON DISCARDABLE \"${CMAKE_CURRENT_BINARY_DIR}/${CUSTOM_STATE_ICON_SHARED_NAME}.ico\"\n")
add_custom_command(
OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${CFAPI_SHELL_EXTENSIONS_LIB_NAME}.rc"
COMMAND ${CMAKE_COMMAND}
ARGS -E copy "${CMAKE_CURRENT_BINARY_DIR}/${CFAPI_SHELL_EXTENSIONS_LIB_NAME}.rc.in" "${CMAKE_CURRENT_BINARY_DIR}/${CFAPI_SHELL_EXTENSIONS_LIB_NAME}.rc"
DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/${CUSTOM_STATE_ICON_LOCKED_NAME}.ico" "${CMAKE_CURRENT_BINARY_DIR}/${CUSTOM_STATE_ICON_SHARED_NAME}.ico"
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
)
message("CUSTOM_STATE_ICON_LOCKED_OUT: ${CUSTOM_STATE_ICON_LOCKED_OUT}")
message("CUSTOM_STATE_ICON_SHARED_OUT: ${CUSTOM_STATE_ICON_SHARED_OUT}")
#
# Windows SDK command-line tools require native paths
file(TO_NATIVE_PATH "${CMAKE_CURRENT_SOURCE_DIR}" MidleFileFolder)
set(GeneratedFilesPath "${CMAKE_CURRENT_BINARY_DIR}\\Generated")
set(MidlOutputPathHeader "${GeneratedFilesPath}\\CustomStateProvider.g.h")
set(MidlOutputPathTlb "${GeneratedFilesPath}\\CustomStateProvider.tlb")
set(MidlOutputPathWinmd "${GeneratedFilesPath}\\CustomStateProvider.winmd")
add_custom_target(CustomStateProviderImpl
DEPENDS ${MidlOutputPathHeader}
)
if(NOT DEFINED ENV{WindowsSdkDir})
message("Getting WindowsSdkDir from Registry")
get_filename_component(WindowsSdkDir "[HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows Kits\\Installed Roots;KitsRoot10]" ABSOLUTE)
else()
set(WindowsSdkDir $ENV{WindowsSdkDir})
message("Setting WindowsSdkDir from ENV{WindowsSdkDir")
endif()
# we need cmake path to work with subfolders
file(TO_CMAKE_PATH "${WindowsSdkDir}" WindowsSdkDir)
MACRO(SUBDIRLIST result curdir)
FILE(GLOB children RELATIVE ${curdir} ${curdir}/*)
SET(dirlist "")
FOREACH(child ${children})
IF(IS_DIRECTORY ${curdir}/${child})
LIST(APPEND dirlist ${child})
ENDIF()
ENDFOREACH()
SET(${result} ${dirlist})
ENDMACRO()
SUBDIRLIST(WindowsSdkList "${WindowsSdkDir}/bin")
# pick only dirs that start with 10.0
list(FILTER WindowsSdkList INCLUDE REGEX "10.0.")
# sort the list of subdirs and choose the latest
list(SORT WindowsSdkList ORDER ASCENDING)
list(GET WindowsSdkList -1 WindowsSdkLatest)
message("WindowsSdkLatest has been set to: ${WindowsSdkLatest}")
if(NOT WindowsSdkLatest)
message( FATAL_ERROR "Windows SDK not found")
endif()
SUBDIRLIST(listFoundationContracts "${WindowsSdkDir}/References/${WindowsSdkLatest}/Windows.Foundation.FoundationContract")
list(FILTER listFoundationContracts INCLUDE REGEX "[0-9]+\.")
list(SORT listFoundationContracts ORDER ASCENDING)
list(GET listFoundationContracts -1 WindowsFoundationContractVersion)
message("WindowsFoundationContractVersion has been set to: ${WindowsFoundationContractVersion}")
if(NOT WindowsFoundationContractVersion)
message( FATAL_ERROR "Windows Foundation Contract is not found in ${WindowsSdkLatest} SDK.")
endif()
SUBDIRLIST(listCloudFilesContracts "${WindowsSdkDir}/References/${WindowsSdkLatest}/Windows.Storage.Provider.CloudFilesContract")
list(FILTER listCloudFilesContracts INCLUDE REGEX "[0-9]+\.")
list(SORT listCloudFilesContracts ORDER ASCENDING)
list(GET listCloudFilesContracts -1 WindowsStorageProviderCloudFilesContractVersion)
message("WindowsStorageProviderCloudFilesContractVersion has been set to: ${WindowsStorageProviderCloudFilesContractVersion}")
if(NOT WindowsStorageProviderCloudFilesContractVersion)
message( FATAL_ERROR "Windows Storage Provider Cloud Files Contract is not found in ${WindowsSdkLatest} SDK.")
endif()
# we no longer need to work with sub folders, so convert the WindowsSdkDir to native path
file(TO_NATIVE_PATH ${WindowsSdkDir} WindowsSdkDir)
message("WindowsSdkDir has been set to: ${WindowsSdkDir}")
message("WindowsSdkList has been set to: ${WindowsSdkList}")
message("WindowsSdkLatest has been set to: ${WindowsSdkLatest}")
set(TargetPlatform "x64")
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
set(TargetPlatform "x64")
elseif(CMAKE_SIZEOF_VOID_P EQUAL 4)
set(TargetPlatform "x86")
endif()
set(WindowsSDKReferencesPath "${WindowsSdkDir}\\References\\${WindowsSdkLatest}")
set(WindowsSDKBinPathForTools "${WindowsSdkDir}\\bin\\${WindowsSdkLatest}\\${TargetPlatform}")
set(WindowsSDKMetadataDirectory "${WindowsSdkDir}\\UnionMetadata\\${WindowsSdkLatest}")
IF(NOT EXISTS "${WindowsSDKReferencesPath}" OR NOT IS_DIRECTORY "${WindowsSDKReferencesPath}")
message( FATAL_ERROR "Please install Windows SDK ${WindowsSdkLatest}")
ENDIF()
IF(NOT EXISTS "${WindowsSDKBinPathForTools}" OR NOT IS_DIRECTORY "${WindowsSDKBinPathForTools}")
message( FATAL_ERROR "Please install Windows SDK ${WindowsSdkLatest}")
ENDIF()
IF(NOT EXISTS "${WindowsSDKMetadataDirectory}" OR NOT IS_DIRECTORY "${WindowsSDKMetadataDirectory}")
message( FATAL_ERROR "Please install Windows SDK ${WindowsSdkLatest}")
ENDIF()
set(midlExe "${WindowsSDKBinPathForTools}\\midl.exe")
set(cppWinRtExe "${WindowsSDKBinPathForTools}\\cppwinrt.exe")
message("cppWinRtExe: ${cppWinRtExe}")
message("midlExe: ${midlExe}")
# use midl.exe and cppwinrt.exe to generate files for CustomStateProvider (WinRT class)
add_custom_command(OUTPUT ${MidlOutputPathHeader}
COMMAND ${midlExe} /winrt /h nul /tlb ${MidlOutputPathTlb} /winmd ${MidlOutputPathWinmd} /metadata_dir "${WindowsSDKReferencesPath}\\Windows.Foundation.FoundationContract\\${WindowsFoundationContractVersion}" /nomidl /reference "${WindowsSDKReferencesPath}\\Windows.Foundation.FoundationContract\\${WindowsFoundationContractVersion}\\Windows.Foundation.FoundationContract.winmd" /reference "${WindowsSDKReferencesPath}\\Windows.Storage.Provider.CloudFilesContract\\${WindowsStorageProviderCloudFilesContractVersion}\\Windows.Storage.Provider.CloudFilesContract.winmd" /I ${MidleFileFolder} customstateprovider.idl
COMMAND ${cppWinRtExe} -in ${MidlOutputPathWinmd} -comp ${GeneratedFilesPath} -pch pch.h -ref ${WindowsSDKMetadataDirectory} -out ${GeneratedFilesPath} -verbose
COMMENT "Creating generated files from customstateprovider.idl"
)
add_library(CfApiShellExtensions MODULE
dllmain.cpp
cfapishellintegrationclassfactory.cpp
customstateprovideripc.cpp
ipccommon.cpp
thumbnailprovider.cpp
thumbnailprovideripc.cpp
${CMAKE_SOURCE_DIR}/src/common/shellextensionutils.cpp
customstateprovider.cpp
CfApiShellIntegration.def
)
target_link_libraries(CfApiShellExtensions shlwapi Gdiplus Nextcloud::csync Qt5::Core Qt5::Network)
message("CUSTOM_STATE_ICON_LOCKED_OUT: ${CUSTOM_STATE_ICON_LOCKED_OUT}")
message("CUSTOM_STATE_ICON_SHARED_OUT: ${CUSTOM_STATE_ICON_SHARED_OUT}")
if (CUSTOM_STATE_ICON_LOCKED_OUT AND CUSTOM_STATE_ICON_SHARED_OUT)
message("Adding ${CMAKE_CURRENT_BINARY_DIR}/${CFAPI_SHELL_EXTENSIONS_LIB_NAME}.rc...")
target_sources(CfApiShellExtensions PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/${CFAPI_SHELL_EXTENSIONS_LIB_NAME}.rc")
else()
message(WARNING "Could not add ${CMAKE_CURRENT_BINARY_DIR}/${CFAPI_SHELL_EXTENSIONS_LIB_NAME}.rc to CfApiShellExtensions. Custom states for Windows Virtual Files won't work.")
endif()
add_dependencies(CfApiShellExtensions CustomStateProviderImpl)
target_link_libraries(CfApiShellExtensions shlwapi Gdiplus onecoreuap Nextcloud::csync Qt5::Core Qt5::Network)
target_include_directories(CfApiShellExtensions PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_include_directories(CfApiShellExtensions PRIVATE ${GeneratedFilesPath})
target_include_directories(CfApiShellExtensions PRIVATE ${CMAKE_SOURCE_DIR})
target_compile_features(CfApiShellExtensions PRIVATE cxx_std_17)
set_target_properties(CfApiShellExtensions
PROPERTIES
LIBRARY_OUTPUT_NAME
@ -29,3 +208,5 @@ install(TARGETS CfApiShellExtensions
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_BINDIR}
)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/configvfscfapishellext.h.in ${CMAKE_CURRENT_BINARY_DIR}/configvfscfapishellext.h)

View File

@ -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();
}
}

View File

@ -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

View File

@ -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;
}

View File

@ -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>
{
};
}

View File

@ -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 = {};
}

View File

@ -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;
};
}

View File

@ -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();

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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[] = {

View File

@ -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);

View File

@ -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)

View File

@ -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);
}
}
}

View File

@ -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()

View File

@ -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"

View File

@ -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>

After

Width:  |  Height:  |  Size: 268 B

View File

@ -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>

After

Width:  |  Height:  |  Size: 290 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 B