mirror of
https://github.com/nextcloud/desktop.git
synced 2024-11-21 12:35:52 +03:00
Implement Secure filedrop link share. Move data from 'filedrop' to 'files' when syncing E2EE folders.
Signed-off-by: alex-z <blackslayer4@gmail.com>
This commit is contained in:
parent
33e1a900ad
commit
b6ba1fe0d6
37 changed files with 830 additions and 91 deletions
|
@ -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 )
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 ¬e)
|
|||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -99,6 +99,8 @@ set(libsync_SRCS
|
|||
syncoptions.cpp
|
||||
theme.h
|
||||
theme.cpp
|
||||
updatefiledropmetadata.h
|
||||
updatefiledropmetadata.cpp
|
||||
clientsideencryption.h
|
||||
clientsideencryption.cpp
|
||||
clientsideencryptionjobs.h
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -259,6 +259,8 @@ public:
|
|||
*/
|
||||
[[nodiscard]] bool serverVersionUnsupported() const;
|
||||
|
||||
[[nodiscard]] bool secureFileDropSupported() const;
|
||||
|
||||
[[nodiscard]] bool isUsernamePrefillSupported() const;
|
||||
|
||||
[[nodiscard]] bool isChecksumRecalculateRequestSupported() const;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
src/libsync/updatefiledropmetadata.cpp
Normal file
212
src/libsync/updatefiledropmetadata.cpp
Normal file
|
@ -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
src/libsync/updatefiledropmetadata.h
Normal file
69
src/libsync/updatefiledropmetadata.h
Normal file
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
|
@ -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
test/fake2eelocksucceeded.json
Normal file
10
test/fake2eelocksucceeded.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"ocs": {
|
||||
"data": { "e2e-token": "U1SHqQwKzjEIlJUkFIcpYPJeZsM80T6OkegKFu2pSc6BFqORcGfB0Y8PZzRjc6Lm" },
|
||||
"meta": {
|
||||
"message": "OK",
|
||||
"status": "ok",
|
||||
"statuscode": 200
|
||||
}
|
||||
}
|
||||
}
|
10
test/fakefiledrope2eefoldermetadata.json
Normal file
10
test/fakefiledrope2eefoldermetadata.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"ocs": {
|
||||
"data": { "meta-data": "{\"files\":{\"083c2bffea5d4b0f824e2fd5df5369d2\":{\"authenticationTag\":\"LRWVv0vR01WUhrv26kGvOg==\",\"encrypted\":\"dUlVcf+8xaMgIxkWd7YYIsYLIotqD3ZjQtES1VsepKa7+aUYJGdNlPT25+CTQl65mY5ggu9d03hiysiBIUO7BH7klyUY9OQM80kGVE1xuWXQ1aCfgiFruN4h1VSS8S/9jrgBojxncBnsGZjU/NOGZUjA1svdE2hM+O4fywPKUyT09an9t2EbqUGgUl242ezJ|ja9flmYfZAl/MUjF11chaA==\",\"initializationVector\":\"vNfZNAVYVs0eGdB5vbo5KA==\",\"metadataKey\":0}},\"metadata\":{\"metadataKeys\":{\"0\":\"GHKkcNTxsyigJODA45neTO+8Y0NDfB+7mez90EwjW39mZvNnCUBeGO2R6vLzEf5apYjDNsWNS5sHvUZ188OLa9zCDmMm00m8dwfMPEUA0H5Rp9yewUbnM8YRl6vCZWvDa5HLTCdC8UCIKsbvuifAvveQXEO/vafzWrP8IAhG6WsNXZ4qqaUX/0pm84KXvHmStH60xpZpT8U/kKBNdxDeOTdp5T6FglRTnsF9wt9cplPtRHV+BzkC5NgfBqAvLVSP8gckj5iJNQrRM7IQmBPO0AMUv9yU609o7X4WUZ4LwNGqwL6ciCzS83BQ+FCEbf4HyViEWrEq2OLVFgDH7ML18Q==\"},\"version\":1},\"filedrop\":{\"1a1e95ae836b4005bf69e369661e81ba\":{\"encrypted\":\"r7o01Y3dBQbOlhR38ulPW77X0aoGS8riwJmnRp0k0fQgfySy5++GJkaTJqSdcQYvw8stn+hU7j6wwOJMA/aJ3UV5i8H7yPR+RQHiKjrU4L379HU+H1Xuu5KbkhbLoOc7GAxaCpyC0USYI4UCUcmKnSpqAhpHdnpScmyYztA6qnfumclIzgSfE87lCRRFnIp4mKm305hD+4gBuk3WfERewqbgK2Yo08sFhOjR6zULrJgqQBbHc61R7TZb18H26u0fa+mjRmehTSiqhy0dDePip7sr8+a0dCNCEcBG8qKK9xPDYCFmDrAq7iaEpcRoZ4CgUavXOSF+zdunBXXWmKTq6w==\",\"initializationVector\":\"hel7omho/1XsYqov8XCXgA==\",\"authenticationTag\":\"ni6/k9UpFEkS5FYoMPU+/g==\",\"metadataKey\":0}}}" },
|
||||
"meta": {
|
||||
"message": "OK",
|
||||
"status": "ok",
|
||||
"statuscode": 200
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
test/testsecurefiledrop.cpp
Normal file
168
test/testsecurefiledrop.cpp
Normal file
|
@ -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"
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue