ソースを参照

Implement 'Leave this share' context menu entry. Fix incorrect sharing state for incoming and my shares in custom state icons and local database.

Signed-off-by: allexzander <blackslayer4@gmail.com>
allexzander 3 年 前
コミット
6affc6e6ab

+ 10 - 6
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, isShared, lastShareStateFetchedTimestmap " \
+        "  lock, lockOwnerDisplayName, lockOwnerId, lockType, lockOwnerEditor, lockTime, lockTimeout, isShared, lastShareStateFetchedTimestmap, sharedByMe" \
         " FROM metadata" \
         "  LEFT JOIN checksumtype as contentchecksumtype ON metadata.contentChecksumTypeId == contentchecksumtype.id"
 
@@ -75,7 +75,8 @@ static void fillFileRecordFromGetQuery(SyncJournalFileRecord &rec, SqlQuery &que
     rec._lockstate._lockTime = query.int64Value(17);
     rec._lockstate._lockTimeout = query.int64Value(18);
     rec._isShared = query.intValue(19) > 0;
-    rec._lastShareStateFetchedTimestmap = query.int64Value(20);
+    rec._lastShareStateFetchedTimestamp = query.int64Value(20);
+    rec._sharedByMe = query.intValue(21) > 0;
 }
 
 static QByteArray defaultJournalMode(const QString &dbPath)
@@ -731,6 +732,7 @@ bool SyncJournalDb::updateMetadataTableStructure()
     addColumn(QStringLiteral("isE2eEncrypted"), QStringLiteral("INTEGER"));
     addColumn(QStringLiteral("isShared"), QStringLiteral("INTEGER"));
     addColumn(QStringLiteral("lastShareStateFetchedTimestmap"), QStringLiteral("INTEGER"));
+    addColumn(QStringLiteral("sharedByMe"), QStringLiteral("INTEGER"));
 
     auto uploadInfoColumns = tableColumns("uploadinfo");
     if (uploadInfoColumns.isEmpty())
@@ -894,8 +896,9 @@ Result<void, QString> SyncJournalDb::setFileRecord(const SyncJournalFileRecord &
                  << "lock owner:" << record._lockstate._lockOwnerDisplayName
                  << "lock owner id:" << record._lockstate._lockOwnerId
                  << "lock editor:" << record._lockstate._lockEditorApp
+                 << "sharedByMe:" << record._sharedByMe
                  << "isShared:" << record._isShared
-                 << "lastShareStateFetchedTimestmap:" << record._lastShareStateFetchedTimestmap;
+                 << "lastShareStateFetchedTimestamp:" << record._lastShareStateFetchedTimestamp;
 
     const qint64 phash = getPHash(record._path);
     if (!checkConnect()) {
@@ -921,8 +924,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, 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);"),
+                                                                                                        "lockOwnerEditor, lockTime, lockTimeout, isShared, lastShareStateFetchedTimestmap, sharedByMe) "
+                                                                                                        "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, ?28);"),
         _db);
     if (!query) {
         return query->error();
@@ -954,7 +957,8 @@ Result<void, QString> SyncJournalDb::setFileRecord(const SyncJournalFileRecord &
     query->bindValue(24, record._lockstate._lockTime);
     query->bindValue(25, record._lockstate._lockTimeout);
     query->bindValue(26, record._isShared);
-    query->bindValue(27, record._lastShareStateFetchedTimestmap);
+    query->bindValue(27, record._lastShareStateFetchedTimestamp);
+    query->bindValue(28, record._sharedByMe);
 
     if (!query->exec()) {
         return query->error();

+ 2 - 1
src/common/syncjournalfilerecord.h

@@ -82,7 +82,8 @@ public:
     bool _isE2eEncrypted = false;
     SyncJournalFileLockInfo _lockstate;
     bool _isShared = false;
-    qint64 _lastShareStateFetchedTimestmap = 0;
+    qint64 _lastShareStateFetchedTimestamp = 0;
+    bool _sharedByMe = false;
 };
 
 bool OCSYNC_EXPORT

+ 15 - 0
src/gui/folderman.cpp

@@ -1451,6 +1451,21 @@ void FolderMan::setDirtyNetworkLimits()
     }
 }
 
+void FolderMan::leaveShare(const QString &localFile)
+{
+    if (const auto folder = FolderMan::instance()->folderForPath(localFile)) {
+        const auto filePathRelative = QString(localFile).remove(folder->path());
+
+        const auto leaveShareJob = new SimpleApiJob(folder->accountState()->account(), folder->accountState()->account()->davPath() + filePathRelative);
+        leaveShareJob->setVerb(SimpleApiJob::Verb::Delete);
+        connect(leaveShareJob, &SimpleApiJob::resultReceived, this, [this, folder](int statusCode) {
+            Q_UNUSED(statusCode)
+            scheduleFolder(folder);
+        });
+        leaveShareJob->start();
+    }
+}
+
 void FolderMan::trayOverallStatus(const QList<Folder *> &folders,
     SyncResult::Status *status, bool *unresolvedConflicts)
 {

+ 3 - 0
src/gui/folderman.h

@@ -215,6 +215,9 @@ public:
     void setDirtyProxy();
     void setDirtyNetworkLimits();
 
+    /** removes current user from the share **/
+    void leaveShare(const QString &localFile);
+
 signals:
     /**
       * signal to indicate a folder has changed its sync state.

+ 85 - 142
src/gui/shellextensionsserver.cpp

@@ -19,7 +19,6 @@
 #include <libsync/vfs/cfapi/shellext/configvfscfapishellext.h>
 #include "folder.h"
 #include "folderman.h"
-#include "ocssharejob.h"
 #include <QDir>
 #include <QJsonArray>
 #include <QJsonDocument>
@@ -28,7 +27,6 @@
 
 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 {
@@ -102,6 +100,7 @@ void ShellExtensionsServer::processCustomStateRequest(QLocalSocket *socket, cons
         sendEmptyDataAndCloseSession(socket);
         return;
     }
+
     const auto filePathRelative = QString(customStateRequestInfo.path).remove(folder->path());
 
     SyncJournalFileRecord record;
@@ -123,43 +122,14 @@ void ShellExtensionsServer::processCustomStateRequest(QLocalSocket *socket, cons
             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.";
+    if (QDateTime::currentMSecsSinceEpoch() - record._lastShareStateFetchedTimestamp < _isSharedInvalidationInterval) {
+        qCInfo(lcShellExtServer) << record.path() << " record._lastShareStateFetchedTimestamp 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 lsColJobPath = [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);
@@ -170,13 +140,88 @@ void ShellExtensionsServer::processCustomStateRequest(QLocalSocket *socket, cons
         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;
+    if (_runningLsColJobsForPaths.contains(lsColJobPath)) {
+        qCInfo(lcShellExtServer) << "LsColJob is already running for path: " << lsColJobPath;
+        sendJsonMessageWithVersion(socket, composeMessageReplyFromRecord(record));
+        closeSession(socket);
+        return;
     }
+
+    _customStateSocketConnections.insert(socket->socketDescriptor(), QObject::connect(this, &ShellExtensionsServer::directoryListingIterationFinished, [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 LsColJob for socket: " << socket->socketDescriptor() << " and record: " << record.path();
+        sendJsonMessageWithVersion(socket, composeMessageReplyFromRecord(record));
+        closeSession(socket);
+    }));
+
+    auto *const lsColJob = new LsColJob(folder->accountState()->account(), QDir::cleanPath(folder->remotePath() + lsColJobPath), this);
+    lsColJob->setProperties({QByteArrayLiteral("http://owncloud.org/ns:share-types"), QByteArrayLiteral("http://owncloud.org/ns:permissions")});
+
+    const auto folderAlias = customStateRequestInfo.folderAlias;
+
+    QObject::connect(lsColJob, &LsColJob::directoryListingIterated, this, [this, folderAlias, lsColJobPath](const QString &name, const QMap<QString, QString> &properties) {
+        const auto folder = FolderMan::instance()->folder(folderAlias);
+
+        if (!folder) {
+            qCWarning(lcShellExtServer) << "No folder found for folderAlias: " << folderAlias;
+            return;
+        }
+
+        SyncJournalFileRecord record;
+        const auto filePathWithoutDavPath = QString(name).remove(folder->accountState()->account()->davPathRoot());
+        const auto filePathAdjusted = (filePathWithoutDavPath.size() > 1 && filePathWithoutDavPath.startsWith(QLatin1Char('/'))) ? filePathWithoutDavPath.mid(1) : filePathWithoutDavPath;
+        if (filePathAdjusted.isEmpty() || filePathAdjusted == lsColJobPath) {
+            // we are skipping the first item as it is the current path, but we are interested in nested items
+            return;
+        }
+        if (!folder || !folder->journalDb()->getFileRecord(filePathAdjusted, &record) || !record.isValid()) {
+            return;
+        }
+
+        const auto isIncomingShare = properties.contains(QStringLiteral("permissions")) && RemotePermissions::fromServerString(properties.value(QStringLiteral("permissions"))).hasPermission(OCC::RemotePermissions::IsShared);
+
+        const auto sharedByMe = !properties.value(QStringLiteral("share-types")).isEmpty();
+
+        record._sharedByMe = sharedByMe;
+
+        record._isShared = isIncomingShare || sharedByMe;
+        record._lastShareStateFetchedTimestamp = QDateTime::currentMSecsSinceEpoch();
+
+        if (!folder->journalDb()->setFileRecord(record)) {
+            qCWarning(lcShellExtServer) << "Could not set file record for path: " << record._path;
+            emit directoryListingIterationFinished(folderAlias);
+            return;
+        }
+    });
+
+    QObject::connect(lsColJob, &LsColJob::finishedWithError, this, [this, folderAlias, lsColJobPath](QNetworkReply *reply) {
+        _runningLsColJobsForPaths.removeOne(lsColJobPath);
+        const auto httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+        qCWarning(lcShellExtServer) << "LSCOL job error" << reply->errorString() << httpCode << reply->error();
+        emit directoryListingIterationFinished(folderAlias);
+    });
+
+    QObject::connect(lsColJob, &LsColJob::finishedWithoutError, this, [this, folderAlias, lsColJobPath]() {
+        _runningLsColJobsForPaths.removeOne(lsColJobPath);
+        emit directoryListingIterationFinished(folderAlias);
+    });
+    
+    _runningLsColJobsForPaths.push_back(lsColJobPath);
+    lsColJob->start();
 }
 
 void ShellExtensionsServer::processThumbnailRequest(QLocalSocket *socket, const ThumbnailRequestInfo &thumbnailRequestInfo)
@@ -252,108 +297,6 @@ void ShellExtensionsServer::slotNewConnection()
     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();

+ 3 - 4
src/gui/shellextensionsserver.h

@@ -22,6 +22,7 @@
 
 class QJsonDocument;
 class QLocalSocket;
+class QNetworkReply;
 
 namespace OCC {
 class ShellExtensionsServer : public QObject
@@ -53,7 +54,7 @@ public:
     void setIsSharedInvalidationInterval(qint64 interval);
 
 signals:
-    void fetchSharesJobFinished(const QString &folderAlias);
+    void directoryListingIterationFinished(const QString &folderAlias);
 
 private:
     void sendJsonMessageWithVersion(QLocalSocket *socket, const QVariantMap &message);
@@ -67,12 +68,10 @@ private:
 
 private slots:
     void slotNewConnection();
-    void slotSharesFetched(const QJsonDocument &reply);
-    void slotSharesFetchError(int statusCode, const QString &message);
 
 private:
     QLocalServer _localServer;
-    QStringList _runningFetchShareJobsForPaths;
+    QStringList _runningLsColJobsForPaths;
     QMap<qintptr, QMetaObject::Connection> _customStateSocketConnections;
     qint64 _isSharedInvalidationInterval = 0;
 };

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

@@ -23,6 +23,7 @@
 
 #include "config.h"
 #include "configfile.h"
+#include "deletejob.h"
 #include "folderman.h"
 #include "folder.h"
 #include "theme.h"
@@ -541,6 +542,12 @@ void SocketApi::processShareRequest(const QString &localFile, SocketListener *li
     }
 }
 
+void SocketApi::processLeaveShareRequest(const QString &localFile, SocketListener *listener)
+{
+    Q_UNUSED(listener)
+    FolderMan::instance()->leaveShare(QDir::fromNativeSeparators(localFile));
+}
+
 void SocketApi::broadcastStatusPushMessage(const QString &systemPath, SyncFileStatus fileStatus)
 {
     QString msg = buildMessage(QLatin1String("STATUS"), systemPath, fileStatus.toSocketAPIString());
@@ -584,6 +591,11 @@ void SocketApi::command_SHARE(const QString &localFile, SocketListener *listener
     processShareRequest(localFile, listener);
 }
 
+void SocketApi::command_LEAVESHARE(const QString &localFile, SocketListener *listener)
+{
+    processLeaveShareRequest(localFile, listener);
+}
+
 void SocketApi::command_ACTIVITY(const QString &localFile, SocketListener *listener)
 {
     Q_UNUSED(listener);
@@ -1047,6 +1059,10 @@ void SocketApi::sendSharingContextMenuOptions(const FileData &fileData, SocketLi
     if (!capabilities.shareAPI() || !(theme->userGroupSharing() || (theme->linkSharing() && capabilities.sharePublicLink())))
         return;
 
+    if (record._isShared && !record._sharedByMe) {
+        listener->sendMessage(QLatin1String("MENU_ITEM:LEAVESHARE") + flagString + tr("Leave this share"));
+    }
+
     // If sharing is globally disabled, do not show any sharing entries.
     // If there is no permission to share for this file, add a disabled entry saying so
     if (isOnTheServer && !record._remotePerm.isNull() && !record._remotePerm.hasPermission(RemotePermissions::CanReshare)) {

+ 9 - 9
src/gui/socketapi/socketapi.h

@@ -15,9 +15,9 @@
 #ifndef SOCKETAPI_H
 #define SOCKETAPI_H
 
-#include "syncfileitem.h"
 #include "common/syncfilestatus.h"
 #include "common/syncjournalfilerecord.h"
+#include "syncfileitem.h"
 
 #include "config.h"
 
@@ -28,8 +28,8 @@ class QLocalSocket;
 class QStringList;
 class QFileInfo;
 
-namespace OCC {
-
+namespace OCC
+{
 class SyncFileStatus;
 class Folder;
 class SocketListener;
@@ -102,6 +102,7 @@ private:
 
     // opens share dialog, sends reply
     void processShareRequest(const QString &localFile, SocketListener *listener);
+    void processLeaveShareRequest(const QString &localFile, SocketListener *listener);
     void processFileActivityRequest(const QString &localFile);
 
     Q_INVOKABLE void command_RETRIEVE_FOLDER_STATUS(const QString &argument, OCC::SocketListener *listener);
@@ -114,6 +115,7 @@ private:
     // The context menu actions
     Q_INVOKABLE void command_ACTIVITY(const QString &localFile, OCC::SocketListener *listener);
     Q_INVOKABLE void command_SHARE(const QString &localFile, OCC::SocketListener *listener);
+    Q_INVOKABLE void command_LEAVESHARE(const QString &localFile, SocketListener *listener);
     Q_INVOKABLE void command_MANAGE_PUBLIC_LINKS(const QString &localFile, OCC::SocketListener *listener);
     Q_INVOKABLE void command_COPY_PUBLIC_LINK(const QString &localFile, OCC::SocketListener *listener);
     Q_INVOKABLE void command_COPY_PRIVATE_LINK(const QString &localFile, OCC::SocketListener *listener);
@@ -149,15 +151,13 @@ private:
     // Sends the context menu options relating to sharing to listener
     void sendSharingContextMenuOptions(const FileData &fileData, SocketListener *listener, bool enabled);
 
-    void sendLockFileCommandMenuEntries(const QFileInfo &fileInfo,
-                                        Folder * const syncFolder,
-                                        const FileData &fileData,
-                                        const SocketListener * const listener) const;
+    void
+    sendLockFileCommandMenuEntries(const QFileInfo &fileInfo, Folder *const syncFolder, const FileData &fileData, const SocketListener *const listener) const;
 
     void sendLockFileInfoMenuEntries(const QFileInfo &fileInfo,
-                                     Folder * const syncFolder,
+                                     Folder* const syncFolder,
                                      const FileData &fileData,
-                                     const SocketListener * const listener,
+                                     const SocketListener* const listener,
                                      const SyncJournalFileRecord &record) const;
 
     /** Send the list of menu item. (added in version 1.1)

+ 6 - 1
src/libsync/account.cpp

@@ -96,7 +96,12 @@ Account::~Account() = default;
 
 QString Account::davPath() const
 {
-    return davPathBase() + QLatin1Char('/') + davUser() + QLatin1Char('/');
+    return davPathRoot() + QLatin1Char('/');
+}
+
+QString Account::davPathRoot() const
+{
+    return davPathBase() + QLatin1Char('/') + davUser();
 }
 
 void Account::setSharedThis(AccountPtr sharedThis)

+ 7 - 0
src/libsync/account.h

@@ -141,6 +141,13 @@ public:
      */
     [[nodiscard]] QString davPath() const;
 
+    /**
+     * @brief The possibly themed dav path root for the account. It has
+     *        no trailing slash.
+     * @returns the (themeable) dav path for the account.
+     */
+    [[nodiscard]] QString davPathRoot() const;
+
     /** Returns webdav entry URL, based on url() */
     [[nodiscard]] QUrl davUrl() const;
 

+ 2 - 2
src/libsync/bulkpropagatorjob.cpp

@@ -393,8 +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();
+    singleFile._item->_isShared = singleFile._item->_remotePerm.hasPermission(RemotePermissions::IsShared) || singleFile._item->_sharedByMe;
+    singleFile._item->_lastShareStateFetchedTimestamp = QDateTime::currentMSecsSinceEpoch();
 
     if (getHeaderFromJsonReply(fileReply, "X-OC-MTime") != "accepted") {
         // X-OC-MTime is supported since owncloud 5.0.   But not when chunking.

+ 8 - 5
src/libsync/discovery.cpp

@@ -475,8 +475,9 @@ 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->_isShared = serverEntry.remotePerm.hasPermission(RemotePermissions::IsShared) || serverEntry.sharedByMe;
+    item->_sharedByMe = serverEntry.sharedByMe;
+    item->_lastShareStateFetchedTimestamp = QDateTime::currentMSecsSinceEpoch();
     item->_type = serverEntry.isDirectory ? ItemTypeDirectory : ItemTypeFile;
     item->_etag = serverEntry.etag;
     item->_directDownloadUrl = serverEntry.directDownloadUrl;
@@ -1280,7 +1281,8 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
         item->_fileId = base._fileId;
         item->_remotePerm = base._remotePerm;
         item->_isShared = base._isShared;
-        item->_lastShareStateFetchedTimestmap = base._lastShareStateFetchedTimestmap;
+        item->_sharedByMe = base._sharedByMe;
+        item->_lastShareStateFetchedTimestamp = base._lastShareStateFetchedTimestamp;
         item->_etag = base._etag;
         item->_type = base._type;
 
@@ -1406,8 +1408,9 @@ 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._isShared = serverEntry.remotePerm.hasPermission(RemotePermissions::IsShared) || serverEntry.sharedByMe;
+            rec._sharedByMe = serverEntry.sharedByMe;
+            rec._lastShareStateFetchedTimestamp = QDateTime::currentMSecsSinceEpoch();
             rec._checksumHeader = serverEntry.checksumHeader;
             const auto result = _discoveryData->_statedb->setFileRecord(rec);
             if (!result) {

+ 1 - 0
src/libsync/discoveryphase.cpp

@@ -451,6 +451,7 @@ static void propertyMapToRemoteInfo(const QMap<QString, QString> &map, RemoteInf
                 // if we are the owner or not.
                 // Piggy back on the persmission field
                 result.remotePerm.setPermission(RemotePermissions::IsShared);
+                result.sharedByMe = true;
             }
         } else if (property == "is-encrypted" && value == QStringLiteral("1")) {
             result.isE2eEncrypted = true;

+ 1 - 0
src/libsync/discoveryphase.h

@@ -68,6 +68,7 @@ struct RemoteInfo
     bool isDirectory = false;
     bool isE2eEncrypted = false;
     QString e2eMangledName;
+    bool sharedByMe = false;
 
     [[nodiscard]] bool isValid() const { return !name.isNull(); }
 

+ 4 - 3
src/libsync/propagateremotemkdir.cpp

@@ -140,12 +140,13 @@ void PropagateRemoteMkdir::finalizeMkColJob(QNetworkReply::NetworkError err, con
 
     propagator()->_activeJobList.append(this);
     auto propfindJob = new PropfindJob(propagator()->account(), jobPath, this);
-    propfindJob->setProperties({"http://owncloud.org/ns:permissions"});
+    propfindJob->setProperties({QByteArrayLiteral("http://owncloud.org/ns:share-types"), QByteArrayLiteral("http://owncloud.org/ns:permissions")});
     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();
+        _item->_sharedByMe = !result.value(QStringLiteral("share-types")).toString().isEmpty();
+        _item->_isShared = _item->_remotePerm.hasPermission(RemotePermissions::IsShared) || _item->_sharedByMe;
+        _item->_lastShareStateFetchedTimestamp = QDateTime::currentMSecsSinceEpoch();
 
         if (!_uploadEncryptedHelper && !_item->_isEncrypted) {
             success();

+ 4 - 2
src/libsync/syncfileitem.cpp

@@ -42,7 +42,8 @@ SyncJournalFileRecord SyncFileItem::toSyncJournalFileRecordWithInode(const QStri
     rec._fileSize = _size;
     rec._remotePerm = _remotePerm;
     rec._isShared = _isShared;
-    rec._lastShareStateFetchedTimestmap = _lastShareStateFetchedTimestmap;
+    rec._sharedByMe = _sharedByMe;
+    rec._lastShareStateFetchedTimestamp = _lastShareStateFetchedTimestamp;
     rec._serverHasIgnoredFiles = _serverHasIgnoredFiles;
     rec._checksumHeader = _checksumHeader;
     rec._e2eMangledName = _encryptedFileName.toUtf8();
@@ -91,8 +92,9 @@ SyncFileItemPtr SyncFileItem::fromSyncJournalFileRecord(const SyncJournalFileRec
     item->_lockEditorApp = rec._lockstate._lockEditorApp;
     item->_lockTime = rec._lockstate._lockTime;
     item->_lockTimeout = rec._lockstate._lockTimeout;
+    item->_sharedByMe = rec._sharedByMe;
     item->_isShared = rec._isShared;
-    item->_lastShareStateFetchedTimestmap = rec._lastShareStateFetchedTimestmap;
+    item->_lastShareStateFetchedTimestamp = rec._lastShareStateFetchedTimestamp;
     return item;
 }
 

+ 3 - 1
src/libsync/syncfileitem.h

@@ -310,7 +310,9 @@ public:
     qint64 _lockTimeout = 0;
 
     bool _isShared = false;
-    time_t _lastShareStateFetchedTimestmap = 0;
+    time_t _lastShareStateFetchedTimestamp = 0;
+
+    bool _sharedByMe = false;
 };
 
 inline bool operator<(const SyncFileItemPtr &item1, const SyncFileItemPtr &item2)

+ 101 - 1
test/testfolderman.cpp

@@ -13,7 +13,9 @@
 #include "folderman.h"
 #include "account.h"
 #include "accountstate.h"
+#include <accountmanager.h>
 #include "configfile.h"
+#include "syncenginetestutils.h"
 #include "testhelper.h"
 
 using namespace OCC;
@@ -24,7 +26,105 @@ class TestFolderMan: public QObject
 
     FolderMan _fm;
 
+signals:
+    void incomingShareDeleted();
+
 private slots:
+    void testLeaveShare()
+    {
+        constexpr auto firstSharePath = "A/sharedwithme_A.txt";
+        constexpr auto secondSharePath = "A/B/sharedwithme_B.data";
+
+        QScopedPointer<FakeQNAM> fakeQnam(new FakeQNAM({}));
+        OCC::AccountPtr account = OCC::Account::create();
+        account->setCredentials(new FakeCredentials{fakeQnam.data()});
+        account->setUrl(QUrl(("http://example.de")));
+        OCC::AccountManager::instance()->addAccount(account);
+
+        FakeFolder fakeFolder{FileInfo{}};
+        fakeFolder.remoteModifier().mkdir("A");
+
+        fakeFolder.remoteModifier().insert(firstSharePath, 100);
+        const auto firstShare = fakeFolder.remoteModifier().find(firstSharePath);
+        QVERIFY(firstShare);
+        firstShare->permissions.setPermission(OCC::RemotePermissions::IsShared);
+
+        fakeFolder.remoteModifier().mkdir("A/B");
+
+        fakeFolder.remoteModifier().insert(secondSharePath, 100);
+        const auto secondShare = fakeFolder.remoteModifier().find(secondSharePath);
+        QVERIFY(secondShare);
+        secondShare->permissions.setPermission(OCC::RemotePermissions::IsShared);
+
+        FolderMan *folderman = FolderMan::instance();
+        QCOMPARE(folderman, &_fm);
+        OCC::AccountState *accountState = OCC::AccountManager::instance()->accounts().first().data();
+        const auto folder = folderman->addFolder(accountState, folderDefinition(fakeFolder.localPath()));
+        QVERIFY(folder);
+
+        auto realFolder = FolderMan::instance()->folderForPath(fakeFolder.localPath());
+        QVERIFY(realFolder);
+
+        QVERIFY(fakeFolder.syncOnce());
+        QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+        fakeQnam->setOverride([this, accountState, &fakeFolder](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) {
+            Q_UNUSED(device);
+            QNetworkReply *reply = nullptr;
+
+            if (op != QNetworkAccessManager::DeleteOperation) {
+                reply = new FakeErrorReply(op, req, this, 405);
+                return reply;
+            }
+
+            if (req.url().path().isEmpty()) {
+                reply = new FakeErrorReply(op, req, this, 404);
+                return reply;
+            }
+
+            const auto filePathRelative = req.url().path().remove(accountState->account()->davPath());
+
+            const auto foundFileInRemoteFolder = fakeFolder.remoteModifier().find(filePathRelative);
+
+            if (filePathRelative.isEmpty() || !foundFileInRemoteFolder) {
+                reply = new FakeErrorReply(op, req, this, 404);
+                return reply;
+            }
+
+           fakeFolder.remoteModifier().remove(filePathRelative);
+           reply = new FakePayloadReply(op, req, {}, nullptr);
+
+           emit incomingShareDeleted();
+           
+           return reply;
+        });
+
+        QSignalSpy incomingShareDeletedSignal(this, &TestFolderMan::incomingShareDeleted);
+
+        // verify first share gets deleted
+        folderman->leaveShare(fakeFolder.localPath() + firstSharePath);
+        QCOMPARE(incomingShareDeletedSignal.count(), 1);
+        QVERIFY(!fakeFolder.remoteModifier().find(firstSharePath));
+        QVERIFY(fakeFolder.syncOnce());
+        QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+        // verify no share gets deleted
+        folderman->leaveShare(fakeFolder.localPath() + "A/B/notsharedwithme_B.data");
+        QCOMPARE(incomingShareDeletedSignal.count(), 1);
+        QVERIFY(fakeFolder.remoteModifier().find("A/B/sharedwithme_B.data"));
+        QVERIFY(fakeFolder.syncOnce());
+        QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+        // verify second share gets deleted
+        folderman->leaveShare(fakeFolder.localPath() + secondSharePath);
+        QCOMPARE(incomingShareDeletedSignal.count(), 2);
+        QVERIFY(!fakeFolder.remoteModifier().find(secondSharePath));
+        QVERIFY(fakeFolder.syncOnce());
+        QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+        OCC::AccountManager::instance()->deleteAccount(accountState);
+    }
+
     void testCheckPathValidityForNewFolder()
     {
 #ifdef Q_OS_WIN
@@ -210,5 +310,5 @@ private slots:
     }
 };
 
-QTEST_APPLESS_MAIN(TestFolderMan)
+QTEST_GUILESS_MAIN(TestFolderMan)
 #include "testfolderman.moc"