Explorar o código

Implement Secure filedrop link share. Move data from 'filedrop' to 'files' when syncing E2EE folders.

Signed-off-by: alex-z <blackslayer4@gmail.com>
alex-z %!s(int64=3) %!d(string=hai) anos
pai
achega
b6ba1fe0d6
Modificáronse 37 ficheiros con 822 adicións e 91 borrados
  1. 4 0
      VERSION.cmake
  2. 3 2
      src/gui/filedetails/ShareDelegate.qml
  3. 2 0
      src/gui/filedetails/ShareDetailsPage.qml
  4. 101 38
      src/gui/filedetails/sharemodel.cpp
  5. 7 0
      src/gui/filedetails/sharemodel.h
  6. 20 0
      src/gui/ocssharejob.cpp
  7. 2 0
      src/gui/ocssharejob.h
  8. 8 0
      src/gui/sharemanager.cpp
  9. 3 0
      src/gui/sharemanager.h
  10. 63 19
      src/gui/socketapi/socketapi.cpp
  11. 12 1
      src/gui/socketapi/socketapi.h
  12. 2 0
      src/libsync/CMakeLists.txt
  13. 11 0
      src/libsync/account.cpp
  14. 2 0
      src/libsync/account.h
  15. 1 1
      src/libsync/bulkpropagatorjob.cpp
  16. 1 1
      src/libsync/bulkpropagatorjob.h
  17. 54 8
      src/libsync/clientsideencryption.cpp
  18. 10 3
      src/libsync/clientsideencryption.h
  19. 4 2
      src/libsync/clientsideencryptionjobs.cpp
  20. 4 0
      src/libsync/discovery.cpp
  21. 6 0
      src/libsync/discoveryphase.cpp
  22. 3 0
      src/libsync/discoveryphase.h
  23. 2 2
      src/libsync/encryptfolderjob.cpp
  24. 10 4
      src/libsync/owncloudpropagator.cpp
  25. 6 6
      src/libsync/owncloudpropagator.h
  26. 1 1
      src/libsync/propagateremotemove.h
  27. 1 2
      src/libsync/propagateuploadencrypted.cpp
  28. 1 1
      src/libsync/propagatorjobs.h
  29. 2 0
      src/libsync/syncfileitem.h
  30. 212 0
      src/libsync/updatefiledropmetadata.cpp
  31. 69 0
      src/libsync/updatefiledropmetadata.h
  32. 6 0
      test/CMakeLists.txt
  33. 10 0
      test/fake2eelocksucceeded.json
  34. 2 0
      test/fakefiledrope2eefoldermetadata.json
  35. 5 0
      test/syncenginetestutils.cpp
  36. 168 0
      test/testsecurefiledrop.cpp
  37. 4 0
      version.h.in

+ 4 - 0
VERSION.cmake

@@ -9,6 +9,10 @@ set(NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_MAJOR 16)
 set(NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_MINOR 0)
 set(NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_PATCH 0)
 
+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)
+
 if ( NOT DEFINED MIRALL_VERSION_SUFFIX )
     set( MIRALL_VERSION_SUFFIX "git") #e.g. beta1, beta2, rc1
 endif( NOT DEFINED MIRALL_VERSION_SUFFIX )

+ 3 - 2
src/gui/filedetails/ShareDelegate.qml

@@ -53,6 +53,7 @@ GridLayout {
 
     readonly property bool isLinkShare: model.shareType === ShareModel.ShareTypeLink
     readonly property bool isPlaceholderLinkShare: model.shareType === ShareModel.ShareTypePlaceholderLink
+    readonly property bool isSecureFileDropPlaceholderLinkShare: model.shareType === ShareModel.ShareTypeSecureFileDropPlaceholderLink
     readonly property bool isInternalLinkShare: model.shareType === ShareModel.ShareTypeInternalLink
 
     readonly property string text: model.display ?? ""
@@ -163,7 +164,7 @@ GridLayout {
 
             imageSource: "image://svgimage-custom-color/add.svg/" + Style.ncTextColor
 
-            visible: root.isPlaceholderLinkShare && root.canCreateLinkShares
+            visible: (root.isPlaceholderLinkShare || root.isSecureFileDropPlaceholderLinkShare) && root.canCreateLinkShares
             enabled: visible
 
             onClicked: root.createNewLinkShare()
@@ -212,7 +213,7 @@ GridLayout {
 
             imageSource: "image://svgimage-custom-color/more.svg/" + Style.ncTextColor
 
-            visible: !root.isPlaceholderLinkShare && !root.isInternalLinkShare
+            visible: !root.isPlaceholderLinkShare && !root.isSecureFileDropPlaceholderLinkShare && !root.isInternalLinkShare
             enabled: visible
 
             onClicked: root.rootStackView.push(shareDetailsPageComponent, {}, StackView.PushTransition)

+ 2 - 0
src/gui/filedetails/ShareDetailsPage.qml

@@ -70,6 +70,7 @@ Page {
     readonly property bool expireDateEnforced: shareModelData.expireDateEnforced
     readonly property bool passwordProtectEnabled: shareModelData.passwordProtectEnabled
     readonly property bool passwordEnforced: shareModelData.passwordEnforced
+    readonly property bool isSecureFileDropLink: shareModelData.isSecureFileDropLink
 
     readonly property bool isLinkShare: shareModelData.shareType === ShareModel.ShareTypeLink
 
@@ -328,6 +329,7 @@ Page {
                 checked: root.editingAllowed
                 text: qsTr("Allow editing")
                 enabled: !root.waitingForEditingAllowedChange
+                visible: !root.isSecureFileDropLink
 
                 onClicked: {
                     root.toggleAllowEditing(checked);

+ 101 - 38
src/gui/filedetails/sharemodel.cpp

@@ -26,6 +26,7 @@ namespace {
 
 static const auto placeholderLinkShareId = QStringLiteral("__placeholderLinkShareId__");
 static const auto internalLinkShareId = QStringLiteral("__internalLinkShareId__");
+static const auto secureFileDropPlaceholderLinkShareId = QStringLiteral("__secureFileDropPlaceholderLinkShareId__");
 
 QString createRandomPassword()
 {
@@ -39,8 +40,8 @@ QString createRandomPassword()
 }
 }
 
-namespace OCC {
-
+namespace OCC
+{
 Q_LOGGING_CATEGORY(lcShareModel, "com.nextcloud.sharemodel")
 
 ShareModel::ShareModel(QObject *parent)
@@ -52,7 +53,7 @@ ShareModel::ShareModel(QObject *parent)
 
 int ShareModel::rowCount(const QModelIndex &parent) const
 {
-    if(parent.isValid() || !_accountState || _localPath.isEmpty()) {
+    if (parent.isValid() || !_accountState || _localPath.isEmpty()) {
         return 0;
     }
 
@@ -80,6 +81,7 @@ QHash<int, QByteArray> ShareModel::roleNames() const
     roles[PasswordRole] = "password";
     roles[PasswordEnforcedRole] = "passwordEnforced";
     roles[EditingAllowedRole] = "editingAllowed";
+    roles[IsSecureFileDropLinkRole] = "isSecureFileDropLink";
 
     return roles;
 }
@@ -95,8 +97,8 @@ QVariant ShareModel::data(const QModelIndex &index, const int role) const
     }
 
     // Some roles only provide values for the link and user/group share types
-    if(const auto linkShare = share.objectCast<LinkShare>()) {
-        switch(role) {
+    if (const auto linkShare = share.objectCast<LinkShare>()) {
+        switch (role) {
         case LinkRole:
             return linkShare->getLink();
         case LinkShareNameRole:
@@ -109,23 +111,21 @@ QVariant ShareModel::data(const QModelIndex &index, const int role) const
             return linkShare->getNote();
         case ExpireDateEnabledRole:
             return linkShare->getExpireDate().isValid();
-        case ExpireDateRole:
-        {
+        case ExpireDateRole: {
             const auto startOfExpireDayUTC = linkShare->getExpireDate().startOfDay(QTimeZone::utc());
             return startOfExpireDayUTC.toMSecsSinceEpoch();
         }
         }
 
     } else if (const auto userGroupShare = share.objectCast<UserGroupShare>()) {
-        switch(role) {
+        switch (role) {
         case NoteEnabledRole:
             return !userGroupShare->getNote().isEmpty();
         case NoteRole:
             return userGroupShare->getNote();
         case ExpireDateEnabledRole:
             return userGroupShare->getExpireDate().isValid();
-        case ExpireDateRole:
-        {
+        case ExpireDateRole: {
             const auto startOfExpireDayUTC = userGroupShare->getExpireDate().startOfDay(QTimeZone::utc());
             return startOfExpireDayUTC.toMSecsSinceEpoch();
         }
@@ -134,7 +134,7 @@ QVariant ShareModel::data(const QModelIndex &index, const int role) const
         return _privateLinkUrl;
     }
 
-    switch(role) {
+    switch (role) {
     case Qt::DisplayRole:
         return displayStringForShare(share);
     case ShareRole:
@@ -151,6 +151,8 @@ QVariant ShareModel::data(const QModelIndex &index, const int role) const
         return expireDateEnforcedForShare(share);
     case EnforcedMaximumExpireDateRole:
         return enforcedMaxExpireDateForShare(share);
+    case IsSecureFileDropLinkRole:
+        return _isSecureFileDropSupportedFolder && share->getPermissions().testFlag(OCC::SharePermission::SharePermissionCreate);
     case PasswordProtectEnabledRole:
         return share->isPasswordSet();
     case PasswordRole:
@@ -159,9 +161,9 @@ QVariant ShareModel::data(const QModelIndex &index, const int role) const
         }
         return _shareIdRecentlySetPasswords.value(share->getId());
     case PasswordEnforcedRole:
-        return _accountState && _accountState->account() && _accountState->account()->capabilities().isValid() &&
-               ((share->getShareType() == Share::TypeEmail && _accountState->account()->capabilities().shareEmailPasswordEnforced()) ||
-               (share->getShareType() == Share::TypeLink && _accountState->account()->capabilities().sharePublicLinkEnforcePassword()));
+        return _accountState && _accountState->account() && _accountState->account()->capabilities().isValid()
+            && ((share->getShareType() == Share::TypeEmail && _accountState->account()->capabilities().shareEmailPasswordEnforced())
+                || (share->getShareType() == Share::TypeLink && _accountState->account()->capabilities().sharePublicLinkEnforcePassword()));
     case EditingAllowedRole:
         return share->getPermissions().testFlag(SharePermissionUpdate);
 
@@ -177,9 +179,7 @@ QVariant ShareModel::data(const QModelIndex &index, const int role) const
         return {};
     }
 
-    qCWarning(lcShareModel) << "Got unknown role" << role
-                            << "for share of type" << share->getShareType()
-                            << "so returning null value.";
+    qCWarning(lcShareModel) << "Got unknown role" << role << "for share of type" << share->getShareType() << "so returning null value.";
     return {};
 }
 
@@ -214,8 +214,7 @@ void ShareModel::updateData()
     resetData();
 
     if (_localPath.isEmpty() || !_accountState || _accountState->account().isNull()) {
-        qCWarning(lcShareModel) << "Not updating share model data. Local path is:"  << _localPath
-                                << "Is account state null:" << !_accountState;
+        qCWarning(lcShareModel) << "Not updating share model data. Local path is:" << _localPath << "Is account state null:" << !_accountState;
         return;
     }
 
@@ -240,11 +239,8 @@ void ShareModel::updateData()
     SyncJournalFileRecord fileRecord;
     auto resharingAllowed = true; // lets assume the good
 
-    if(_folder->journalDb()->getFileRecord(relPath, &fileRecord) &&
-       fileRecord.isValid() &&
-       !fileRecord._remotePerm.isNull() &&
-       !fileRecord._remotePerm.hasPermission(RemotePermissions::CanReshare)) {
-
+    if (_folder->journalDb()->getFileRecord(relPath, &fileRecord) && fileRecord.isValid() && !fileRecord._remotePerm.isNull()
+        && !fileRecord._remotePerm.hasPermission(RemotePermissions::CanReshare)) {
         qCInfo(lcShareModel) << "File record says resharing not allowed";
         resharingAllowed = false;
     }
@@ -254,6 +250,10 @@ void ShareModel::updateData()
 
     _numericFileId = fileRecord.numericFileId();
 
+    _isEncryptedItem = fileRecord._isE2eEncrypted;
+    _isSecureFileDropSupportedFolder =
+        fileRecord._isE2eEncrypted && fileRecord.e2eMangledName().isEmpty() && _accountState->account()->secureFileDropSupported();
+
     // Will get added when shares are fetched if no link shares are fetched
     _placeholderLinkShare.reset(new Share(_accountState->account(),
                                           placeholderLinkShareId,
@@ -269,12 +269,17 @@ void ShareModel::updateData()
                                        _sharePath,
                                        Share::TypeInternalLink));
 
+    _secureFileDropPlaceholderLinkShare.reset(new Share(_accountState->account(),
+                                                        secureFileDropPlaceholderLinkShareId,
+                                                        _accountState->account()->id(),
+                                                        _accountState->account()->davDisplayName(),
+                                                        _sharePath,
+                                                        Share::TypeSecureFileDropPlaceholderLink));
+
     auto job = new PropfindJob(_accountState->account(), _sharePath);
-    job->setProperties(
-        QList<QByteArray>()
-        << "http://open-collaboration-services.org/ns:share-permissions"
-        << "http://owncloud.org/ns:fileid" // numeric file id for fallback private link generation
-        << "http://owncloud.org/ns:privatelink");
+    job->setProperties(QList<QByteArray>() << "http://open-collaboration-services.org/ns:share-permissions"
+                                           << "http://owncloud.org/ns:fileid" // numeric file id for fallback private link generation
+                                           << "http://owncloud.org/ns:privatelink");
     job->setTimeout(10 * 1000);
     connect(job, &PropfindJob::result, this, &ShareModel::slotPropfindReceived);
     connect(job, &PropfindJob::finishedWithError, this, [&](const QNetworkReply *reply) {
@@ -306,10 +311,12 @@ void ShareModel::initShareManager()
     if (_manager.isNull() && sharingPossible) {
         _manager.reset(new ShareManager(_accountState->account(), this));
         connect(_manager.data(), &ShareManager::sharesFetched, this, &ShareModel::slotSharesFetched);
-        connect(_manager.data(), &ShareManager::shareCreated, this, [&]{ _manager->fetchShares(_sharePath); });
+        connect(_manager.data(), &ShareManager::shareCreated, this, [&] {
+            _manager->fetchShares(_sharePath);
+        });
         connect(_manager.data(), &ShareManager::linkShareCreated, this, &ShareModel::slotAddShare);
         connect(_manager.data(), &ShareManager::linkShareRequiresPassword, this, &ShareModel::requestPasswordForLinkShare);
-        connect(_manager.data(), &ShareManager::serverError, this, [this](const int code, const QString &message){
+        connect(_manager.data(), &ShareManager::serverError, this, [this](const int code, const QString &message) {
             _hasInitialShareFetchCompleted = true;
             Q_EMIT hasInitialShareFetchCompletedChanged();
             emit serverError(code, message);
@@ -335,7 +342,7 @@ void ShareModel::handlePlaceholderLinkShare()
             placeholderLinkSharePresent = true;
         }
 
-        if(linkSharePresent && placeholderLinkSharePresent) {
+        if (linkSharePresent && placeholderLinkSharePresent) {
             break;
         }
     }
@@ -349,6 +356,43 @@ void ShareModel::handlePlaceholderLinkShare()
     Q_EMIT sharesChanged();
 }
 
+void ShareModel::handleSecureFileDropLinkShare()
+{
+    // We want to add the placeholder if there are no link shares and
+    // if we are not already showing the placeholder link share
+    auto linkSharePresent = false;
+    auto secureFileDropLinkSharePresent = false;
+
+    for (const auto &share : qAsConst(_shares)) {
+        const auto shareType = share->getShareType();
+
+        if (!linkSharePresent && shareType == Share::TypeLink) {
+            linkSharePresent = true;
+        } else if (!secureFileDropLinkSharePresent && shareType == Share::TypeSecureFileDropPlaceholderLink) {
+            secureFileDropLinkSharePresent = true;
+        }
+
+        if (linkSharePresent && secureFileDropLinkSharePresent) {
+            break;
+        }
+    }
+
+    if (linkSharePresent && secureFileDropLinkSharePresent) {
+        slotRemoveShareWithId(secureFileDropPlaceholderLinkShareId);
+    } else if (!linkSharePresent && !secureFileDropLinkSharePresent) {
+        slotAddShare(_secureFileDropPlaceholderLinkShare);
+    }
+}
+
+void ShareModel::handleLinkShare()
+{
+    if (!_isEncryptedItem) {
+        handlePlaceholderLinkShare();
+    } else if (_isSecureFileDropSupportedFolder) {
+        handleSecureFileDropLinkShare();
+    }
+}
+
 void ShareModel::slotPropfindReceived(const QVariantMap &result)
 {
     _fetchOngoing = false;
@@ -403,7 +447,7 @@ void ShareModel::slotSharesFetched(const QList<SharePtr> &shares)
         slotAddShare(share);
     }
 
-    handlePlaceholderLinkShare();
+    handleLinkShare();
 }
 
 void ShareModel::setupInternalLinkShare()
@@ -411,7 +455,8 @@ void ShareModel::setupInternalLinkShare()
     if (!_accountState ||
         _accountState->account().isNull() ||
         _localPath.isEmpty() ||
-        _privateLinkUrl.isEmpty()) {
+        _privateLinkUrl.isEmpty() ||
+        _isEncryptedItem) {
         return;
     }
 
@@ -479,7 +524,8 @@ void ShareModel::slotAddShare(const SharePtr &share)
         connect(_manager.data(), &ShareManager::serverError, this, &ShareModel::slotServerError);
     }
 
-    handlePlaceholderLinkShare();
+    handleLinkShare();
+    Q_EMIT sharesChanged();
 }
 
 void ShareModel::slotRemoveShareWithId(const QString &shareId)
@@ -505,7 +551,9 @@ void ShareModel::slotRemoveShareWithId(const QString &shareId)
     _shares.removeAt(shareIndex.row());
     endRemoveRows();
 
-    handlePlaceholderLinkShare();
+    handleLinkShare();
+
+    Q_EMIT sharesChanged();
 }
 
 void ShareModel::slotServerError(const int code, const QString &message)
@@ -533,7 +581,10 @@ void ShareModel::slotRemoveSharee(const ShareePtr &sharee)
 QString ShareModel::displayStringForShare(const SharePtr &share) const
 {
     if (const auto linkShare = share.objectCast<LinkShare>()) {
-        const auto displayString = tr("Share link");
+
+        const auto isSecureFileDropShare = _isSecureFileDropSupportedFolder && linkShare->getPermissions().testFlag(OCC::SharePermission::SharePermissionCreate);
+
+        const auto displayString = isSecureFileDropShare ? tr("Secure filedrop link") : tr("Share link");
 
         if (!linkShare->getLabel().isEmpty()) {
             return QStringLiteral("%1 (%2)").arg(displayString, linkShare->getLabel());
@@ -544,6 +595,8 @@ QString ShareModel::displayStringForShare(const SharePtr &share) const
         return tr("Link share");
     } else if (share->getShareType() == Share::TypeInternalLink) {
         return tr("Internal link");
+    } else if (share->getShareType() == Share::TypeSecureFileDropPlaceholderLink) {
+        return tr("Secure file drop");
     } else if (share->getShareWith()) {
         return share->getShareWith()->format();
     }
@@ -560,6 +613,7 @@ QString ShareModel::iconUrlForShare(const SharePtr &share) const
     case Share::TypeInternalLink:
         return QString(iconsPath + QStringLiteral("external.svg"));
     case Share::TypePlaceholderLink:
+    case Share::TypeSecureFileDropPlaceholderLink:
     case Share::TypeLink:
         return QString(iconsPath + QStringLiteral("public.svg"));
     case Share::TypeEmail:
@@ -890,10 +944,19 @@ void ShareModel::setShareNoteFromQml(const QVariant &share, const QString &note)
 
 void ShareModel::createNewLinkShare() const
 {
+    if (_isEncryptedItem && !_isSecureFileDropSupportedFolder) {
+        qCWarning(lcShareModel) << "Attempt to create a link share for non-root encrypted folder or a file.";
+        return;
+    }
+
     if (_manager) {
         const auto askOptionalPassword = _accountState->account()->capabilities().sharePublicLinkAskOptionalPassword();
         const auto password = askOptionalPassword ? createRandomPassword() : QString();
-        _manager->createLinkShare(_sharePath, QString(), password);
+        if (_isSecureFileDropSupportedFolder) {
+            _manager->createSecureFileDropShare(_sharePath, {}, password);
+            return;
+        }
+        _manager->createLinkShare(_sharePath, {}, password);
     }
 }
 

+ 7 - 0
src/gui/filedetails/sharemodel.h

@@ -57,6 +57,7 @@ public:
         PasswordRole,
         PasswordEnforcedRole,
         EditingAllowedRole,
+        IsSecureFileDropLinkRole,
     };
     Q_ENUM(Roles)
 
@@ -75,6 +76,7 @@ public:
         ShareTypeRoom = Share::TypeRoom,
         ShareTypePlaceholderLink = Share::TypePlaceholderLink,
         ShareTypeInternalLink = Share::TypeInternalLink,
+        ShareTypeSecureFileDropPlaceholderLink = Share::TypeSecureFileDropPlaceholderLink,
     };
     Q_ENUM(ShareType);
 
@@ -159,6 +161,8 @@ private slots:
     void updateData();
     void initShareManager();
     void handlePlaceholderLinkShare();
+    void handleSecureFileDropLinkShare();
+    void handleLinkShare();
     void setupInternalLinkShare();
 
     void slotPropfindReceived(const QVariantMap &result);
@@ -188,6 +192,7 @@ private:
     bool _hasInitialShareFetchCompleted = false;
     SharePtr _placeholderLinkShare;
     SharePtr _internalLinkShare;
+    SharePtr _secureFileDropPlaceholderLinkShare;
 
     QPointer<AccountState> _accountState;
     QPointer<Folder> _folder;
@@ -196,6 +201,8 @@ private:
     QString _sharePath;
     SharePermissions _maxSharingPermissions;
     QByteArray _numericFileId;
+    bool _isEncryptedItem = false;
+    bool _isSecureFileDropSupportedFolder = false;
     SyncJournalFileLockInfo _filelockState;
     QString _privateLinkUrl;
 

+ 20 - 0
src/gui/ocssharejob.cpp

@@ -155,6 +155,26 @@ void OcsShareJob::createLinkShare(const QString &path,
     start();
 }
 
+void OcsShareJob::createSecureFileDropLinkShare(const QString &path, const QString &name, const QString &password)
+{
+    setVerb("POST");
+
+    addParam(QString::fromLatin1("path"), path);
+    addParam(QString::fromLatin1("shareType"), QString::number(Share::TypeLink));
+    addParam(QString::fromLatin1("permissions"), QString::number(4));
+
+    if (!name.isEmpty()) {
+        addParam(QString::fromLatin1("name"), name);
+    }
+    if (!password.isEmpty()) {
+        addParam(QString::fromLatin1("password"), password);
+    }
+
+    addPassStatusCode(403);
+
+    start();
+}
+
 void OcsShareJob::createShare(const QString &path,
     const Share::ShareType shareType,
     const QString &shareWith,

+ 2 - 0
src/gui/ocssharejob.h

@@ -111,6 +111,8 @@ public:
     void createLinkShare(const QString &path, const QString &name,
         const QString &password);
 
+    void createSecureFileDropLinkShare(const QString &path, const QString &name, const QString &password);
+
     /**
      * Create a new share
      *

+ 8 - 0
src/gui/sharemanager.cpp

@@ -396,6 +396,14 @@ void ShareManager::createLinkShare(const QString &path,
     job->createLinkShare(path, name, password);
 }
 
+void ShareManager::createSecureFileDropShare(const QString &path, const QString &name, const QString &password)
+{
+    const auto createShareJob = new OcsShareJob(_account);
+    connect(createShareJob, &OcsShareJob::shareJobFinished, this, &ShareManager::slotLinkShareCreated);
+    connect(createShareJob, &OcsJob::ocsError, this, &ShareManager::slotOcsError);
+    createShareJob->createSecureFileDropLinkShare(path, name, password);
+}
+
 void ShareManager::slotLinkShareCreated(const QJsonDocument &reply)
 {
     QString message;

+ 3 - 0
src/gui/sharemanager.h

@@ -52,6 +52,7 @@ public:
      * Need to be in sync with Sharee::Type
      */
     enum ShareType {
+        TypeSecureFileDropPlaceholderLink = -3,
         TypeInternalLink = -2,
         TypePlaceholderLink = -1,
         TypeUser = Sharee::User,
@@ -377,6 +378,8 @@ public:
         const QString &name,
         const QString &password);
 
+    void createSecureFileDropShare(const QString &path, const QString &name, const QString &password);
+
     /**
      * Tell the manager to create a new share
      *

+ 63 - 19
src/gui/socketapi/socketapi.cpp

@@ -587,6 +587,13 @@ void SocketApi::processShareRequest(const QString &localFile, SocketListener *li
             return;
         }
 
+        if (!fileData.journalRecord().e2eMangledName().isEmpty()) {
+            // we can not share an encrypted file or a subfolder under encrypted root foolder
+            const QString message = QLatin1String("SHARE:NOP:") + QDir::toNativeSeparators(localFile);
+            listener->sendMessage(message);
+            return;
+        }
+
         auto &remotePath = fileData.serverRelativePath;
 
         // Can't share root folder
@@ -729,12 +736,13 @@ class GetOrCreatePublicLinkShare : public QObject
 {
     Q_OBJECT
 public:
-    GetOrCreatePublicLinkShare(const AccountPtr &account, const QString &localFile,
+    GetOrCreatePublicLinkShare(const AccountPtr &account, const QString &localFile, const bool isSecureFileDropOnlyFolder,
         QObject *parent)
         : QObject(parent)
         , _account(account)
         , _shareManager(account)
         , _localFile(localFile)
+        , _isSecureFileDropOnlyFolder(isSecureFileDropOnlyFolder)
     {
         connect(&_shareManager, &ShareManager::sharesFetched,
             this, &GetOrCreatePublicLinkShare::sharesFetched);
@@ -771,7 +779,11 @@ private slots:
 
         // otherwise create a new one
         qCDebug(lcPublicLink) << "Creating new share";
-        _shareManager.createLinkShare(_localFile, shareName, QString());
+        if (_isSecureFileDropOnlyFolder) {
+            _shareManager.createSecureFileDropShare(_localFile, shareName, QString());
+        } else {
+            _shareManager.createLinkShare(_localFile, shareName, QString());
+        }
     }
 
     void linkShareCreated(const QSharedPointer<OCC::LinkShare> &share)
@@ -832,6 +844,7 @@ private:
     AccountPtr _account;
     ShareManager _shareManager;
     QString _localFile;
+    bool _isSecureFileDropOnlyFolder = false;
 };
 
 #else
@@ -852,19 +865,36 @@ public:
 
 #endif
 
+void SocketApi::command_COPY_SECUREFILEDROP_LINK(const QString &localFile, SocketListener *)
+{
+    const auto fileData = FileData::get(localFile);
+    if (!fileData.folder) {
+        return;
+    }
+
+    const auto account = fileData.folder->accountState()->account();
+    const auto getOrCreatePublicLinkShareJob = new GetOrCreatePublicLinkShare(account, fileData.serverRelativePath, true, this);
+    connect(getOrCreatePublicLinkShareJob, &GetOrCreatePublicLinkShare::done, this, [](const QString &url) { copyUrlToClipboard(url); });
+    connect(getOrCreatePublicLinkShareJob, &GetOrCreatePublicLinkShare::error, this, [=]() { emit shareCommandReceived(fileData.localPath); });
+    getOrCreatePublicLinkShareJob->run();
+}
+
 void SocketApi::command_COPY_PUBLIC_LINK(const QString &localFile, SocketListener *)
 {
-    auto fileData = FileData::get(localFile);
-    if (!fileData.folder)
+    const auto fileData = FileData::get(localFile);
+    if (!fileData.folder) {
         return;
+    }
 
-    AccountPtr account = fileData.folder->accountState()->account();
-    auto job = new GetOrCreatePublicLinkShare(account, fileData.serverRelativePath, this);
-    connect(job, &GetOrCreatePublicLinkShare::done, this,
-        [](const QString &url) { copyUrlToClipboard(url); });
-    connect(job, &GetOrCreatePublicLinkShare::error, this,
-        [=]() { emit shareCommandReceived(fileData.localPath); });
-    job->run();
+    const auto account = fileData.folder->accountState()->account();
+    const auto getOrCreatePublicLinkShareJob = new GetOrCreatePublicLinkShare(account, fileData.serverRelativePath, false, this);
+    connect(getOrCreatePublicLinkShareJob, &GetOrCreatePublicLinkShare::done, this, [](const QString &url) {
+        copyUrlToClipboard(url);
+    });
+    connect(getOrCreatePublicLinkShareJob, &GetOrCreatePublicLinkShare::error, this, [=]() {
+        emit shareCommandReceived(fileData.localPath);
+    });
+    getOrCreatePublicLinkShareJob->run();
 }
 
 // Windows Shell / Explorer pinning fallbacks, see issue: https://github.com/nextcloud/desktop/issues/1599
@@ -1116,11 +1146,12 @@ void SocketApi::command_GET_STRINGS(const QString &argument, SocketListener *lis
     listener->sendMessage(QString("GET_STRINGS:END"));
 }
 
-void SocketApi::sendSharingContextMenuOptions(const FileData &fileData, SocketListener *listener, bool enabled)
+void SocketApi::sendSharingContextMenuOptions(const FileData &fileData, SocketListener *listener, SharingContextItemEncryptedFlag itemEncryptionFlag, SharingContextItemRootEncryptedFolderFlag rootE2eeFolderFlag)
 {
-    auto record = fileData.journalRecord();
-    bool isOnTheServer = record.isValid();
-    auto flagString = isOnTheServer && enabled ? QLatin1String("::") : QLatin1String(":d:");
+    const auto record = fileData.journalRecord();
+    const auto isOnTheServer = record.isValid();
+    const auto isSecureFileDropSupported = rootE2eeFolderFlag == SharingContextItemRootEncryptedFolderFlag::RootEncryptedFolder && fileData.folder->accountState()->account()->secureFileDropSupported();
+    const auto flagString = isOnTheServer && (itemEncryptionFlag == SharingContextItemEncryptedFlag::NotEncryptedItem || isSecureFileDropSupported) ? QLatin1String("::") : QLatin1String(":d:");
 
     auto capabilities = fileData.folder->accountState()->account()->capabilities();
     auto theme = Theme::instance();
@@ -1148,13 +1179,23 @@ void SocketApi::sendSharingContextMenuOptions(const FileData &fileData, SocketLi
             && !capabilities.sharePublicLinkEnforcePassword();
 
         if (canCreateDefaultPublicLink) {
-            listener->sendMessage(QLatin1String("MENU_ITEM:COPY_PUBLIC_LINK") + flagString + tr("Copy public link"));
+            if (isSecureFileDropSupported) {
+                listener->sendMessage(QLatin1String("MENU_ITEM:COPY_SECUREFILEDROP_LINK") + QLatin1String("::") + tr("Copy secure filedrop link"));
+            } else {
+                listener->sendMessage(QLatin1String("MENU_ITEM:COPY_PUBLIC_LINK") + flagString + tr("Copy public link"));
+            }
         } else if (publicLinksEnabled) {
-            listener->sendMessage(QLatin1String("MENU_ITEM:MANAGE_PUBLIC_LINKS") + flagString + tr("Copy public link"));
+            if (isSecureFileDropSupported) {
+                listener->sendMessage(QLatin1String("MENU_ITEM:MANAGE_PUBLIC_LINKS") + QLatin1String("::") + tr("Copy secure filedrop link"));
+            } else {
+                listener->sendMessage(QLatin1String("MENU_ITEM:MANAGE_PUBLIC_LINKS") + flagString + tr("Copy public link"));
+            }
         }
     }
 
-    listener->sendMessage(QLatin1String("MENU_ITEM:COPY_PRIVATE_LINK") + flagString + tr("Copy internal link"));
+    if (itemEncryptionFlag == SharingContextItemEncryptedFlag::NotEncryptedItem) {
+        listener->sendMessage(QLatin1String("MENU_ITEM:COPY_PRIVATE_LINK") + flagString + tr("Copy internal link"));
+    }
 
     // Disabled: only providing email option for private links would look odd,
     // and the copy option is more general.
@@ -1312,6 +1353,7 @@ void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListe
         const auto record = fileData.journalRecord();
         const bool isOnTheServer = record.isValid();
         const auto isE2eEncryptedPath = fileData.journalRecord()._isE2eEncrypted || !fileData.journalRecord()._e2eMangledName.isEmpty();
+        const auto isE2eEncryptedRootFolder = fileData.journalRecord()._isE2eEncrypted && fileData.journalRecord()._e2eMangledName.isEmpty();
         auto flagString = isOnTheServer && !isE2eEncryptedPath ? QLatin1String("::") : QLatin1String(":d:");
 
         const QFileInfo fileInfo(fileData.localPath);
@@ -1331,7 +1373,9 @@ void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListe
 
         sendEncryptFolderCommandMenuEntries(fileInfo, fileData, isE2eEncryptedPath, listener);
         sendLockFileCommandMenuEntries(fileInfo, syncFolder, fileData, listener);
-        sendSharingContextMenuOptions(fileData, listener, !isE2eEncryptedPath);
+        const auto itemEncryptionFlag = isE2eEncryptedPath ? SharingContextItemEncryptedFlag::EncryptedItem : SharingContextItemEncryptedFlag::NotEncryptedItem;
+        const auto rootE2eeFolderFlag = isE2eEncryptedRootFolder ? SharingContextItemRootEncryptedFolderFlag::RootEncryptedFolder : SharingContextItemRootEncryptedFolderFlag::NonRootEncryptedFolder;
+        sendSharingContextMenuOptions(fileData, listener, itemEncryptionFlag, rootE2eeFolderFlag);
 
         // Conflict files get conflict resolution actions
         bool isConflict = Utility::isConflictFile(fileData.folderRelativePath);

+ 12 - 1
src/gui/socketapi/socketapi.h

@@ -51,6 +51,16 @@ class SocketApi : public QObject
 {
     Q_OBJECT
 
+    enum SharingContextItemEncryptedFlag {
+        EncryptedItem,
+        NotEncryptedItem
+    };
+
+    enum SharingContextItemRootEncryptedFolderFlag {
+        RootEncryptedFolder,
+        NonRootEncryptedFolder
+    };
+
 public:
     explicit SocketApi(QObject *parent = nullptr);
     ~SocketApi() override;
@@ -119,6 +129,7 @@ private:
     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_SECUREFILEDROP_LINK(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);
     Q_INVOKABLE void command_EMAIL_PRIVATE_LINK(const QString &localFile, OCC::SocketListener *listener);
@@ -151,7 +162,7 @@ private:
     Q_INVOKABLE void command_GET_STRINGS(const QString &argument, OCC::SocketListener *listener);
 
     // Sends the context menu options relating to sharing to listener
-    void sendSharingContextMenuOptions(const FileData &fileData, SocketListener *listener, bool enabled);
+    void sendSharingContextMenuOptions(const FileData &fileData, SocketListener *listener, SharingContextItemEncryptedFlag itemEncryptionFlag, SharingContextItemRootEncryptedFolderFlag rootE2eeFolderFlag);
 
     void sendEncryptFolderCommandMenuEntries(const QFileInfo &fileInfo,
                                              const FileData &fileData,

+ 2 - 0
src/libsync/CMakeLists.txt

@@ -99,6 +99,8 @@ set(libsync_SRCS
     syncoptions.cpp
     theme.h
     theme.cpp
+    updatefiledropmetadata.h
+    updatefiledropmetadata.cpp
     clientsideencryption.h
     clientsideencryption.cpp
     clientsideencryptionjobs.h

+ 11 - 0
src/libsync/account.cpp

@@ -696,6 +696,17 @@ bool Account::serverVersionUnsupported() const
                NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_MINOR, NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_PATCH);
 }
 
+bool Account::secureFileDropSupported() const
+{
+    if (serverVersionInt() == 0) {
+        // not detected yet, assume it is fine.
+        return true;
+    }
+    return serverVersionInt() >= makeServerVersion(NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_MAJOR,
+                                                   NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_MINOR,
+                                                   NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_PATCH);
+}
+
 bool Account::isUsernamePrefillSupported() const
 {
     return serverVersionInt() >= makeServerVersion(usernamePrefillServerVersionMinSupportedMajor, 0, 0);

+ 2 - 0
src/libsync/account.h

@@ -259,6 +259,8 @@ public:
      */
     [[nodiscard]] bool serverVersionUnsupported() const;
 
+    [[nodiscard]] bool secureFileDropSupported() const;
+
     [[nodiscard]] bool isUsernamePrefillSupported() const;
 
     [[nodiscard]] bool isChecksumRecalculateRequestSupported() const;

+ 1 - 1
src/libsync/bulkpropagatorjob.cpp

@@ -106,7 +106,7 @@ bool BulkPropagatorJob::scheduleSelfOrChild()
     return _items.empty() && _filesToUpload.empty();
 }
 
-PropagatorJob::JobParallelism BulkPropagatorJob::parallelism()
+PropagatorJob::JobParallelism BulkPropagatorJob::parallelism() const
 {
     return PropagatorJob::JobParallelism::FullParallelism;
 }

+ 1 - 1
src/libsync/bulkpropagatorjob.h

@@ -64,7 +64,7 @@ public:
 
     bool scheduleSelfOrChild() override;
 
-    JobParallelism parallelism() override;
+    [[nodiscard]] JobParallelism parallelism() const override;
 
 private slots:
     void startUploadFile(OCC::SyncFileItemPtr item, OCC::BulkPropagatorJob::UploadFileInfo fileToUpload);

+ 54 - 8
src/libsync/clientsideencryption.cpp

@@ -24,7 +24,6 @@
 #include <QLoggingCategory>
 #include <QFileInfo>
 #include <QDir>
-#include <QJsonObject>
 #include <QXmlStreamReader>
 #include <QXmlStreamNamespaceDeclaration>
 #include <QStack>
@@ -1534,6 +1533,8 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata)
   QByteArray sharing = metadataObj["sharing"].toString().toLocal8Bit();
   QJsonObject files = metaDataDoc.object()["files"].toObject();
 
+  _fileDrop = metaDataDoc.object().value("filedrop").toObject();
+
   QJsonDocument debugHelper;
   debugHelper.setObject(metadataKeys);
   qCDebug(lcCse) << "Keys: " << debugHelper.toJson(QJsonDocument::Compact);
@@ -1546,7 +1547,7 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata)
      * We have to base64 decode the metadatakey here. This was a misunderstanding in the RFC
      * Now we should be compatible with Android and IOS. Maybe we can fix it later.
      */
-    QByteArray b64DecryptedKey = decryptMetadataKey(currB64Pass);
+    QByteArray b64DecryptedKey = decryptData(currB64Pass);
     if (b64DecryptedKey.isEmpty()) {
       qCDebug(lcCse()) << "Could not decrypt metadata for key" << it.key();
       continue;
@@ -1615,7 +1616,7 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata)
 }
 
 // RSA/ECB/OAEPWithSHA-256AndMGF1Padding using private / public key.
-QByteArray FolderMetadata::encryptMetadataKey(const QByteArray& data) const
+QByteArray FolderMetadata::encryptData(const QByteArray& data) const
 {
     Bio publicKeyBio;
     QByteArray publicKeyPem = _account->e2e()->_publicKey.toPem();
@@ -1626,7 +1627,7 @@ QByteArray FolderMetadata::encryptMetadataKey(const QByteArray& data) const
     return EncryptionHelper::encryptStringAsymmetric(publicKey, data.toBase64());
 }
 
-QByteArray FolderMetadata::decryptMetadataKey(const QByteArray& encryptedMetadata) const
+QByteArray FolderMetadata::decryptData(const QByteArray &data) const
 {
     Bio privateKeyBio;
     QByteArray privateKeyPem = _account->e2e()->_privateKey;
@@ -1634,8 +1635,7 @@ QByteArray FolderMetadata::decryptMetadataKey(const QByteArray& encryptedMetadat
     auto key = ClientSideEncryption::PKey::readPrivateKey(privateKeyBio);
 
     // Also base64 decode the result
-    QByteArray decryptResult = EncryptionHelper::decryptStringAsymmetric(
-                    key, QByteArray::fromBase64(encryptedMetadata));
+    QByteArray decryptResult = EncryptionHelper::decryptStringAsymmetric(key, QByteArray::fromBase64(data));
 
     if (decryptResult.isEmpty())
     {
@@ -1672,7 +1672,7 @@ void FolderMetadata::setupEmptyMetadata() {
     _sharing.append({displayName, publicKey});
 }
 
-QByteArray FolderMetadata::encryptedMetadata() {
+QByteArray FolderMetadata::encryptedMetadata() const {
     qCDebug(lcCse) << "Generating metadata";
 
     if (_metadataKeys.isEmpty()) {
@@ -1686,7 +1686,7 @@ QByteArray FolderMetadata::encryptedMetadata() {
          * We have to already base64 encode the metadatakey here. This was a misunderstanding in the RFC
          * Now we should be compatible with Android and IOS. Maybe we can fix it later.
          */
-        const QByteArray encryptedKey = encryptMetadataKey(it.value().toBase64());
+        const QByteArray encryptedKey = encryptData(it.value().toBase64());
         metadataKeys.insert(QString::number(it.key()), QString(encryptedKey));
     }
 
@@ -1761,6 +1761,52 @@ QVector<EncryptedFile> FolderMetadata::files() const {
     return _files;
 }
 
+bool FolderMetadata::isFileDropPresent() const
+{
+    return _fileDrop.size() > 0;
+}
+
+bool FolderMetadata::moveFromFileDropToFiles()
+{
+    if (_fileDrop.isEmpty()) {
+        return false;
+    }
+
+    for (auto it = _fileDrop.constBegin(); it != _fileDrop.constEnd(); ++it) {
+        const auto fileObject = it.value().toObject();
+
+        const auto encryptedFile = fileObject["encrypted"].toString().toLocal8Bit();
+        const auto decryptedFile = decryptData(encryptedFile);
+        const auto decryptedFileDocument = QJsonDocument::fromJson(decryptedFile);
+        const auto decryptedFileObject = decryptedFileDocument.object();
+
+        EncryptedFile file;
+        file.encryptedFilename = it.key();
+        file.metadataKey = fileObject["metadataKey"].toInt();
+        file.authenticationTag = QByteArray::fromBase64(fileObject["authenticationTag"].toString().toLocal8Bit());
+        file.initializationVector = QByteArray::fromBase64(fileObject["initializationVector"].toString().toLocal8Bit());
+
+        file.originalFilename = decryptedFileObject["filename"].toString();
+        file.encryptionKey = QByteArray::fromBase64(decryptedFileObject["key"].toString().toLocal8Bit());
+        file.mimetype = decryptedFileObject["mimetype"].toString().toLocal8Bit();
+        file.fileVersion = decryptedFileObject["version"].toInt();
+
+        // In case we wrongly stored "inode/directory" we try to recover from it
+        if (file.mimetype == QByteArrayLiteral("inode/directory")) {
+            file.mimetype = QByteArrayLiteral("httpd/unix-directory");
+        }
+
+        _files.push_back(file);
+    }
+
+    return true;
+}
+
+QJsonObject FolderMetadata::fileDrop() const
+{
+    return _fileDrop;
+}
+
 bool EncryptionHelper::fileEncryption(const QByteArray &key, const QByteArray &iv, QFile *input, QFile *output, QByteArray& returnTag)
 {
     if (!input->open(QIODevice::ReadOnly)) {

+ 10 - 3
src/libsync/clientsideencryption.h

@@ -4,6 +4,7 @@
 #include <QString>
 #include <QObject>
 #include <QJsonDocument>
+#include <QJsonObject>
 #include <QSslCertificate>
 #include <QSslKey>
 #include <QFile>
@@ -188,13 +189,18 @@ struct EncryptedFile {
 class OWNCLOUDSYNC_EXPORT FolderMetadata {
 public:
     FolderMetadata(AccountPtr account, const QByteArray& metadata = QByteArray(), int statusCode = -1);
-    QByteArray encryptedMetadata();
+    [[nodiscard]] QByteArray encryptedMetadata() const;
     void addEncryptedFile(const EncryptedFile& f);
     void removeEncryptedFile(const EncryptedFile& f);
     void removeAllEncryptedFiles();
     [[nodiscard]] QVector<EncryptedFile> files() const;
     [[nodiscard]] bool isMetadataSetup() const;
 
+    [[nodiscard]] bool isFileDropPresent() const;
+
+    [[nodiscard]] bool moveFromFileDropToFiles();
+
+    [[nodiscard]] QJsonObject fileDrop() const;
 
 private:
     /* Use std::string and std::vector internally on this class
@@ -203,8 +209,8 @@ private:
     void setupEmptyMetadata();
     void setupExistingMetadata(const QByteArray& metadata);
 
-    [[nodiscard]] QByteArray encryptMetadataKey(const QByteArray& metadataKey) const;
-    [[nodiscard]] QByteArray decryptMetadataKey(const QByteArray& encryptedKey) const;
+    [[nodiscard]] QByteArray encryptData(const QByteArray &data) const;
+    [[nodiscard]] QByteArray decryptData(const QByteArray &data) const;
 
     [[nodiscard]] QByteArray encryptJsonObject(const QByteArray& obj, const QByteArray pass) const;
     [[nodiscard]] QByteArray decryptJsonObject(const QByteArray& encryptedJsonBlob, const QByteArray& pass) const;
@@ -213,6 +219,7 @@ private:
     QMap<int, QByteArray> _metadataKeys;
     AccountPtr _account;
     QVector<QPair<QString, QString>> _sharing;
+    QJsonObject _fileDrop;
 };
 
 } // namespace OCC

+ 4 - 2
src/libsync/clientsideencryptionjobs.cpp

@@ -293,8 +293,10 @@ bool LockEncryptFolderApiJob::finished()
 
     qCInfo(lcCseJob()) << "lock folder finished with code" << retCode << " for:" << path() << " for fileId: " << _fileId << " token:" << token;
 
-    const auto folderTokenEncrypted = EncryptionHelper::encryptStringAsymmetric(_publicKey, token);
-    _journalDb->setE2EeLockedFolder(_fileId, folderTokenEncrypted);
+    if (!_publicKey.isNull()) {
+        const auto folderTokenEncrypted = EncryptionHelper::encryptStringAsymmetric(_publicKey, token);
+        _journalDb->setE2EeLockedFolder(_fileId, folderTokenEncrypted);
+    }
 
     //TODO: Parse the token and submit.
     emit success(_fileId, token);

+ 4 - 0
src/libsync/discovery.cpp

@@ -1848,6 +1848,10 @@ DiscoverySingleDirectoryJob *ProcessDirectoryJob::startAsyncServerQuery()
     _discoveryData->_currentlyActiveJobs++;
     _pendingAsyncJobs++;
     connect(serverJob, &DiscoverySingleDirectoryJob::finished, this, [this, serverJob](const auto &results) {
+        if (_dirItem) {
+            _dirItem->_isFileDropDetected = serverJob->isFileDropDetected();
+            qCInfo(lcDisco) << "serverJob has finished for folder:" << _dirItem->_file << " and it has _isFileDropDetected:" << true;
+        }
         _discoveryData->_currentlyActiveJobs--;
         _pendingAsyncJobs--;
         if (results) {

+ 6 - 0
src/libsync/discoveryphase.cpp

@@ -405,6 +405,11 @@ void DiscoverySingleDirectoryJob::abort()
     }
 }
 
+bool DiscoverySingleDirectoryJob::isFileDropDetected() const
+{
+    return _isFileDropDetected;
+}
+
 static void propertyMapToRemoteInfo(const QMap<QString, QString> &map, RemoteInfo &result)
 {
     for (auto it = map.constBegin(); it != map.constEnd(); ++it) {
@@ -617,6 +622,7 @@ void DiscoverySingleDirectoryJob::metadataReceived(const QJsonDocument &json, in
     Q_ASSERT(_subPath.startsWith('/'));
 
     const auto metadata = FolderMetadata(_account, json.toJson(QJsonDocument::Compact), statusCode);
+    _isFileDropDetected = metadata.isFileDropPresent();
     const auto encryptedFiles = metadata.files();
 
     const auto findEncryptedFile = [=](const QString &name) {

+ 3 - 0
src/libsync/discoveryphase.h

@@ -67,6 +67,7 @@ struct RemoteInfo
     int64_t sizeOfFolder = 0;
     bool isDirectory = false;
     bool isE2eEncrypted = false;
+    bool isFileDropDetected = false;
     QString e2eMangledName;
     bool sharedByMe = false;
 
@@ -142,6 +143,7 @@ public:
     void setIsRootPath() { _isRootPath = true; }
     void start();
     void abort();
+    [[nodiscard]] bool isFileDropDetected() const;
 
     // This is not actually a network job, it is just a job
 signals:
@@ -173,6 +175,7 @@ private:
     bool _isExternalStorage = false;
     // If this directory is e2ee
     bool _isE2eEncrypted = false;
+    bool _isFileDropDetected = false;
     // If set, the discovery will finish with an error
     int64_t _size = 0;
     QString _error;

+ 2 - 2
src/libsync/encryptfolderjob.cpp

@@ -83,8 +83,8 @@ void EncryptFolderJob::slotLockForEncryptionSuccess(const QByteArray &fileId, co
 {
     _folderToken = token;
 
-    FolderMetadata emptyMetadata(_account);
-    auto encryptedMetadata = emptyMetadata.encryptedMetadata();
+    const FolderMetadata emptyMetadata(_account);
+    const auto encryptedMetadata = emptyMetadata.encryptedMetadata();
     if (encryptedMetadata.isEmpty()) {
         //TODO: Mark the folder as unencrypted as the metadata generation failed.
         _errorString = tr("Could not generate the metadata for encryption, Unlocking the folder.\n"

+ 10 - 4
src/libsync/owncloudpropagator.cpp

@@ -22,6 +22,7 @@
 #include "propagateremotemove.h"
 #include "propagateremotemkdir.h"
 #include "bulkpropagatorjob.h"
+#include "updatefiledropmetadata.h"
 #include "propagatorjobs.h"
 #include "filesystem.h"
 #include "common/utility.h"
@@ -584,7 +585,7 @@ void OwncloudPropagator::start(SyncFileItemVector &&items)
                                       directoriesToRemove,
                                       removedDirectory,
                                       items);
-        } else {
+        } else if (!directories.top().second->_item->_isFileDropDetected) {
             startFilePropagation(item,
                                  directories,
                                  directoriesToRemove,
@@ -645,6 +646,11 @@ void OwncloudPropagator::startDirectoryPropagation(const SyncFileItemPtr &item,
         const auto currentDirJob = directories.top().second;
         currentDirJob->appendJob(directoryPropagationJob.get());
     }
+    if (item->_isFileDropDetected) {
+        directoryPropagationJob->appendJob(new UpdateFileDropMetadataJob(this, item->_file));
+        item->_instruction = CSYNC_INSTRUCTION_NONE;
+        _anotherSyncNeeded = true;
+    }
     directories.push(qMakePair(item->destination() + "/", directoryPropagationJob.release()));
 }
 
@@ -1066,7 +1072,7 @@ OwncloudPropagator *PropagatorJob::propagator() const
 
 // ================================================================================
 
-PropagatorJob::JobParallelism PropagatorCompositeJob::parallelism()
+PropagatorJob::JobParallelism PropagatorCompositeJob::parallelism() const
 {
     // If any of the running sub jobs is not parallel, we have to wait
     for (int i = 0; i < _runningJobs.count(); ++i) {
@@ -1215,7 +1221,7 @@ PropagateDirectory::PropagateDirectory(OwncloudPropagator *propagator, const Syn
     connect(&_subJobs, &PropagatorJob::finished, this, &PropagateDirectory::slotSubJobsFinished);
 }
 
-PropagatorJob::JobParallelism PropagateDirectory::parallelism()
+PropagatorJob::JobParallelism PropagateDirectory::parallelism() const
 {
     // If any of the non-finished sub jobs is not parallel, we have to wait
     if (_firstJob && _firstJob->parallelism() != FullParallelism) {
@@ -1330,7 +1336,7 @@ PropagateRootDirectory::PropagateRootDirectory(OwncloudPropagator *propagator)
     connect(&_dirDeletionJobs, &PropagatorJob::finished, this, &PropagateRootDirectory::slotDirDeletionJobsFinished);
 }
 
-PropagatorJob::JobParallelism PropagateRootDirectory::parallelism()
+PropagatorJob::JobParallelism PropagateRootDirectory::parallelism() const
 {
     // the root directory parallelism isn't important
     return WaitForFinished;

+ 6 - 6
src/libsync/owncloudpropagator.h

@@ -62,7 +62,7 @@ class PropagatorCompositeJob;
  *
  * @ingroup libsync
  */
-class PropagatorJob : public QObject
+class OWNCLOUDSYNC_EXPORT PropagatorJob : public QObject
 {
     Q_OBJECT
 
@@ -98,7 +98,7 @@ public:
 
     Q_ENUM(JobParallelism)
 
-    virtual JobParallelism parallelism() { return FullParallelism; }
+    [[nodiscard]] virtual JobParallelism parallelism() const { return FullParallelism; }
 
     /**
      * For "small" jobs
@@ -215,7 +215,7 @@ public:
         return true;
     }
 
-    JobParallelism parallelism() override { return _parallelism; }
+    [[nodiscard]] JobParallelism parallelism() const override { return _parallelism; }
 
     SyncFileItemPtr _item;
 
@@ -254,7 +254,7 @@ public:
     }
 
     bool scheduleSelfOrChild() override;
-    JobParallelism parallelism() override;
+    [[nodiscard]] JobParallelism parallelism() const override;
 
     /*
      * Abort synchronously or asynchronously - some jobs
@@ -320,7 +320,7 @@ public:
     }
 
     bool scheduleSelfOrChild() override;
-    JobParallelism parallelism() override;
+    [[nodiscard]] JobParallelism parallelism() const override;
     void abort(PropagatorJob::AbortType abortType) override
     {
         if (_firstJob)
@@ -366,7 +366,7 @@ public:
     explicit PropagateRootDirectory(OwncloudPropagator *propagator);
 
     bool scheduleSelfOrChild() override;
-    JobParallelism parallelism() override;
+    [[nodiscard]] JobParallelism parallelism() const override;
     void abort(PropagatorJob::AbortType abortType) override;
 
     [[nodiscard]] qint64 committedDiskSpace() const override;

+ 1 - 1
src/libsync/propagateremotemove.h

@@ -57,7 +57,7 @@ public:
     }
     void start() override;
     void abort(PropagatorJob::AbortType abortType) override;
-    JobParallelism parallelism() override { return _item->isDirectory() ? WaitForFinished : FullParallelism; }
+    [[nodiscard]] JobParallelism parallelism() const override { return _item->isDirectory() ? WaitForFinished : FullParallelism; }
 
     /**
      * Rename the directory in the selective sync list

+ 1 - 2
src/libsync/propagateuploadencrypted.cpp

@@ -111,8 +111,7 @@ void PropagateUploadEncrypted::slotFolderEncryptedMetadataError(const QByteArray
     Q_UNUSED(fileId);
     Q_UNUSED(httpReturnCode);
     qCDebug(lcPropagateUploadEncrypted()) << "Error Getting the encrypted metadata. Pretend we got empty metadata.";
-    FolderMetadata emptyMetadata(_propagator->account());
-    emptyMetadata.encryptedMetadata();
+    const FolderMetadata emptyMetadata(_propagator->account());
     auto json = QJsonDocument::fromJson(emptyMetadata.encryptedMetadata());
     slotFolderEncryptedMetadataReceived(json, httpReturnCode);
 }

+ 1 - 1
src/libsync/propagatorjobs.h

@@ -87,7 +87,7 @@ class PropagateLocalRename : public PropagateItemJob
 public:
     PropagateLocalRename(OwncloudPropagator *propagator, const SyncFileItemPtr &item);
     void start() override;
-    JobParallelism parallelism() override { return _item->isDirectory() ? WaitForFinished : FullParallelism; }
+    [[nodiscard]] JobParallelism parallelism() const override { return _item->isDirectory() ? WaitForFinished : FullParallelism; }
 
 private:
     bool deleteOldDbRecord(const QString &fileName);

+ 2 - 0
src/libsync/syncfileitem.h

@@ -317,6 +317,8 @@ public:
     time_t _lastShareStateFetchedTimestamp = 0;
 
     bool _sharedByMe = false;
+
+    bool _isFileDropDetected = false;
 };
 
 inline bool operator<(const SyncFileItemPtr &item1, const SyncFileItemPtr &item2)

+ 212 - 0
src/libsync/updatefiledropmetadata.cpp

@@ -0,0 +1,212 @@
+/*
+ * 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 "updatefiledropmetadata.h"
+
+#include "account.h"
+#include "clientsideencryptionjobs.h"
+#include "clientsideencryption.h"
+#include "syncfileitem.h"
+
+#include <QLoggingCategory>
+#include <QNetworkReply>
+
+namespace OCC {
+
+Q_LOGGING_CATEGORY(lcUpdateFileDropMetadataJob, "nextcloud.sync.propagator.updatefiledropmetadatajob", QtInfoMsg)
+
+}
+
+namespace OCC {
+
+UpdateFileDropMetadataJob::UpdateFileDropMetadataJob(OwncloudPropagator *propagator, const QString &path)
+    : PropagatorJob(propagator)
+    , _path(path)
+{
+}
+
+void UpdateFileDropMetadataJob::start()
+{
+    qCDebug(lcUpdateFileDropMetadataJob) << "Folder is encrypted, let's get the Id from it.";
+    const auto fetchFolderEncryptedIdJob = new LsColJob(propagator()->account(), _path, this);
+    fetchFolderEncryptedIdJob->setProperties({"resourcetype", "http://owncloud.org/ns:fileid"});
+    connect(fetchFolderEncryptedIdJob, &LsColJob::directoryListingSubfolders, this, &UpdateFileDropMetadataJob::slotFolderEncryptedIdReceived);
+    connect(fetchFolderEncryptedIdJob, &LsColJob::finishedWithError, this, &UpdateFileDropMetadataJob::slotFolderEncryptedIdError);
+    fetchFolderEncryptedIdJob->start();
+}
+
+bool UpdateFileDropMetadataJob::scheduleSelfOrChild()
+{
+    if (_state == Finished) {
+        return false;
+    }
+
+    if (_state == NotYetStarted) {
+        _state = Running;
+        start();
+    }
+
+    return true;
+}
+
+PropagatorJob::JobParallelism UpdateFileDropMetadataJob::parallelism() const
+{
+    return PropagatorJob::JobParallelism::WaitForFinished;
+}
+
+void UpdateFileDropMetadataJob::slotFolderEncryptedIdReceived(const QStringList &list)
+{
+    qCDebug(lcUpdateFileDropMetadataJob) << "Received id of folder, trying to lock it so we can prepare the metadata";
+    const auto fetchFolderEncryptedIdJob = qobject_cast<LsColJob *>(sender());
+    Q_ASSERT(fetchFolderEncryptedIdJob);
+    if (!fetchFolderEncryptedIdJob) {
+        qCCritical(lcUpdateFileDropMetadataJob) << "slotFolderEncryptedIdReceived must be called by a signal";
+        emit finished(SyncFileItem::Status::FatalError);
+        return;
+    }
+    Q_ASSERT(!list.isEmpty());
+    if (list.isEmpty()) {
+        qCCritical(lcUpdateFileDropMetadataJob) << "slotFolderEncryptedIdReceived list.isEmpty()";
+        emit finished(SyncFileItem::Status::FatalError);
+        return;
+    }
+    const auto &folderInfo = fetchFolderEncryptedIdJob->_folderInfos.value(list.first());
+    slotTryLock(folderInfo.fileId);
+}
+
+void UpdateFileDropMetadataJob::slotTryLock(const QByteArray &fileId)
+{
+    const auto lockJob = new LockEncryptFolderApiJob(propagator()->account(), fileId, propagator()->_journal, propagator()->account()->e2e()->_publicKey, this);
+    connect(lockJob, &LockEncryptFolderApiJob::success, this, &UpdateFileDropMetadataJob::slotFolderLockedSuccessfully);
+    connect(lockJob, &LockEncryptFolderApiJob::error, this, &UpdateFileDropMetadataJob::slotFolderLockedError);
+    lockJob->start();
+}
+
+void UpdateFileDropMetadataJob::slotFolderLockedSuccessfully(const QByteArray &fileId, const QByteArray &token)
+{
+    qCDebug(lcUpdateFileDropMetadataJob) << "Folder" << fileId << "Locked Successfully for Upload, Fetching Metadata"; 
+    _folderToken = token;
+    _folderId = fileId;
+    _isFolderLocked = true;
+
+    const auto fetchMetadataJob = new GetMetadataApiJob(propagator()->account(), _folderId);
+    connect(fetchMetadataJob, &GetMetadataApiJob::jsonReceived, this, &UpdateFileDropMetadataJob::slotFolderEncryptedMetadataReceived);
+    connect(fetchMetadataJob, &GetMetadataApiJob::error, this, &UpdateFileDropMetadataJob::slotFolderEncryptedMetadataError);
+
+    fetchMetadataJob->start();
+}
+
+void UpdateFileDropMetadataJob::slotFolderEncryptedMetadataError(const QByteArray &fileId, int httpReturnCode)
+{
+    Q_UNUSED(fileId);
+    Q_UNUSED(httpReturnCode);
+    qCDebug(lcUpdateFileDropMetadataJob()) << "Error Getting the encrypted metadata. Pretend we got empty metadata.";
+    const FolderMetadata emptyMetadata(propagator()->account());
+    const auto encryptedMetadataJson = QJsonDocument::fromJson(emptyMetadata.encryptedMetadata());
+    slotFolderEncryptedMetadataReceived(encryptedMetadataJson, httpReturnCode);
+}
+
+void UpdateFileDropMetadataJob::slotFolderEncryptedMetadataReceived(const QJsonDocument &json, int statusCode)
+{
+    qCDebug(lcUpdateFileDropMetadataJob) << "Metadata Received, Preparing it for the new file." << json.toVariant();
+
+    // Encrypt File!
+    _metadata.reset(new FolderMetadata(propagator()->account(), json.toJson(QJsonDocument::Compact), statusCode));
+    if (!_metadata->moveFromFileDropToFiles()) {
+        unlockFolder();
+        return;
+    }
+
+    emit fileDropMetadataParsedAndAdjusted(_metadata.data());
+
+    const auto updateMetadataJob = new UpdateMetadataApiJob(propagator()->account(), _folderId, _metadata->encryptedMetadata(), _folderToken);
+    connect(updateMetadataJob, &UpdateMetadataApiJob::success, this, &UpdateFileDropMetadataJob::slotUpdateMetadataSuccess);
+    connect(updateMetadataJob, &UpdateMetadataApiJob::error, this, &UpdateFileDropMetadataJob::slotUpdateMetadataError);
+    updateMetadataJob->start();
+}
+
+void UpdateFileDropMetadataJob::slotUpdateMetadataSuccess(const QByteArray &fileId)
+{
+    Q_UNUSED(fileId);
+    qCDebug(lcUpdateFileDropMetadataJob) << "Uploading of the metadata success, Encrypting the file";
+
+    qCDebug(lcUpdateFileDropMetadataJob) << "Finalizing the upload part, now the actuall uploader will take over";
+    unlockFolder();
+}
+
+void UpdateFileDropMetadataJob::slotUpdateMetadataError(const QByteArray &fileId, int httpErrorResponse)
+{
+    qCDebug(lcUpdateFileDropMetadataJob) << "Update metadata error for folder" << fileId << "with error" << httpErrorResponse;
+    qCDebug(lcUpdateFileDropMetadataJob()) << "Unlocking the folder.";
+    unlockFolder();
+}
+
+void UpdateFileDropMetadataJob::slotFolderLockedError(const QByteArray &fileId, int httpErrorCode)
+{
+    Q_UNUSED(httpErrorCode);
+    qCDebug(lcUpdateFileDropMetadataJob) << "Folder" << fileId << "with path" << _path << "Coundn't be locked. httpErrorCode" << httpErrorCode;
+    emit finished(SyncFileItem::Status::NormalError);
+}
+
+void UpdateFileDropMetadataJob::slotFolderEncryptedIdError(QNetworkReply *reply)
+{
+    if (!reply) {
+        qCDebug(lcUpdateFileDropMetadataJob) << "Error retrieving the Id of the encrypted folder" << _path;
+    } else {
+        qCDebug(lcUpdateFileDropMetadataJob) << "Error retrieving the Id of the encrypted folder" << _path << "with httpErrorCode" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+    }
+    emit finished(SyncFileItem::Status::NormalError);
+}
+
+void UpdateFileDropMetadataJob::unlockFolder()
+{
+    Q_ASSERT(!_isUnlockRunning);
+
+    if (!_isFolderLocked) {
+        emit finished(SyncFileItem::Status::Success);
+        return;
+    }
+
+    if (_isUnlockRunning) {
+        qCWarning(lcUpdateFileDropMetadataJob) << "Double-call to unlockFolder.";
+        return;
+    }
+
+    _isUnlockRunning = true;
+
+    qCDebug(lcUpdateFileDropMetadataJob) << "Calling Unlock";
+    const auto unlockJob = new UnlockEncryptFolderApiJob(propagator()->account(), _folderId, _folderToken, propagator()->_journal, this);
+
+    connect(unlockJob, &UnlockEncryptFolderApiJob::success, [this](const QByteArray &folderId) {
+        qCDebug(lcUpdateFileDropMetadataJob) << "Successfully Unlocked";
+        _folderToken = "";
+        _folderId = "";
+        _isFolderLocked = false;
+
+        emit folderUnlocked(folderId, 200);
+        _isUnlockRunning = false;
+        emit finished(SyncFileItem::Status::Success);
+    });
+    connect(unlockJob, &UnlockEncryptFolderApiJob::error, [this](const QByteArray &folderId, int httpStatus) {
+        qCDebug(lcUpdateFileDropMetadataJob) << "Unlock Error";
+
+        emit folderUnlocked(folderId, httpStatus);
+        _isUnlockRunning = false;
+        emit finished(SyncFileItem::Status::NormalError);
+    });
+    unlockJob->start();
+}
+
+
+}

+ 69 - 0
src/libsync/updatefiledropmetadata.h

@@ -0,0 +1,69 @@
+/*
+ * 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 "owncloudpropagator.h"
+
+#include <QScopedPointer>
+
+class QNetworkReply;
+
+namespace OCC {
+
+class FolderMetadata;
+
+class OWNCLOUDSYNC_EXPORT UpdateFileDropMetadataJob : public PropagatorJob
+{
+    Q_OBJECT
+
+public:
+    explicit UpdateFileDropMetadataJob(OwncloudPropagator *propagator, const QString &path);
+
+    bool scheduleSelfOrChild() override;
+
+    [[nodiscard]] JobParallelism parallelism() const override;
+
+private slots:
+    void start();
+    void slotFolderEncryptedIdReceived(const QStringList &list);
+    void slotFolderEncryptedIdError(QNetworkReply *reply);
+    void slotFolderLockedSuccessfully(const QByteArray &fileId, const QByteArray &token);
+    void slotFolderLockedError(const QByteArray &fileId, int httpErrorCode);
+    void slotTryLock(const QByteArray &fileId);
+    void slotFolderEncryptedMetadataReceived(const QJsonDocument &json, int statusCode);
+    void slotFolderEncryptedMetadataError(const QByteArray &fileId, int httpReturnCode);
+    void slotUpdateMetadataSuccess(const QByteArray &fileId);
+    void slotUpdateMetadataError(const QByteArray &fileId, int httpReturnCode);
+    void unlockFolder();
+
+signals:
+    void folderUnlocked(const QByteArray &folderId, int httpStatus);
+
+    void fileDropMetadataParsedAndAdjusted(const FolderMetadata *const metadata);
+
+private:
+    QString _path;
+    bool _currentLockingInProgress = false;
+
+    bool _isUnlockRunning = false;
+    bool _isFolderLocked = false;
+    
+    QByteArray _folderToken;
+    QByteArray _folderId;
+
+    QScopedPointer<FolderMetadata> _metadata;
+};
+
+}

+ 6 - 0
test/CMakeLists.txt

@@ -69,6 +69,12 @@ nextcloud_add_test(LockFile)
 nextcloud_add_test(ShareModel)
 nextcloud_add_test(ShareeModel)
 nextcloud_add_test(SortedShareModel)
+nextcloud_add_test(SecureFileDrop)
+
+target_link_libraries(SecureFileDropTest PRIVATE Nextcloud::sync)
+configure_file(fake2eelocksucceeded.json "${PROJECT_BINARY_DIR}/bin/fake2eelocksucceeded.json" COPYONLY)
+configure_file(fakefiledrope2eefoldermetadata.json "${PROJECT_BINARY_DIR}/bin/fakefiledrope2eefoldermetadata.json" COPYONLY)
+
 
 if(ADD_E2E_TESTS)
     nextcloud_add_test(E2eServerSetup)

+ 10 - 0
test/fake2eelocksucceeded.json

@@ -0,0 +1,10 @@
+{
+	"ocs": {
+		"data": { "e2e-token": "U1SHqQwKzjEIlJUkFIcpYPJeZsM80T6OkegKFu2pSc6BFqORcGfB0Y8PZzRjc6Lm" },
+		"meta": {
+			"message": "OK",
+			"status": "ok",
+			"statuscode": 200
+		}
+	}
+}

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 2 - 0
test/fakefiledrope2eefoldermetadata.json


+ 5 - 0
test/syncenginetestutils.cpp

@@ -1014,6 +1014,11 @@ QJsonObject FakeQNAM::forEachReplyPart(QIODevice *outgoingData,
 
 QNetworkReply *FakeQNAM::createRequest(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData)
 {
+    if (op == QNetworkAccessManager::CustomOperation) {
+        qInfo() << "Operation" << request.attribute(QNetworkRequest::CustomVerbAttribute).toString() << request.url();
+    } else {
+        qInfo() << "Operation" << op << request.url();
+    }
     QNetworkReply *reply = nullptr;
     auto newRequest = request;
     newRequest.setRawHeader("X-Request-ID", OCC::AccessManager::generateRequestId());

+ 168 - 0
test/testsecurefiledrop.cpp

@@ -0,0 +1,168 @@
+/*
+ *    This software is in the public domain, furnished "as is", without technical
+ *    support, and with no warranty, express or implied, as to its usefulness for
+ *    any purpose.
+ *
+ */
+
+#include "updatefiledropmetadata.h"
+#include "syncengine.h"
+#include "syncenginetestutils.h"
+#include "testhelper.h"
+#include "owncloudpropagator_p.h"
+#include "propagatorjobs.h"
+#include "clientsideencryption.h"
+
+#include <QtTest>
+
+namespace
+{
+   constexpr auto fakeE2eeFolderName = "fake_e2ee_folder";
+   const QString fakeE2eeFolderPath = QStringLiteral("/") + fakeE2eeFolderName;
+ };
+
+using namespace OCC;
+
+class TestSecureFileDrop : public QObject
+{
+    Q_OBJECT
+
+    FakeFolder _fakeFolder{FileInfo()};
+    QSharedPointer<OwncloudPropagator> _propagator;
+    QScopedPointer<FolderMetadata> _parsedMetadataWithFileDrop;
+    QScopedPointer<FolderMetadata> _parsedMetadataAfterProcessingFileDrop;
+
+    int _lockCallsCount = 0;
+    int _unlockCallsCount = 0;
+    int _propFindCallsCount = 0;
+    int _getMetadataCallsCount = 0;
+    int _putMetadataCallsCount = 0;
+
+private slots:
+    void initTestCase()
+    {
+        _fakeFolder.remoteModifier().mkdir(fakeE2eeFolderName);
+        _fakeFolder.remoteModifier().insert(fakeE2eeFolderName + QStringLiteral("/") + QStringLiteral("fake_e2ee_file"), 100);
+        _fakeFolder.setServerOverride([this](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) {
+            Q_UNUSED(device);
+            QNetworkReply *reply = nullptr;
+
+            const auto path = req.url().path();
+
+            if (path.contains(QStringLiteral("/end_to_end_encryption/api/v1/lock/"))) {
+                if (op == QNetworkAccessManager::DeleteOperation) {
+                    reply = new FakePayloadReply(op, req, {}, nullptr);
+                    ++_unlockCallsCount;
+                } else if (op == QNetworkAccessManager::PostOperation) {
+                    QFile fakeJsonReplyFile(QStringLiteral("fake2eelocksucceeded.json"));
+                    if (fakeJsonReplyFile.open(QFile::ReadOnly)) {
+                        const auto jsonDoc = QJsonDocument::fromJson(fakeJsonReplyFile.readAll());
+                        reply = new FakePayloadReply(op, req, jsonDoc.toJson(), nullptr);
+                        ++_lockCallsCount;
+                    } else {
+                        qCritical() << "Could not open fake JSON file!";
+                        reply = new FakePayloadReply(op, req, {}, nullptr);
+                    }
+                }
+            } else if (path.contains(QStringLiteral("/end_to_end_encryption/api/v1/meta-data/"))) {
+                if (op == QNetworkAccessManager::GetOperation) {
+                    QFile fakeJsonReplyFile(QStringLiteral("fakefiledrope2eefoldermetadata.json"));
+                    if (fakeJsonReplyFile.open(QFile::ReadOnly)) {
+                        const auto jsonDoc = QJsonDocument::fromJson(fakeJsonReplyFile.readAll());
+                        _parsedMetadataWithFileDrop.reset(new FolderMetadata(_fakeFolder.syncEngine().account(), jsonDoc.toJson()));
+                        _parsedMetadataAfterProcessingFileDrop.reset(new FolderMetadata(_fakeFolder.syncEngine().account(), jsonDoc.toJson()));
+                        [[maybe_unused]] const auto result = _parsedMetadataAfterProcessingFileDrop->moveFromFileDropToFiles();
+                        reply = new FakePayloadReply(op, req, jsonDoc.toJson(), nullptr);
+                        ++_getMetadataCallsCount;
+                    } else {
+                        qCritical() << "Could not open fake JSON file!";
+                        reply = new FakePayloadReply(op, req, {}, nullptr);
+                    }
+                } else if (op == QNetworkAccessManager::PutOperation) {
+                    reply = new FakePayloadReply(op, req, {}, nullptr);
+                    ++_putMetadataCallsCount;
+                }
+            } else if (req.attribute(QNetworkRequest::CustomVerbAttribute) == QStringLiteral("PROPFIND") && path.endsWith(fakeE2eeFolderPath)) {
+                auto fileState = _fakeFolder.currentRemoteState();
+                reply = new FakePropfindReply(fileState, op, req, nullptr);
+                ++_propFindCallsCount;
+            }
+
+            return reply;
+        });
+
+        auto transProgress = connect(&_fakeFolder.syncEngine(), &SyncEngine::transmissionProgress, [&](const ProgressInfo &pi) {
+            Q_UNUSED(pi);
+            _propagator = _fakeFolder.syncEngine().getPropagator();
+        });
+
+        QVERIFY(_fakeFolder.syncOnce());
+
+        disconnect(transProgress);
+    };
+
+    void testUpdateFileDropMetadata()
+    {
+        const auto updateFileDropMetadataJob = new UpdateFileDropMetadataJob(_propagator.data(), fakeE2eeFolderPath);
+        connect(updateFileDropMetadataJob, &UpdateFileDropMetadataJob::fileDropMetadataParsedAndAdjusted, this, [this](const FolderMetadata *const metadata) {
+            if (!metadata || metadata->files().isEmpty() || metadata->fileDrop().isEmpty()) {
+                return;
+            }
+
+            if (_parsedMetadataAfterProcessingFileDrop->files().size() != metadata->files().size()) {
+                return;
+            }
+
+            if (_parsedMetadataAfterProcessingFileDrop->fileDrop() != metadata->fileDrop()) {
+                return;
+            }
+
+            bool isAnyFileDropFileMissing = false;
+
+            for (const auto &key : metadata->fileDrop().keys()) {
+                if (std::find_if(metadata->files().constBegin(), metadata->files().constEnd(), [&key](const EncryptedFile &encryptedFile) {
+                    return encryptedFile.encryptedFilename == key;
+                }) == metadata->files().constEnd()) {
+                    isAnyFileDropFileMissing = true;
+                }
+            }
+
+            if (!isAnyFileDropFileMissing) {
+                emit fileDropMetadataParsedAndAdjusted();
+            }
+        });
+        QSignalSpy updateFileDropMetadataJobSpy(updateFileDropMetadataJob, &UpdateFileDropMetadataJob::finished);
+        QSignalSpy fileDropMetadataParsedAndAdjustedSpy(this, &TestSecureFileDrop::fileDropMetadataParsedAndAdjusted);
+        
+        QVERIFY(updateFileDropMetadataJob->scheduleSelfOrChild());
+
+        QVERIFY(updateFileDropMetadataJobSpy.wait(3000));
+
+        QVERIFY(_parsedMetadataWithFileDrop);
+        QVERIFY(_parsedMetadataWithFileDrop->isFileDropPresent());
+
+        QVERIFY(_parsedMetadataAfterProcessingFileDrop);
+
+        QVERIFY(_parsedMetadataAfterProcessingFileDrop->files().size() != _parsedMetadataWithFileDrop->files().size());
+
+        QVERIFY(!updateFileDropMetadataJobSpy.isEmpty());
+        QVERIFY(!updateFileDropMetadataJobSpy.at(0).isEmpty());
+        QCOMPARE(updateFileDropMetadataJobSpy.at(0).first().toInt(), SyncFileItem::Status::Success);
+
+        QVERIFY(!fileDropMetadataParsedAndAdjustedSpy.isEmpty());
+
+        QCOMPARE(_lockCallsCount, 1);
+        QCOMPARE(_unlockCallsCount, 1);
+        QCOMPARE(_propFindCallsCount, 2);
+        QCOMPARE(_getMetadataCallsCount, 1);
+        QCOMPARE(_putMetadataCallsCount, 1);
+
+        updateFileDropMetadataJob->deleteLater();
+    }
+
+signals:
+    void fileDropMetadataParsedAndAdjusted();
+};
+
+QTEST_GUILESS_MAIN(TestSecureFileDrop)
+#include "testsecurefiledrop.moc"

+ 4 - 0
version.h.in

@@ -41,4 +41,8 @@ constexpr int NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_MAJOR = @NEXTCLOUD_SERVER_V
 constexpr int NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_MINOR = @NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_MINOR@;
 constexpr int NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_PATCH = @NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_PATCH@;
 
+constexpr int NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_MAJOR = @NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_MAJOR@;
+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@;
+
 #endif // VERSION_H

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio