properly compute if a folder is top level or child extern mounted
authorMatthieu Gallien <matthieu.gallien@nextcloud.com>
Mon, 25 Mar 2024 21:06:11 +0000 (22:06 +0100)
committerMatthieu Gallien <matthieu.gallien@nextcloud.com>
Wed, 27 Mar 2024 13:29:01 +0000 (14:29 +0100)
asks new permission to server to be able to know if a folder is a top
level mounted folder

should allow detecting the top level folders from external storages or
group folders

should also make the client reliably detect that it is handling a child
folder inside a group folder and be allowed to rename such folders

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
16 files changed:
VERSION.cmake
src/common/remotepermissions.cpp
src/common/remotepermissions.h
src/gui/editlocallyjob.cpp
src/gui/folderstatusmodel.cpp
src/gui/invalidfilenamedialog.cpp
src/gui/shellextensionsserver.cpp
src/libsync/account.cpp
src/libsync/account.h
src/libsync/caseclashconflictsolver.cpp
src/libsync/discovery.cpp
src/libsync/discoveryphase.cpp
src/libsync/propagateremotemkdir.cpp
src/libsync/syncfileitem.cpp
src/libsync/syncfileitem.h
version.h.in

index 481e7b8a1519f072b4111e04b0e49224dd264481..2045ea0ed63f8f28fc1a69b0b009aa4dcbbcf2cc 100644 (file)
@@ -13,6 +13,10 @@ set(NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_MAJOR 26)
 set(NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_MINOR 0)
 set(NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_PATCH 0)
 
+set(NEXTCLOUD_SERVER_VERSION_MOUNT_ROOT_PROPERTY_SUPPORTED_MAJOR 28)
+set(NEXTCLOUD_SERVER_VERSION_MOUNT_ROOT_PROPERTY_SUPPORTED_MINOR 0)
+set(NEXTCLOUD_SERVER_VERSION_MOUNT_ROOT_PROPERTY_SUPPORTED_PATCH 3)
+
 if ( NOT DEFINED MIRALL_VERSION_SUFFIX )
     set( MIRALL_VERSION_SUFFIX "git") #e.g. beta1, beta2, rc1
 endif( NOT DEFINED MIRALL_VERSION_SUFFIX )
index 00827f8f13f7514e849c8e753d365430503a4b04..bfe93922a679cf943020c169a797639648bf78bc 100644 (file)
  */
 
 #include "remotepermissions.h"
+
+#include <QLoggingCategory>
+
 #include <cstring>
 
 namespace OCC {
 
+Q_LOGGING_CATEGORY(lcRemotePermissions, "nextcloud.sync.remotepermissions", QtInfoMsg)
+
 static const char letters[] = " WDNVCKRSMm";
 
 
@@ -68,11 +73,43 @@ RemotePermissions RemotePermissions::fromDbValue(const QByteArray &value)
     return perm;
 }
 
-RemotePermissions RemotePermissions::fromServerString(const QString &value)
+template <typename T>
+RemotePermissions RemotePermissions::internalFromServerString(const QString &value,
+                                                              const T&otherProperties,
+                                                              MountedPermissionAlgorithm algorithm)
 {
     RemotePermissions perm;
     perm.fromArray(value.utf16());
+
+    if (algorithm == MountedPermissionAlgorithm::WildGuessMountedSubProperty) {
+        return perm;
+    }
+
+    if ((otherProperties.contains(QStringLiteral("is-mount-root")) && otherProperties.value(QStringLiteral("is-mount-root")) == QStringLiteral("false") && perm.hasPermission(RemotePermissions::IsMounted)) ||
+        (!otherProperties.contains(QStringLiteral("is-mount-root")) && perm.hasPermission(RemotePermissions::IsMounted))) {
+        /* All the entries in a external storage have 'M' in their permission. However, for all
+           purposes in the desktop client, we only need to know about the mount points.
+           So replace the 'M' by a 'm' for every sub entries in an external storage */
+        perm.unsetPermission(RemotePermissions::IsMounted);
+        perm.setPermission(RemotePermissions::IsMountedSub);
+        qCInfo(lcRemotePermissions()) << otherProperties.value(QStringLiteral("permissions")) << "replacing M permissions by m for subfolders inside a group folder";
+    }
+
     return perm;
 }
 
+RemotePermissions RemotePermissions::fromServerString(const QString &value,
+                                                      MountedPermissionAlgorithm algorithm,
+                                                      const QMap<QString, QString> &otherProperties)
+{
+    return internalFromServerString(value, otherProperties, algorithm);
+}
+
+RemotePermissions RemotePermissions::fromServerString(const QString &value,
+                                                      MountedPermissionAlgorithm algorithm,
+                                                      const QVariantMap &otherProperties)
+{
+    return internalFromServerString(value, otherProperties, algorithm);
+}
+
 } // namespace OCC
index 6d7c5663324c678d16b19f8f089b306d959be27e..a84c359d3b4468c9d60c4484c40b0e0a3644576f 100644 (file)
@@ -59,6 +59,11 @@ public:
         PermissionsCount = IsMountedSub
     };
 
+    enum class MountedPermissionAlgorithm {
+        UseMountRootProperty,
+        WildGuessMountedSubProperty,
+    };
+
     /// null permissions
     RemotePermissions() = default;
 
@@ -72,7 +77,14 @@ public:
     static RemotePermissions fromDbValue(const QByteArray &);
 
     /// read a permissions string received from the server, never null
-    static RemotePermissions fromServerString(const QString &);
+    static RemotePermissions fromServerString(const QString &value,
+                                              MountedPermissionAlgorithm algorithm = MountedPermissionAlgorithm::WildGuessMountedSubProperty,
+                                              const QMap<QString, QString> &otherProperties = {});
+
+    /// read a permissions string received from the server, never null
+    static RemotePermissions fromServerString(const QString &value,
+                                              MountedPermissionAlgorithm algorithm,
+                                              const QVariantMap &otherProperties = {});
 
     [[nodiscard]] bool hasPermission(Permissions p) const
     {
@@ -101,6 +113,13 @@ public:
     {
         return dbg << p.toString();
     }
+
+private:
+
+    template <typename T>
+    static RemotePermissions internalFromServerString(const QString &value,
+                                                      const T&otherProperties,
+                                                      MountedPermissionAlgorithm algorithm);
 };
 
 
index 167cf99c33cb90ea0b3fc920f42444bced613ecc..f4109a970d8c4e019d1c73acaac37e4be5f4f6c5 100644 (file)
@@ -213,7 +213,8 @@ void EditLocallyJob::fetchRemoteFileParentInfo()
                                   QByteArrayLiteral("http://owncloud.org/ns:size"),
                                   QByteArrayLiteral("http://owncloud.org/ns:id"),
                                   QByteArrayLiteral("http://owncloud.org/ns:permissions"),
-                                  QByteArrayLiteral("http://owncloud.org/ns:checksums")};
+                                  QByteArrayLiteral("http://owncloud.org/ns:checksums"),
+                                  QByteArrayLiteral("http://nextcloud.org/ns:is-mount-root")};
 
     job->setProperties(props);
     connect(job, &LsColJob::directoryListingIterated, this, &EditLocallyJob::slotDirectoryListingIterated);
@@ -545,7 +546,9 @@ void EditLocallyJob::slotDirectoryListingIterated(const QString &name, const QMa
         const auto cleanName = nameWithoutDavPath.startsWith(remoteFolderPathWithoutLeadingSlash)
             ? nameWithoutDavPath.mid(remoteFolderPathWithoutLeadingSlash.size()) : nameWithoutDavPath;
         disconnect(job, &LsColJob::directoryListingIterated, this, &EditLocallyJob::slotDirectoryListingIterated);
-        _fileParentItem = SyncFileItem::fromProperties(cleanName, properties);
+        _fileParentItem = SyncFileItem::fromProperties(cleanName,
+                                                       properties,
+                                                       _accountState->account()->serverHasMountRootProperty() ? RemotePermissions::MountedPermissionAlgorithm::UseMountRootProperty : RemotePermissions::MountedPermissionAlgorithm::WildGuessMountedSubProperty);
     }
 }
 
index cc3aede3617158e643a8b911c01546613a912f48..e21644aecd8f7e261f030f3434b21fbe55cada3c 100644 (file)
@@ -616,6 +616,7 @@ void FolderStatusModel::fetchMore(const QModelIndex &parent)
     auto props = QList<QByteArray>() << "resourcetype"
                                      << "http://owncloud.org/ns:size"
                                      << "http://owncloud.org/ns:permissions"
+                                     << "http://nextcloud.org/ns:is-mount-root"
                                      << "http://owncloud.org/ns:fileid";
     if (_accountState->account()->capabilities().clientSideEncryptionAvailable()) {
         props << "http://nextcloud.org/ns:is-encrypted";
index 235f29650e9dde1fabbe228d1ea51c5d4f65ecba..844f5b5e4bdf286bbe72ec62f2d4de0c59dc7727 100644 (file)
@@ -117,7 +117,7 @@ InvalidFilenameDialog::~InvalidFilenameDialog() = default;
 void InvalidFilenameDialog::checkIfAllowedToRename()
 {
     const auto propfindJob = new PropfindJob(_account, QDir::cleanPath(_folder->remotePath() + _originalFileName));
-    propfindJob->setProperties({ "http://owncloud.org/ns:permissions" });
+    propfindJob->setProperties({"http://owncloud.org/ns:permissions", "http://nextcloud.org/ns:is-mount-root"});
     connect(propfindJob, &PropfindJob::result, this, &InvalidFilenameDialog::onPropfindPermissionSuccess);
     connect(propfindJob, &PropfindJob::finishedWithError, this, &InvalidFilenameDialog::onPropfindPermissionError);
     propfindJob->start();
index 88c5571b8b128e12c47baaa662f336e6a1115e8d..1150a3107b7a0e260dfaa6a2c2631a95bf7d467b 100644 (file)
@@ -169,7 +169,7 @@ void ShellExtensionsServer::processCustomStateRequest(QLocalSocket *socket, cons
     }));
 
     auto *const lsColJob = new LsColJob(folder->accountState()->account(), QDir::cleanPath(folder->remotePath() + lsColJobPath));
-    lsColJob->setProperties({QByteArrayLiteral("http://owncloud.org/ns:share-types"), QByteArrayLiteral("http://owncloud.org/ns:permissions")});
+    lsColJob->setProperties({QByteArrayLiteral("http://owncloud.org/ns:share-types"), QByteArrayLiteral("http://owncloud.org/ns:permissions"), QByteArrayLiteral("http://nextcloud.org/ns:is-mount-root")});
 
     const auto folderAlias = customStateRequestInfo.folderAlias;
 
index 15712b11e165ff3091b5b931b57a70b439a7b282..9be583b49c8aa816c264ccd4f5dbd2aae11bb8d0 100644 (file)
@@ -731,6 +731,17 @@ int Account::serverVersionInt() const
         components.value(2).toInt());
 }
 
