Add dialog to resolve invalid filenames
authorFelix Weilbach <felix.weilbach@nextcloud.com>
Mon, 6 Sep 2021 13:09:29 +0000 (15:09 +0200)
committerFelix Weilbach (Rebase PR Action) <felix.weilbach@t-online.de>
Thu, 23 Sep 2021 10:45:15 +0000 (10:45 +0000)
Fixes #3751

Signed-off-by: Felix Weilbach <felix.weilbach@nextcloud.com>
src/gui/CMakeLists.txt
src/gui/invalidfilenamedialog.cpp [new file with mode: 0644]
src/gui/invalidfilenamedialog.h [new file with mode: 0644]
src/gui/invalidfilenamedialog.ui [new file with mode: 0644]
src/gui/tray/ActivityListModel.cpp
src/gui/tray/ActivityListModel.h
src/libsync/propagateremotemkdir.cpp
src/libsync/propagateremotemove.h

index ef1d693830763b3f7c9e7febbe000cc72bad7537..94894046dfd049037ea2459cee68e54a0b651f8b 100644 (file)
@@ -18,6 +18,7 @@ set(theme_dir ${CMAKE_SOURCE_DIR}/theme)
 set(client_UI_SRCS
     accountsettings.ui
     conflictdialog.ui
+    invalidfilenamedialog.ui
     foldercreationdialog.ui
     folderwizardsourcepage.ui
     folderwizardtargetpage.ui
@@ -55,6 +56,7 @@ set(client_SRCS
     accountmanager.cpp
     accountsettings.cpp
     application.cpp
+    invalidfilenamedialog.cpp
     conflictdialog.cpp
     conflictsolver.cpp
     connectionvalidator.cpp
diff --git a/src/gui/invalidfilenamedialog.cpp b/src/gui/invalidfilenamedialog.cpp
new file mode 100644 (file)
index 0000000..64e0e8f
--- /dev/null
@@ -0,0 +1,170 @@
+#include "invalidfilenamedialog.h"
+#include "accountfwd.h"
+#include "common/syncjournalfilerecord.h"
+#include "propagateremotemove.h"
+#include "ui_invalidfilenamedialog.h"
+
+#include <folder.h>
+
+#include <QPushButton>
+#include <QDir>
+#include <qabstractbutton.h>
+#include <QDialogButtonBox>
+#include <QFileInfo>
+#include <QPushButton>
+
+#include <array>
+
+namespace {
+constexpr std::array<QChar, 9> illegalCharacters({ '\\', '/', ':', '?', '*', '\"', '<', '>', '|' });
+
+QVector<QChar> getIllegalCharsFromString(const QString &string)
+{
+    QVector<QChar> result;
+    for (const auto &character : string) {
+        if (std::find(illegalCharacters.begin(), illegalCharacters.end(), character)
+            != illegalCharacters.end()) {
+            result.push_back(character);
+        }
+    }
+    return result;
+}
+
+QString illegalCharacterListToString(const QVector<QChar> &illegalCharacters)
+{
+    QString illegalCharactersString;
+    if (illegalCharacters.size() > 0) {
+        illegalCharactersString += illegalCharacters[0];
+    }
+
+    for (int i = 1; i < illegalCharacters.count(); ++i) {
+        if (illegalCharactersString.contains(illegalCharacters[i])) {
+            continue;
+        }
+        illegalCharactersString += " " + illegalCharacters[i];
+    }
+    return illegalCharactersString;
+}
+}
+
+namespace OCC {
+
+InvalidFilenameDialog::InvalidFilenameDialog(AccountPtr account, Folder *folder, QString filePath, QWidget *parent)
+    : QDialog(parent)
+    , _ui(new Ui::InvalidFilenameDialog)
+    , _account(account)
+    , _folder(folder)
+    , _filePath(std::move(filePath))
+{
+    Q_ASSERT(_account);
+    Q_ASSERT(_folder);
+
+    const auto filePathFileInfo = QFileInfo(_filePath);
+    _relativeFilePath = filePathFileInfo.path() + QStringLiteral("/");
+    _relativeFilePath = _relativeFilePath.replace(folder->path(), QStringLiteral(""));
+    _relativeFilePath = _relativeFilePath.isEmpty() ? QStringLiteral("") : _relativeFilePath + QStringLiteral("/");
+
+    _originalFileName = _relativeFilePath + filePathFileInfo.fileName();
+
+    _ui->setupUi(this);
+    _ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
+    _ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Rename file"));
+
+    _ui->descriptionLabel->setText(tr("The file %1 could not be synced because it contains characters which are not allowed on this system.").arg(_originalFileName));
+    _ui->explanationLabel->setText(tr("The following characters are not allowed on the system: * \" | & ? , ; : \\ / ~ < >"));
+    _ui->filenameLineEdit->setText(filePathFileInfo.fileName());
+
+    connect(_ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
+    connect(_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
+
+    connect(_ui->filenameLineEdit, &QLineEdit::textChanged, this,
+        &InvalidFilenameDialog::onFilenameLineEditTextChanged);
+
+    checkIfAllowedToRename();
+}
+
+InvalidFilenameDialog::~InvalidFilenameDialog() = default;
+
+void InvalidFilenameDialog::checkIfAllowedToRename()
+{
+    const auto propfindJob = new PropfindJob(_account, QDir::cleanPath(_folder->remotePath() + _originalFileName));
+    propfindJob->setProperties({ "http://owncloud.org/ns:permissions" });
+    connect(propfindJob, &PropfindJob::result, this, &InvalidFilenameDialog::onPropfindPermissionSuccess);
+    propfindJob->start();
+}
+
+void InvalidFilenameDialog::onPropfindPermissionSuccess(const QVariantMap &values)
+{
+    if (!values.contains("permissions")) {
+        return;
+    }
+    const auto remotePermissions = RemotePermissions::fromServerString(values["permissions"].toString());
+    if (!remotePermissions.hasPermission(remotePermissions.CanRename)
+        || !remotePermissions.hasPermission(remotePermissions.CanMove)) {
+        _ui->errorLabel->setText(
+            tr("You don't have the permission to rename this file. Please ask the author of the file to rename it."));
+        _ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
+        _ui->filenameLineEdit->setEnabled(false);
+    }
+}
+
+void InvalidFilenameDialog::accept()
+{
+    _newFilename = _relativeFilePath + _ui->filenameLineEdit->text().trimmed();
+    const auto propfindJob = new PropfindJob(_account, QDir::cleanPath(_folder->remotePath() + _newFilename));
+    connect(propfindJob, &PropfindJob::result, this, &InvalidFilenameDialog::onRemoteFileAlreadyExists);
+    connect(propfindJob, &PropfindJob::finishedWithError, this, &InvalidFilenameDialog::onRemoteFileDoesNotExist);
+    propfindJob->start();
+}
+
+void InvalidFilenameDialog::onFilenameLineEditTextChanged(const QString &text)
+{
+    const auto isNewFileNameDifferent = text != _originalFileName;
+    const auto illegalContainedCharacters = getIllegalCharsFromString(text);
+    const auto containsIllegalChars = !illegalContainedCharacters.empty() || text.endsWith(QLatin1Char('.'));
+    const auto isTextValid = isNewFileNameDifferent && !containsIllegalChars;
+
+    if (isTextValid) {
+        _ui->errorLabel->setText("");
+    } else {
+        _ui->errorLabel->setText(tr("Filename contains illegal characters: %1")
+                                     .arg(illegalCharacterListToString(illegalContainedCharacters)));
+    }
+
+    _ui->buttonBox->button(QDialogButtonBox::Ok)
+        ->setEnabled(isTextValid);
+}
+
+void InvalidFilenameDialog::onMoveJobFinished()
+{
+    const auto job = qobject_cast<MoveJob *>(sender());
+    const auto error = job->reply()->error();
+
+    if (error != QNetworkReply::NoError) {
+        _ui->errorLabel->setText(tr("Could not rename file. Please make sure you are connected to the server."));
+        return;
+    }
+
+    QDialog::accept();
+}
+
+void InvalidFilenameDialog::onRemoteFileAlreadyExists(const QVariantMap &values)
+{
+    Q_UNUSED(values);
+
+    _ui->errorLabel->setText(tr("Can not rename file because file with the same name does already exist on the server. Please pick another name."));
+    _ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
+}
+
+void InvalidFilenameDialog::onRemoteFileDoesNotExist(QNetworkReply *reply)
+{
+    Q_UNUSED(reply);
+
+    // File does not exist. We can rename it.
+    const auto remoteSource = QDir::cleanPath(_folder->remotePath() + _originalFileName);
+    const auto remoteDestionation = QDir::cleanPath(_account->davUrl().path() + _folder->remotePath() + _newFilename);
+    const auto moveJob = new MoveJob(_account, remoteSource, remoteDestionation, this);
+    connect(moveJob, &MoveJob::finishedSignal, this, &InvalidFilenameDialog::onMoveJobFinished);
+    moveJob->start();
+}
+}
diff --git a/src/gui/invalidfilenamedialog.h b/src/gui/invalidfilenamedialog.h
new file mode 100644 (file)
index 0000000..2ae23ad
--- /dev/null
@@ -0,0 +1,47 @@
+#pragma once
+
+#include <accountfwd.h>
+#include <account.h>
+
+#include <memory>
+
+#include <QDialog>
+
+namespace OCC {
+
+class Folder;
+
+namespace Ui {
+    class InvalidFilenameDialog;
+}
+
+
+class InvalidFilenameDialog : public QDialog
+{
+    Q_OBJECT
+
+public:
+    explicit InvalidFilenameDialog(AccountPtr account, Folder *folder, QString filePath, QWidget *parent = nullptr);
+
+    ~InvalidFilenameDialog() override;
+
+    void accept() override;
+
+private:
+    std::unique_ptr<Ui::InvalidFilenameDialog> _ui;
+
+    AccountPtr _account;
+    Folder *_folder;
+    QString _filePath;
+    QString _relativeFilePath;
+    QString _originalFileName;
+    QString _newFilename;
+
+    void onFilenameLineEditTextChanged(const QString &text);
+    void onMoveJobFinished();
+    void onRemoteFileAlreadyExists(const QVariantMap &values);
+    void onRemoteFileDoesNotExist(QNetworkReply *reply);
+    void checkIfAllowedToRename();
+    void onPropfindPermissionSuccess(const QVariantMap &values);
+};
+}
diff --git a/src/gui/invalidfilenamedialog.ui b/src/gui/invalidfilenamedialog.ui
new file mode 100644 (file)
index 0000000..6800870
--- /dev/null
@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>OCC::InvalidFilenameDialog</class>
+ <widget class="QDialog" name="OCC::InvalidFilenameDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>411</width>
+    <height>192</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Invalid filename</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <property name="sizeConstraint">
+    <enum>QLayout::SetDefaultConstraint</enum>
+   </property>
+   <item>
+    <widget class="QLabel" name="descriptionLabel">
+     <property name="text">
+      <string>The file could not be synced because it contains characters which are not allowed on this system.</string>
+     </property>
+     <property name="alignment">
+      <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+     </property>
+     <property name="wordWrap">
+      <bool>false</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLabel" name="explanationLabel">
+     <property name="text">
+      <string>Error</string>
+     </property>
+     <property name="alignment">
+      <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+     </property>
+     <property name="wordWrap">
+      <bool>false</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLabel" name="label">
+     <property name="text">
+      <string>Please enter a new name for the file:</string>
+     </property>
+     <property name="alignment">
+      <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+     </property>
+     <property name="wordWrap">
+      <bool>false</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLineEdit" name="filenameLineEdit">
+     <property name="placeholderText">
+      <string>New filename</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLabel" name="errorLabel">
+     <property name="palette">
+      <palette>
+       <active>
+        <colorrole role="WindowText">
+         <brush brushstyle="SolidPattern">
+          <color alpha="200">
+           <red>255</red>
+           <green>0</green>
+           <blue>0</blue>
+          </color>
+         </brush>
+        </colorrole>
+       </active>
+       <inactive>
+        <colorrole role="WindowText">
+         <brush brushstyle="SolidPattern">
+          <color alpha="200">
+           <red>255</red>
+           <green>0</green>
+           <blue>0</blue>
+          </color>
+         </brush>
+        </colorrole>
+       </inactive>
+       <disabled>
+        <colorrole role="WindowText">
+         <brush brushstyle="SolidPattern">
+          <color alpha="115">
+           <red>255</red>
+           <green>255</green>
+           <blue>255</blue>
+          </color>
+         </brush>
+        </colorrole>
+       </disabled>
+      </palette>
+     </property>
+     <property name="text">
+      <string/>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
index 1a9a7578ac1e2f7d0ef286ffee4f8f831a4a87ef..22ce0ae528560e99c4fb614683a53ae566c9b5bd 100644 (file)
@@ -28,6 +28,7 @@
 #include "accessmanager.h"
 #include "owncloudgui.h"
 #include "guiutility.h"
+#include "invalidfilenamedialog.h"
 
 #include "ActivityData.h"
 #include "ActivityListModel.h"
@@ -446,6 +447,21 @@ void ActivityListModel::triggerDefaultAction(int activityIndex)
         _currentConflictDialog->open();
         ownCloudGui::raiseDialog(_currentConflictDialog);
         return;
+    } else if (activity._status == SyncFileItem::FileNameInvalid) {
+        if (!_currentInvalidFilenameDialog.isNull()) {
+            _currentInvalidFilenameDialog->close();
+        }
+
+        auto folder = FolderMan::instance()->folder(activity._folder);
+        const auto folderDir = QDir(folder->path());
+        _currentInvalidFilenameDialog = new InvalidFilenameDialog(_accountState->account(), folder,
+            folderDir.filePath(activity._file));
+        connect(_currentInvalidFilenameDialog, &InvalidFilenameDialog::accepted, folder, [folder]() {
+            folder->scheduleThisFolderSoon();
+        });
+        _currentInvalidFilenameDialog->open();
+        ownCloudGui::raiseDialog(_currentInvalidFilenameDialog);
+        return;
     }
 
     if (path.isValid()) {
index 554ac320f16838bdf0844029615e90feaa8a8aff..6c67af92d1f11531df467840589c0ba73d40e2d9 100644 (file)
@@ -27,6 +27,7 @@ Q_DECLARE_LOGGING_CATEGORY(lcActivity)
 
 class AccountState;
 class ConflictDialog;
+class InvalidFilenameDialog;
 
 /**
  * @brief The ActivityListModel
@@ -115,6 +116,7 @@ private:
     bool _showMoreActivitiesAvailableEntry = false;
 
     QPointer<ConflictDialog> _currentConflictDialog;
+    QPointer<InvalidFilenameDialog> _currentInvalidFilenameDialog;
 };
 }
 
index 4e27b42a8180b58971ab7ceaae18354eb8910dea..af056d5e902d22c5bda4b054d481a71a1824c39f 100644 (file)
@@ -139,7 +139,7 @@ void PropagateRemoteMkdir::finalizeMkColJob(QNetworkReply::NetworkError err, con
 
     propagator()->_activeJobList.append(this);
     auto propfindJob = new PropfindJob(_job->account(), _job->path(), this);
-    propfindJob->setProperties({"http://owncloud.org/ns:permissions"});
+    propfindJob->setProperties({ "oc:permissions" });
     connect(propfindJob, &PropfindJob::result, this, [this, jobPath](const QVariantMap &result){
         propagator()->_activeJobList.removeOne(this);
         _item->_remotePerm = RemotePermissions::fromServerString(result.value(QStringLiteral("permissions")).toString());
index 567896dd8e3155ecfd722e7c49290b19c6c9759c..64ee0842e3729cca2150ab97340629de6fb0467c 100644 (file)
@@ -22,7 +22,7 @@ namespace OCC {
  * @brief The MoveJob class
  * @ingroup libsync
  */
-class MoveJob : public AbstractNetworkJob
+class OWNCLOUDSYNC_EXPORT MoveJob : public AbstractNetworkJob
 {
     Q_OBJECT
     const QString _destination;