+bool Account::serverHasMountRootProperty() const
+{
+    if (serverVersionInt() == 0) {
+        return false;
+    }
+
+    return serverVersionInt() >= Account::makeServerVersion(NEXTCLOUD_SERVER_VERSION_MOUNT_ROOT_PROPERTY_SUPPORTED_MAJOR,
+                                                            NEXTCLOUD_SERVER_VERSION_MOUNT_ROOT_PROPERTY_SUPPORTED_MINOR,
+                                                            NEXTCLOUD_SERVER_VERSION_MOUNT_ROOT_PROPERTY_SUPPORTED_PATCH);
+}
+
 bool Account::serverVersionUnsupported() const
 {
     if (serverVersionInt() == 0) {
index d21748f872a1a877c7a50b7ae59be48839b4e93f..b7702e525fdf8ee3f6daf1a819f304f9924a83a6 100644 (file)
@@ -247,6 +247,8 @@ public:
      */
     [[nodiscard]] int serverVersionInt() const;
 
+    [[nodiscard]] bool serverHasMountRootProperty() const;
+
     static constexpr int makeServerVersion(const int majorVersion, const int minorVersion, const int patchVersion) {
         return (majorVersion << 16) + (minorVersion << 8) + patchVersion;
     };
index b93c06d6ba37be65ea853a2cd86b80ba70110233..753c2a7c0fc68fef908666849fb1053df8b26743 100644 (file)
@@ -220,7 +220,7 @@ void CaseClashConflictSolver::processLeadingOrTrailingSpacesError(const QString
 void CaseClashConflictSolver::checkIfAllowedToRename()
 {
     const auto propfindJob = new PropfindJob(_account, QDir::cleanPath(remoteTargetFilePath()));
-    propfindJob->setProperties({ "http://owncloud.org/ns:permissions" });
+    propfindJob->setProperties({"http://owncloud.org/ns:permissions", "http://nextcloud.org/ns:is-mount-root"});
     connect(propfindJob, &PropfindJob::result, this, &CaseClashConflictSolver::onPropfindPermissionSuccess);
     connect(propfindJob, &PropfindJob::finishedWithError, this, &CaseClashConflictSolver::onPropfindPermissionError);
     propfindJob->start();
index 2fc2b5bf26d8c0e9efa79f0324de38f5574bb46a..ef0644ca1426072630598177e85bfeae348277d3 100644 (file)
@@ -1438,14 +1438,18 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
 
     // Check local permission if we are allowed to put move the file here
     // Technically we should use the permissions from the server, but we'll assume it is the same
+    const auto serverHasMountRootProperty = _discoveryData->_account->serverHasMountRootProperty();
     const auto isExternalStorage = base._remotePerm.hasPermission(RemotePermissions::IsMounted);
     const auto movePerms = checkMovePermissions(base._remotePerm, originalPath, item->isDirectory());
-    if (!movePerms.sourceOk || !movePerms.destinationOk || isExternalStorage || isE2eeMoveOnlineOnlyItemWithCfApi) {
+    if (!movePerms.sourceOk || !movePerms.destinationOk || (serverHasMountRootProperty && isExternalStorage) || isE2eeMoveOnlineOnlyItemWithCfApi) {
         qCInfo(lcDisco) << "Move without permission to rename base file, "
                         << "source:" << movePerms.sourceOk
                         << ", target:" << movePerms.destinationOk
                         << ", targetNew:" << movePerms.destinationNewOk
-                        << ", isExternalStorage:" << isExternalStorage;
+                        << ", isExternalStorage:" << isExternalStorage
+                        << ", serverHasMountRootProperty:" << serverHasMountRootProperty
+                        << ", base._remotePerm:" << base._remotePerm.toString()
+                        << ", base.path():" << base.path();
 
         // If we can create the destination, do that.
         // Permission errors on the destination will be handled by checkPermissions later.
index fdf66e4058b025916a371d11f679b0fc46d11818..33516635d9384089354d8ee33916c5e3474c458c 100644 (file)
@@ -419,6 +419,7 @@ void DiscoverySingleDirectoryJob::start()
               << "http://nextcloud.org/ns:lock-time"
               << "http://nextcloud.org/ns:lock-timeout";
     }
+    props << "http://nextcloud.org/ns:is-mount-root";
 
     lsColJob->setProperties(props);
 
@@ -458,7 +459,7 @@ SyncFileItem::EncryptionStatus DiscoverySingleDirectoryJob::requiredEncryptionSt
     return _encryptionStatusRequired;
 }
 
-static void propertyMapToRemoteInfo(const QMap<QString, QString> &map, RemoteInfo &result)
+static void propertyMapToRemoteInfo(const QMap<QString, QString> &map, RemotePermissions::MountedPermissionAlgorithm algorithm, RemoteInfo &result)
 {
     for (auto it = map.constBegin(); it != map.constEnd(); ++it) {
         QString property = it.key();
@@ -490,7 +491,7 @@ static void propertyMapToRemoteInfo(const QMap<QString, QString> &map, RemoteInf
         } else if (property == "dDC") {
             result.directDownloadCookies = value;
         } else if (property == "permissions") {
-            result.remotePerm = RemotePermissions::fromServerString(value);
+            result.remotePerm = RemotePermissions::fromServerString(value, algorithm, map);
         } else if (property == "checksums") {
             result.checksumHeader = findBestChecksum(value.toUtf8());
         } else if (property == "share-types" && !value.isEmpty()) {
@@ -560,7 +561,10 @@ void DiscoverySingleDirectoryJob::directoryListingIteratedSlot(const QString &fi
         // The first entry is for the folder itself, we should process it differently.
         _ignoredFirst = true;
         if (map.contains("permissions")) {
-            auto perm = RemotePermissions::fromServerString(map.value("permissions"));
+            auto perm = RemotePermissions::fromServerString(map.value("permissions"),
+                                                            _account->serverHasMountRootProperty() ? RemotePermissions::MountedPermissionAlgorithm::UseMountRootProperty : RemotePermissions::MountedPermissionAlgorithm::WildGuessMountedSubProperty,
+                                                            map);
+            qCInfo(lcDiscovery()) << file << map.value("permissions") << map;
             emit firstDirectoryPermissions(perm);
             _isExternalStorage = perm.hasPermission(RemotePermissions::IsMounted);
         }
@@ -585,22 +589,17 @@ void DiscoverySingleDirectoryJob::directoryListingIteratedSlot(const QString &fi
             _size = map.value("size").toInt();
         }
     } else {
-
         RemoteInfo result;
         int slash = file.lastIndexOf('/');
         result.name = file.mid(slash + 1);
         result.size = -1;
-        propertyMapToRemoteInfo(map, result);
+        propertyMapToRemoteInfo(map,
+                                _account->serverHasMountRootProperty() ? RemotePermissions::MountedPermissionAlgorithm::UseMountRootProperty : RemotePermissions::MountedPermissionAlgorithm::WildGuessMountedSubProperty,
+                                result);
         if (result.isDirectory)
             result.size = 0;
 
-        if (_isExternalStorage && result.remotePerm.hasPermission(RemotePermissions::IsMounted)) {
-            /* All the entries in a external storage have 'M' in their permission. However, for all
-               purposes in the desktop client, we only need to know about the mount points.
-               So replace the 'M' by a 'm' for every sub entries in an external storage */
-            result.remotePerm.unsetPermission(RemotePermissions::IsMounted);
-            result.remotePerm.setPermission(RemotePermissions::IsMountedSub);
-        }
+        qCInfo(lcDiscovery()) << file << map.value("permissions") << result.remotePerm.toString() << map;
         _results.push_back(std::move(result));
     }
 
index cb7a6587888ea321c5367b38f35d7b7dde63e5d3..e301beed5a7234dfdc9d76b4ff20de05d9788ec9 100644 (file)
@@ -139,10 +139,13 @@ void PropagateRemoteMkdir::finalizeMkColJob(QNetworkReply::NetworkError err, con
 
     propagator()->_activeJobList.append(this);
     auto propfindJob = new PropfindJob(propagator()->account(), jobPath, this);
-    propfindJob->setProperties({QByteArrayLiteral("http://owncloud.org/ns:share-types"), QByteArrayLiteral("http://owncloud.org/ns:permissions")});
+    propfindJob->setProperties({QByteArrayLiteral("http://owncloud.org/ns:share-types"), QByteArrayLiteral("http://owncloud.org/ns:permissions"), QByteArrayLiteral("http://nextcloud.org/ns:is-mount-root")});
     connect(propfindJob, &PropfindJob::result, this, [this, jobPath](const QVariantMap &result){
         propagator()->_activeJobList.removeOne(this);
-        _item->_remotePerm = RemotePermissions::fromServerString(result.value(QStringLiteral("permissions")).toString());
+        _item->_remotePerm = RemotePermissions::fromServerString(result.value(QStringLiteral("permissions")).toString(),
+                                                                 propagator()->account()->serverHasMountRootProperty() ? RemotePermissions::MountedPermissionAlgorithm::UseMountRootProperty : RemotePermissions::MountedPermissionAlgorithm::WildGuessMountedSubProperty,
+                                                                 result);
+
         _item->_sharedByMe = !result.value(QStringLiteral("share-types")).toString().isEmpty();
         _item->_isShared = _item->_remotePerm.hasPermission(RemotePermissions::IsShared) || _item->_sharedByMe;
         _item->_lastShareStateFetchedTimestamp = QDateTime::currentMSecsSinceEpoch();
@@ -231,6 +234,7 @@ void PropagateRemoteMkdir::slotMkcolJobFinished()
 
     _item->_fileId = _job->reply()->rawHeader("OC-FileId");
 
+    qCInfo(lcPropagateRemoteMkdir()) << "mkcol job error string:" << _item->_errorString << _job->errorString();
     _item->_errorString = _job->errorString();
 
     const auto jobHttpReasonPhraseString = _job->reply()->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
index 4a169d9cbefbbc70ded7ff43252d843432360f2e..6bf10cd7af1b0190c4e62e0c1eb7ebca4c2f85d2 100644 (file)
@@ -169,7 +169,7 @@ SyncFileItemPtr SyncFileItem::fromSyncJournalFileRecord(const SyncJournalFileRec
     return item;
 }
 
-SyncFileItemPtr SyncFileItem::fromProperties(const QString &filePath, const QMap<QString, QString> &properties)
+SyncFileItemPtr SyncFileItem::fromProperties(const QString &filePath, const QMap<QString, QString> &properties, RemotePermissions::MountedPermissionAlgorithm algorithm)
 {
     SyncFileItemPtr item(new SyncFileItem);
     item->_file = filePath;
@@ -182,7 +182,7 @@ SyncFileItemPtr SyncFileItem::fromProperties(const QString &filePath, const QMap
     item->_fileId = properties.value(QStringLiteral("id")).toUtf8();
 
     if (properties.contains(QStringLiteral("permissions"))) {
-        item->_remotePerm = RemotePermissions::fromServerString(properties.value("permissions"));
+        item->_remotePerm = RemotePermissions::fromServerString(properties.value("permissions"), algorithm, properties);
     }
 
     if (!properties.value(QStringLiteral("share-types")).isEmpty()) {
index 89e68ca99b209d0771246c73b58fe361e9f346f6..d06e6f6d93b23ca4c6991428af234a749f2b171b 100644 (file)
@@ -133,7 +133,7 @@ public:
 
     /** Creates a basic SyncFileItem from remote properties
      */
-    [[nodiscard]] static SyncFileItemPtr fromProperties(const QString &filePath, const QMap<QString, QString> &properties);
+    [[nodiscard]] static SyncFileItemPtr fromProperties(const QString &filePath, const QMap<QString, QString> &properties, RemotePermissions::MountedPermissionAlgorithm algorithm);
 
 
     SyncFileItem()
index 9e508d937b811f31bf2a4a51dba4d6204e2d8597..b8d97bb785746269688531c73f963b5252ad4943 100644 (file)
@@ -45,4 +45,8 @@ constexpr int NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_MAJOR = @NE
 constexpr int NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_MINOR = @NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_MINOR@;
 constexpr int NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_PATCH = @NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_PATCH@;
 
+constexpr int NEXTCLOUD_SERVER_VERSION_MOUNT_ROOT_PROPERTY_SUPPORTED_MAJOR = @NEXTCLOUD_SERVER_VERSION_MOUNT_ROOT_PROPERTY_SUPPORTED_MAJOR@;
+constexpr int NEXTCLOUD_SERVER_VERSION_MOUNT_ROOT_PROPERTY_SUPPORTED_MINOR = @NEXTCLOUD_SERVER_VERSION_MOUNT_ROOT_PROPERTY_SUPPORTED_MINOR@;
+constexpr int NEXTCLOUD_SERVER_VERSION_MOUNT_ROOT_PROPERTY_SUPPORTED_PATCH = @NEXTCLOUD_SERVER_VERSION_MOUNT_ROOT_PROPERTY_SUPPORTED_PATCH@;
+
 #endif // VERSION_H