/* * Copyright (C) by Daniel Molkentin * * 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 "accountsettings.h" #include "common/syncjournaldb.h" #include "common/syncjournalfilerecord.h" #include "qmessagebox.h" #include "ui_accountsettings.h" #include "theme.h" #include "foldercreationdialog.h" #include "folderman.h" #include "folderwizard.h" #include "folderstatusmodel.h" #include "folderstatusdelegate.h" #include "common/utility.h" #include "guiutility.h" #include "application.h" #include "configfile.h" #include "account.h" #include "accountstate.h" #include "userinfo.h" #include "accountmanager.h" #include "owncloudsetupwizard.h" #include "creds/abstractcredentials.h" #include "creds/httpcredentialsgui.h" #include "tooltipupdater.h" #include "filesystem.h" #include "encryptfolderjob.h" #include "syncresult.h" #include "ignorelisttablewidget.h" #include "wizard/owncloudwizard.h" #include "networksettings.h" #include "ui_mnemonicdialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef BUILD_FILE_PROVIDER_MODULE #include "macOS/fileprovider.h" #endif #include "account.h" namespace { constexpr auto propertyFolder = "folder"; constexpr auto propertyPath = "path"; constexpr auto e2eUiActionIdKey = "id"; constexpr auto e2EeUiActionEnableEncryptionId = "enable_encryption"; constexpr auto e2EeUiActionDisableEncryptionId = "disable_encryption"; constexpr auto e2EeUiActionDisplayMnemonicId = "display_mnemonic"; } namespace OCC { class AccountSettings; Q_LOGGING_CATEGORY(lcAccountSettings, "nextcloud.gui.account.settings", QtInfoMsg) static const char progressBarStyleC[] = "QProgressBar {" "border: 1px solid grey;" "border-radius: 5px;" "text-align: center;" "}" "QProgressBar::chunk {" "background-color: %1; width: 1px;" "}"; void showEnableE2eeWithVirtualFilesWarningDialog(std::function onAccept) { const auto messageBox = new QMessageBox; messageBox->setAttribute(Qt::WA_DeleteOnClose); messageBox->setText(AccountSettings::tr("End-to-end Encryption with Virtual Files")); messageBox->setInformativeText(AccountSettings::tr("You seem to have the Virtual Files feature enabled on this folder. " "At the moment, it is not possible to implicitly download virtual files that are " "end-to-end encrypted. To get the best experience with virtual files and " "end-to-end encryption, make sure the encrypted folder is marked with " "\"Make always available locally\".")); messageBox->setIcon(QMessageBox::Warning); const auto dontEncryptButton = messageBox->addButton(QMessageBox::StandardButton::Cancel); Q_ASSERT(dontEncryptButton); dontEncryptButton->setText(AccountSettings::tr("Do not encrypt folder")); const auto encryptButton = messageBox->addButton(QMessageBox::StandardButton::Ok); Q_ASSERT(encryptButton); encryptButton->setText(AccountSettings::tr("Encrypt folder")); QObject::connect(messageBox, &QMessageBox::accepted, onAccept); messageBox->open(); } void showEnableE2eeWarningDialog(std::function onAccept) { const auto messageBox = new QMessageBox; messageBox->setAttribute(Qt::WA_DeleteOnClose); messageBox->setText(AccountSettings::tr("End-to-end Encryption")); messageBox->setInformativeText( AccountSettings::tr("This will encrypt your folder and all files within it. " "These files will no longer be accessible without your encryption mnemonic key. " "\nThis process is not reversible. Are you sure you want to proceed?")); messageBox->setIcon(QMessageBox::Warning); const auto dontEncryptButton = messageBox->addButton(QMessageBox::StandardButton::Cancel); Q_ASSERT(dontEncryptButton); dontEncryptButton->setText(AccountSettings::tr("Do not encrypt folder")); const auto encryptButton = messageBox->addButton(QMessageBox::StandardButton::Ok); Q_ASSERT(encryptButton); encryptButton->setText(AccountSettings::tr("Encrypt folder")); QObject::connect(messageBox, &QMessageBox::accepted, onAccept); messageBox->open(); } /** * Adjusts the mouse cursor based on the region it is on over the folder tree view. * * Used to show that one can click the red error list box by changing the cursor * to the pointing hand. */ class MouseCursorChanger : public QObject { Q_OBJECT public: MouseCursorChanger(QObject *parent) : QObject(parent) { } QTreeView *folderList = nullptr; FolderStatusModel *model = nullptr; protected: bool eventFilter(QObject *watched, QEvent *event) override { if (event->type() == QEvent::HoverMove) { Qt::CursorShape shape = Qt::ArrowCursor; const auto pos = folderList->mapFromGlobal(QCursor::pos()); const auto index = folderList->indexAt(pos); if (model->classify(index) == FolderStatusModel::RootFolder && (FolderStatusDelegate::errorsListRect(folderList->visualRect(index)).contains(pos) || FolderStatusDelegate::optionsButtonRect(folderList->visualRect(index),folderList->layoutDirection()).contains(pos))) { shape = Qt::PointingHandCursor; } folderList->setCursor(shape); } return QObject::eventFilter(watched, event); } }; AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent) : QWidget(parent) , _ui(new Ui::AccountSettings) , _model(new FolderStatusModel) , _accountState(accountState) , _userInfo(accountState, false, true) { _ui->setupUi(this); _model->setAccountState(_accountState); _model->setParent(this); const auto delegate = new FolderStatusDelegate; delegate->setParent(this); // Connect styleChanged events to our widgets, so they can adapt (Dark-/Light-Mode switching) connect(this, &AccountSettings::styleChanged, delegate, &FolderStatusDelegate::slotStyleChanged); _ui->_folderList->header()->hide(); _ui->_folderList->setItemDelegate(delegate); _ui->_folderList->setModel(_model); #if defined(Q_OS_MAC) _ui->_folderList->setMinimumWidth(400); #else _ui->_folderList->setMinimumWidth(300); #endif new ToolTipUpdater(_ui->_folderList); #if defined(BUILD_FILE_PROVIDER_MODULE) if (Mac::FileProvider::fileProviderAvailable()) { const auto fileProviderTab = _ui->fileProviderTab; const auto fpSettingsLayout = new QVBoxLayout(fileProviderTab); const auto fpAccountUserIdAtHost = _accountState->account()->userIdAtHostWithPort(); const auto fpSettingsController = Mac::FileProviderSettingsController::instance(); const auto fpSettingsWidget = fpSettingsController->settingsViewWidget(fpAccountUserIdAtHost, fileProviderTab); fpSettingsLayout->addWidget(fpSettingsWidget); fileProviderTab->setLayout(fpSettingsLayout); } #else const auto tabWidget = _ui->tabWidget; const auto fileProviderTab = _ui->fileProviderTab; if (const auto fileProviderWidgetTabIndex = tabWidget->indexOf(fileProviderTab); fileProviderWidgetTabIndex >= 0) { tabWidget->removeTab(fileProviderWidgetTabIndex); } tabWidget->setCurrentIndex(0); #endif const auto connectionSettingsTab = _ui->connectionSettingsTab; const auto connectionSettingsLayout = new QVBoxLayout(connectionSettingsTab); const auto networkSettings = new NetworkSettings(_accountState->account(), connectionSettingsTab); connectionSettingsLayout->addWidget(networkSettings); connectionSettingsTab->setLayout(connectionSettingsLayout); const auto mouseCursorChanger = new MouseCursorChanger(this); mouseCursorChanger->folderList = _ui->_folderList; mouseCursorChanger->model = _model; _ui->_folderList->setMouseTracking(true); _ui->_folderList->setAttribute(Qt::WA_Hover, true); _ui->_folderList->installEventFilter(mouseCursorChanger); connect(this, &AccountSettings::removeAccountFolders, AccountManager::instance(), &AccountManager::removeAccountFolders); connect(_ui->_folderList, &QWidget::customContextMenuRequested, this, &AccountSettings::slotCustomContextMenuRequested); connect(_ui->_folderList, &QAbstractItemView::clicked, this, &AccountSettings::slotFolderListClicked); connect(_ui->_folderList, &QTreeView::expanded, this, &AccountSettings::refreshSelectiveSyncStatus); connect(_ui->_folderList, &QTreeView::collapsed, this, &AccountSettings::refreshSelectiveSyncStatus); connect(_ui->selectiveSyncNotification, &QLabel::linkActivated, this, &AccountSettings::slotLinkActivated); connect(_model, &FolderStatusModel::suggestExpand, _ui->_folderList, &QTreeView::expand); connect(_model, &FolderStatusModel::dirtyChanged, this, &AccountSettings::refreshSelectiveSyncStatus); refreshSelectiveSyncStatus(); connect(_model, &QAbstractItemModel::rowsInserted, this, &AccountSettings::refreshSelectiveSyncStatus); auto *syncNowAction = new QAction(this); syncNowAction->setShortcut(QKeySequence(Qt::Key_F6)); connect(syncNowAction, &QAction::triggered, this, &AccountSettings::slotScheduleCurrentFolder); addAction(syncNowAction); auto *syncNowWithRemoteDiscovery = new QAction(this); syncNowWithRemoteDiscovery->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_F6)); connect(syncNowWithRemoteDiscovery, &QAction::triggered, this, &AccountSettings::slotScheduleCurrentFolderForceRemoteDiscovery); addAction(syncNowWithRemoteDiscovery); slotHideSelectiveSyncWidget(); _ui->bigFolderUi->setVisible(false); connect(_model, &QAbstractItemModel::dataChanged, this, &AccountSettings::slotSelectiveSyncChanged); connect(_ui->selectiveSyncApply, &QAbstractButton::clicked, this, &AccountSettings::slotHideSelectiveSyncWidget); connect(_ui->selectiveSyncCancel, &QAbstractButton::clicked, this, &AccountSettings::slotHideSelectiveSyncWidget); connect(_ui->selectiveSyncApply, &QAbstractButton::clicked, _model, &FolderStatusModel::slotApplySelectiveSync); connect(_ui->selectiveSyncCancel, &QAbstractButton::clicked, _model, &FolderStatusModel::resetFolders); connect(_ui->bigFolderApply, &QAbstractButton::clicked, _model, &FolderStatusModel::slotApplySelectiveSync); connect(_ui->bigFolderSyncAll, &QAbstractButton::clicked, _model, &FolderStatusModel::slotSyncAllPendingBigFolders); connect(_ui->bigFolderSyncNone, &QAbstractButton::clicked, _model, &FolderStatusModel::slotSyncNoPendingBigFolders); connect(FolderMan::instance(), &FolderMan::folderListChanged, _model, &FolderStatusModel::resetFolders); connect(this, &AccountSettings::folderChanged, _model, &FolderStatusModel::resetFolders); // quotaProgressBar style now set in customizeStyle() /*QColor color = palette().highlight().color(); _ui->quotaProgressBar->setStyleSheet(QString::fromLatin1(progressBarStyleC).arg(color.name()));*/ // Connect E2E stuff initializeE2eEncryption(); _ui->encryptionMessage->setCloseButtonVisible(false); _ui->connectLabel->setText(tr("No account configured.")); connect(_accountState, &AccountState::stateChanged, this, &AccountSettings::slotAccountStateChanged); slotAccountStateChanged(); connect(&_userInfo, &UserInfo::quotaUpdated, this, &AccountSettings::slotUpdateQuota); customizeStyle(); } void AccountSettings::slotE2eEncryptionMnemonicReady() { const auto actionDisableEncryption = addActionToEncryptionMessage(tr("Disable encryption"), e2EeUiActionDisableEncryptionId); connect(actionDisableEncryption, &QAction::triggered, this, [this] { disableEncryptionForAccount(_accountState->account()); }); const auto actionDisplayMnemonic = addActionToEncryptionMessage(tr("Display mnemonic"), e2EeUiActionDisplayMnemonicId); connect(actionDisplayMnemonic, &QAction::triggered, this, [this]() { displayMnemonic(_accountState->account()->e2e()->_mnemonic); }); _ui->encryptionMessage->setMessageType(KMessageWidget::Positive); _ui->encryptionMessage->setText(tr("End-to-end encryption has been enabled for this account")); _ui->encryptionMessage->setIcon(Theme::createColorAwareIcon(QStringLiteral(":/client/theme/lock.svg"))); _ui->encryptionMessage->show(); } void AccountSettings::slotE2eEncryptionGenerateKeys() { connect(_accountState->account()->e2e(), &ClientSideEncryption::initializationFinished, this, &AccountSettings::slotE2eEncryptionInitializationFinished); _accountState->account()->setE2eEncryptionKeysGenerationAllowed(true); _accountState->account()->setAskUserForMnemonic(true); _accountState->account()->e2e()->initialize(_accountState->account()); } void AccountSettings::slotE2eEncryptionInitializationFinished(bool isNewMnemonicGenerated) { disconnect(_accountState->account()->e2e(), &ClientSideEncryption::initializationFinished, this, &AccountSettings::slotE2eEncryptionInitializationFinished); if (!_accountState->account()->e2e()->_mnemonic.isEmpty()) { removeActionFromEncryptionMessage(e2EeUiActionEnableEncryptionId); slotE2eEncryptionMnemonicReady(); if (isNewMnemonicGenerated) { displayMnemonic(_accountState->account()->e2e()->_mnemonic); } } _accountState->account()->setAskUserForMnemonic(false); } void AccountSettings::slotEncryptFolderFinished(int status) { qCInfo(lcAccountSettings) << "Current folder encryption status code:" << status; auto job = qobject_cast(sender()); Q_ASSERT(job); if (!job->errorString().isEmpty()) { QMessageBox::warning(nullptr, tr("Warning"), job->errorString()); } const auto folder = job->property(propertyFolder).value(); Q_ASSERT(folder); const auto path = job->property(propertyPath).toString(); const auto index = _model->indexForPath(folder, path); Q_ASSERT(index.isValid()); _model->resetAndFetch(index.parent()); job->deleteLater(); } QString AccountSettings::selectedFolderAlias() const { const auto selected = _ui->_folderList->selectionModel()->currentIndex(); if (!selected.isValid()) { return ""; } return _model->data(selected, FolderStatusDelegate::FolderAliasRole).toString(); } void AccountSettings::slotToggleSignInState() { if (_accountState->isSignedOut()) { _accountState->account()->resetRejectedCertificates(); _accountState->signIn(); } else { _accountState->signOutByUi(); } } void AccountSettings::doExpand() { // Make sure at least the root items are expanded for (int i = 0; i < _model->rowCount(); ++i) { const auto idx = _model->index(i); if (!_ui->_folderList->isExpanded(idx)) { _ui->_folderList->setExpanded(idx, true); } } } bool AccountSettings::canEncryptOrDecrypt(const FolderStatusModel::SubFolderInfo *info) { if (const auto folderSyncStatus = info->_folder->syncResult().status(); folderSyncStatus != SyncResult::Status::Success) { auto message = tr("Please wait for the folder to sync before trying to encrypt it."); if (folderSyncStatus == SyncResult::Status::Problem) { message = tr("The folder has a minor sync problem. Encryption of this folder will be possible once it has synced successfully"); } else if (folderSyncStatus == SyncResult::Status::Error) { message = tr("The folder has a sync error. Encryption of this folder will be possible once it has synced successfully"); } QMessageBox msgBox; msgBox.setText(message); msgBox.exec(); return false; } if (!_accountState->account()->e2e() || _accountState->account()->e2e()->_mnemonic.isEmpty()) { QMessageBox msgBox; msgBox.setText(tr("End-to-end encryption is not configured on this device. " "Once it is configured, you will be able to encrypt this folder.\n" "Would you like to set up end-to-end encryption?")); msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); msgBox.setDefaultButton(QMessageBox::Ok); const auto ret = msgBox.exec(); switch (ret) { case QMessageBox::Ok: slotE2eEncryptionGenerateKeys(); break; case QMessageBox::Cancel: default: break; } return false; } // for some reason the actual folder in disk is info->_folder->path + info->_path. QDir folderPath(info->_folder->path() + info->_path); folderPath.setFilter( QDir::AllEntries | QDir::NoDotAndDotDot ); if (folderPath.count() != 0) { QMessageBox msgBox; msgBox.setText(tr("You cannot encrypt a folder with contents, please remove the files.\n" "Wait for the new sync, then encrypt it.")); msgBox.exec(); return false; } return true; } void AccountSettings::slotMarkSubfolderEncrypted(FolderStatusModel::SubFolderInfo* folderInfo) { if (!canEncryptOrDecrypt(folderInfo)) { return; } const auto folder = folderInfo->_folder; Q_ASSERT(folder); const auto folderAlias = folder->alias(); const auto path = folderInfo->_path; const auto fileId = folderInfo->_fileId; const auto encryptFolder = [this, fileId, path, folderAlias] { const auto folder = FolderMan::instance()->folder(folderAlias); if (!folder) { qCWarning(lcAccountSettings) << "Could not encrypt folder because folder" << folderAlias << "does not exist anymore"; QMessageBox::warning(nullptr, tr("Encryption failed"), tr("Could not encrypt folder because the folder does not exist anymore")); return; } // Folder info have directory paths in Foo/Bar/ convention... Q_ASSERT(!path.startsWith('/') && path.endsWith('/')); // But EncryptFolderJob expects directory path Foo/Bar convention const auto choppedPath = path.chopped(1); auto job = new OCC::EncryptFolderJob(accountsState()->account(), folder->journalDb(), choppedPath, choppedPath, folder->remotePath(), fileId); job->setParent(this); job->setProperty(propertyFolder, QVariant::fromValue(folder)); job->setProperty(propertyPath, QVariant::fromValue(path)); connect(job, &OCC::EncryptFolderJob::finished, this, &AccountSettings::slotEncryptFolderFinished); job->start(); }; if (folder->virtualFilesEnabled() && folder->vfs().mode() == Vfs::WindowsCfApi) { showEnableE2eeWithVirtualFilesWarningDialog(encryptFolder); return; } showEnableE2eeWarningDialog(encryptFolder); } void AccountSettings::slotEditCurrentIgnoredFiles() { const auto folder = FolderMan::instance()->folder(selectedFolderAlias()); if (!folder) { return; } openIgnoredFilesDialog(folder->path()); } void AccountSettings::slotOpenMakeFolderDialog() { const auto selected = _ui->_folderList->selectionModel()->currentIndex(); if (!selected.isValid()) { qCWarning(lcAccountSettings) << "Selection model current folder index is not valid."; return; } const auto classification = _model->classify(selected); if (classification != FolderStatusModel::SubFolder && classification != FolderStatusModel::RootFolder) { return; } const auto folder = _model->infoForIndex(selected)->_folder; Q_ASSERT(folder); const auto fileName = [selected, classification, folder, this] { QString result; if (classification == FolderStatusModel::RootFolder) { result = folder->path(); } else { result = _model->data(selected, FolderStatusDelegate::FolderPathRole).toString(); } if (result.endsWith('/')) { result.chop(1); } return result; }(); if (!fileName.isEmpty()) { const auto folderCreationDialog = new FolderCreationDialog(fileName, this); folderCreationDialog->setAttribute(Qt::WA_DeleteOnClose); folderCreationDialog->open(); #ifdef Q_OS_MAC // The macOS FolderWatcher cannot detect file and folder changes made by the watching process -- us. // So we need to manually invoke the slot that is called by watched folder changes. connect(folderCreationDialog, &FolderCreationDialog::folderCreated, this, [folder, fileName](const QString &fullFolderPath) { folder->slotWatchedPathChanged(fullFolderPath, Folder::ChangeReason::Other); }); #endif } } void AccountSettings::slotEditCurrentLocalIgnoredFiles() { const auto selected = _ui->_folderList->selectionModel()->currentIndex(); if (!selected.isValid() || _model->classify(selected) != FolderStatusModel::SubFolder) { return; } const auto fileName = _model->data(selected, FolderStatusDelegate::FolderPathRole).toString(); openIgnoredFilesDialog(fileName); } void AccountSettings::openIgnoredFilesDialog(const QString & absFolderPath) { Q_ASSERT(QFileInfo(absFolderPath).isAbsolute()); const QString ignoreFile{absFolderPath + ".sync-exclude.lst"}; const auto layout = new QVBoxLayout(); const auto ignoreListWidget = new IgnoreListTableWidget(this); ignoreListWidget->readIgnoreFile(ignoreFile); layout->addWidget(ignoreListWidget); const auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); layout->addWidget(buttonBox); const auto dialog = new QDialog(); dialog->setLayout(layout); connect(buttonBox, &QDialogButtonBox::clicked, [=](QAbstractButton * button) { if (buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole) { ignoreListWidget->slotWriteIgnoreFile(ignoreFile); } dialog->close(); }); connect(buttonBox, &QDialogButtonBox::rejected, dialog, &QDialog::close); dialog->open(); } void AccountSettings::slotSubfolderContextMenuRequested(const QModelIndex& index, const QPoint& pos) { Q_UNUSED(pos); QMenu menu; auto ac = menu.addAction(tr("Open folder")); connect(ac, &QAction::triggered, this, &AccountSettings::slotOpenCurrentLocalSubFolder); const auto fileName = _model->data(index, FolderStatusDelegate::FolderPathRole).toString(); if (!QFile::exists(fileName)) { ac->setEnabled(false); } const auto info = _model->infoForIndex(index); const auto acc = _accountState->account(); if (acc->capabilities().clientSideEncryptionAvailable()) { // Verify if the folder is empty before attempting to encrypt. const auto isEncrypted = info->isEncrypted(); const auto isParentEncrypted = _model->isAnyAncestorEncrypted(index); if (!isEncrypted && !isParentEncrypted) { ac = menu.addAction(tr("Encrypt")); connect(ac, &QAction::triggered, [this, info] { slotMarkSubfolderEncrypted(info); }); } else { // Ignore decrypting for now since it only works with an empty folder // connect(ac, &QAction::triggered, [this, &info] { slotMarkSubfolderDecrypted(info); }); } } ac = menu.addAction(tr("Edit Ignored Files")); connect(ac, &QAction::triggered, this, &AccountSettings::slotEditCurrentLocalIgnoredFiles); ac = menu.addAction(tr("Create new folder")); connect(ac, &QAction::triggered, this, &AccountSettings::slotOpenMakeFolderDialog); ac->setEnabled(QFile::exists(fileName)); const auto folder = info->_folder; if (folder && folder->virtualFilesEnabled()) { auto availabilityMenu = menu.addMenu(tr("Availability")); // Has '/' suffix convention for paths here but VFS and // sync engine expects no such suffix Q_ASSERT(info->_path.endsWith('/')); const auto remotePath = info->_path.chopped(1); // It might be an E2EE mangled path, so let's try to demangle it const auto journal = folder->journalDb(); SyncJournalFileRecord rec; if (!journal->getFileRecordByE2eMangledName(remotePath, &rec)) { qCWarning(lcFolderStatus) << "Could not get file record by E2E Mangled Name from local DB" << remotePath; } const auto path = rec.isValid() ? rec._path : remotePath; ac = availabilityMenu->addAction(Utility::vfsPinActionText()); connect(ac, &QAction::triggered, this, [this, folder, path] { slotSetSubFolderAvailability(folder, path, PinState::AlwaysLocal); }); ac = availabilityMenu->addAction(Utility::vfsFreeSpaceActionText()); connect(ac, &QAction::triggered, this, [this, folder, path] { slotSetSubFolderAvailability(folder, path, PinState::OnlineOnly); }); } menu.exec(QCursor::pos()); } void AccountSettings::slotCustomContextMenuRequested(const QPoint &pos) { const auto treeView = _ui->_folderList; const auto index = treeView->indexAt(pos); if (!index.isValid()) { return; } if (_model->classify(index) == FolderStatusModel::SubFolder) { slotSubfolderContextMenuRequested(index, pos); return; } if (_model->classify(index) != FolderStatusModel::RootFolder) { return; } treeView->setCurrentIndex(index); const auto alias = _model->data(index, FolderStatusDelegate::FolderAliasRole).toString(); const auto folderPaused = _model->data(index, FolderStatusDelegate::FolderSyncPaused).toBool(); const auto folderConnected = _model->data(index, FolderStatusDelegate::FolderAccountConnected).toBool(); const auto folderMan = FolderMan::instance(); const auto folder = folderMan->folder(alias); if (!folder) { return; } const auto menu = new QMenu(treeView); menu->setAttribute(Qt::WA_DeleteOnClose); auto ac = menu->addAction(tr("Open folder")); connect(ac, &QAction::triggered, this, &AccountSettings::slotOpenCurrentFolder); ac = menu->addAction(tr("Edit Ignored Files")); connect(ac, &QAction::triggered, this, &AccountSettings::slotEditCurrentIgnoredFiles); ac = menu->addAction(tr("Create new folder")); connect(ac, &QAction::triggered, this, &AccountSettings::slotOpenMakeFolderDialog); ac->setEnabled(QFile::exists(folder->path())); if (!_ui->_folderList->isExpanded(index) && folder->supportsSelectiveSync()) { ac = menu->addAction(tr("Choose what to sync")); ac->setEnabled(folderConnected); connect(ac, &QAction::triggered, this, &AccountSettings::doExpand); } if (!folderPaused) { ac = menu->addAction(tr("Force sync now")); if (folder && folder->isSyncRunning()) { ac->setText(tr("Restart sync")); } ac->setEnabled(folderConnected); connect(ac, &QAction::triggered, this, &AccountSettings::slotForceSyncCurrentFolder); } ac = menu->addAction(folderPaused ? tr("Resume sync") : tr("Pause sync")); connect(ac, &QAction::triggered, this, &AccountSettings::slotEnableCurrentFolder); ac = menu->addAction(tr("Remove folder sync connection")); connect(ac, &QAction::triggered, this, &AccountSettings::slotRemoveCurrentFolder); if (folder->virtualFilesEnabled()) { auto availabilityMenu = menu->addMenu(tr("Availability")); ac = availabilityMenu->addAction(Utility::vfsPinActionText()); connect(ac, &QAction::triggered, this, [this]() { slotSetCurrentFolderAvailability(PinState::AlwaysLocal); }); ac->setDisabled(Theme::instance()->enforceVirtualFilesSyncFolder()); ac = availabilityMenu->addAction(Utility::vfsFreeSpaceActionText()); connect(ac, &QAction::triggered, this, [this]() { slotSetCurrentFolderAvailability(PinState::OnlineOnly); }); ac = menu->addAction(tr("Disable virtual file support …")); connect(ac, &QAction::triggered, this, &AccountSettings::slotDisableVfsCurrentFolder); ac->setDisabled(Theme::instance()->enforceVirtualFilesSyncFolder()); } if (Theme::instance()->showVirtualFilesOption() && !folder->virtualFilesEnabled() && Vfs::checkAvailability(folder->path())) { const auto mode = bestAvailableVfsMode(); if (mode == Vfs::WindowsCfApi || ConfigFile().showExperimentalOptions()) { ac = menu->addAction(tr("Enable virtual file support %1 …").arg(mode == Vfs::WindowsCfApi ? QString() : tr("(experimental)"))); // TODO: remove when UX decision is made ac->setEnabled(!Utility::isPathWindowsDrivePartitionRoot(folder->path())); // connect(ac, &QAction::triggered, this, &AccountSettings::slotEnableVfsCurrentFolder); } } menu->popup(treeView->mapToGlobal(pos)); } void AccountSettings::slotFolderListClicked(const QModelIndex &indx) { if (indx.data(FolderStatusDelegate::AddButton).toBool()) { // "Add Folder Sync Connection" const auto treeView = _ui->_folderList; const auto pos = treeView->mapFromGlobal(QCursor::pos()); QStyleOptionViewItem opt; opt.initFrom(treeView); const auto btnRect = treeView->visualRect(indx); const auto btnSize = treeView->itemDelegate(indx)->sizeHint(opt, indx); const auto actual = QStyle::visualRect(opt.direction, btnRect, QRect(btnRect.topLeft(), btnSize)); if (!actual.contains(pos)) { return; } if (indx.flags() & Qt::ItemIsEnabled) { slotAddFolder(); } else { QToolTip::showText( QCursor::pos(), _model->data(indx, Qt::ToolTipRole).toString(), this); } return; } if (_model->classify(indx) == FolderStatusModel::RootFolder) { // tries to find if we clicked on the '...' button. const auto treeView = _ui->_folderList; const auto pos = treeView->mapFromGlobal(QCursor::pos()); if (FolderStatusDelegate::optionsButtonRect(treeView->visualRect(indx), layoutDirection()).contains(pos)) { slotCustomContextMenuRequested(pos); return; } if (FolderStatusDelegate::errorsListRect(treeView->visualRect(indx)).contains(pos)) { emit showIssuesList(_accountState); return; } // Expand root items on single click if (_accountState && _accountState->state() == AccountState::Connected) { const auto expanded = !(_ui->_folderList->isExpanded(indx)); _ui->_folderList->setExpanded(indx, expanded); } } } void AccountSettings::slotAddFolder() { const auto folderMan = FolderMan::instance(); folderMan->setSyncEnabled(false); // do not start more syncs. const auto folderWizard = new FolderWizard(_accountState->account(), this); folderWizard->setAttribute(Qt::WA_DeleteOnClose); connect(folderWizard, &QDialog::accepted, this, &AccountSettings::slotFolderWizardAccepted); connect(folderWizard, &QDialog::rejected, this, &AccountSettings::slotFolderWizardRejected); folderWizard->open(); } void AccountSettings::slotFolderWizardAccepted() { const auto folderWizard = qobject_cast(sender()); const auto folderMan = FolderMan::instance(); qCInfo(lcAccountSettings) << "Folder wizard completed"; FolderDefinition definition; definition.localPath = FolderDefinition::prepareLocalPath( folderWizard->field(QLatin1String("sourceFolder")).toString()); definition.targetPath = FolderDefinition::prepareTargetPath( folderWizard->property("targetPath").toString()); if (folderWizard->property("useVirtualFiles").toBool()) { definition.virtualFilesMode = bestAvailableVfsMode(); } { QDir dir(definition.localPath); if (!dir.exists()) { qCInfo(lcAccountSettings) << "Creating folder" << definition.localPath; if (!dir.mkpath(".")) { QMessageBox::warning(this, tr("Folder creation failed"), tr("

Could not create local folder %1.

") .arg(QDir::toNativeSeparators(definition.localPath))); return; } } FileSystem::setFolderMinimumPermissions(definition.localPath); Utility::setupFavLink(definition.localPath); } /* take the value from the definition of already existing folders. All folders have * the same setting so far. * The default is to sync hidden files */ definition.ignoreHiddenFiles = folderMan->ignoreHiddenFiles(); if (folderMan->navigationPaneHelper().showInExplorerNavigationPane()) { definition.navigationPaneClsid = QUuid::createUuid(); } const auto selectiveSyncBlackList = folderWizard->property("selectiveSyncBlackList").toStringList(); folderMan->setSyncEnabled(true); const auto folder = folderMan->addFolder(_accountState, definition); if (folder) { if (definition.virtualFilesMode != Vfs::Off && folderWizard->property("useVirtualFiles").toBool()) { folder->setRootPinState(PinState::OnlineOnly); } folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, selectiveSyncBlackList); // The user already accepted the selective sync dialog. everything is in the white list folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncWhiteList, QStringList() << QLatin1String("/")); folderMan->scheduleAllFolders(); emit folderChanged(); } } void AccountSettings::slotFolderWizardRejected() { qCInfo(lcAccountSettings) << "Folder wizard cancelled"; const auto folderMan = FolderMan::instance(); folderMan->setSyncEnabled(true); } void AccountSettings::slotRemoveCurrentFolder() { const auto folder = FolderMan::instance()->folder(selectedFolderAlias()); const auto selected = _ui->_folderList->selectionModel()->currentIndex(); if (selected.isValid() && folder) { const auto row = selected.row(); qCInfo(lcAccountSettings) << "Remove Folder alias " << folder->alias(); const auto shortGuiLocalPath = folder->shortGuiLocalPath(); auto messageBox = new QMessageBox(QMessageBox::Question, tr("Confirm Folder Sync Connection Removal"), tr("

Do you really want to stop syncing the folder %1?

" "

Note: This will not delete any files.

") .arg(shortGuiLocalPath), QMessageBox::NoButton, this); messageBox->setAttribute(Qt::WA_DeleteOnClose); const auto yesButton = messageBox->addButton(tr("Remove Folder Sync Connection"), QMessageBox::YesRole); messageBox->addButton(tr("Cancel"), QMessageBox::NoRole); connect(messageBox, &QMessageBox::finished, this, [messageBox, yesButton, folder, row, this]{ if (messageBox->clickedButton() == yesButton) { Utility::removeFavLink(folder->path()); FolderMan::instance()->removeFolder(folder); _model->removeRow(row); // single folder fix to show add-button and hide remove-button emit folderChanged(); } }); messageBox->open(); } } void AccountSettings::slotOpenCurrentFolder() { const auto alias = selectedFolderAlias(); if (!alias.isEmpty()) { emit openFolderAlias(alias); } } void AccountSettings::slotOpenCurrentLocalSubFolder() { const auto selected = _ui->_folderList->selectionModel()->currentIndex(); if (!selected.isValid() || _model->classify(selected) != FolderStatusModel::SubFolder) { return; } const auto fileName = _model->data(selected, FolderStatusDelegate::FolderPathRole).toString(); const auto url = QUrl::fromLocalFile(fileName); QDesktopServices::openUrl(url); } void AccountSettings::slotEnableVfsCurrentFolder() { const auto folderMan = FolderMan::instance(); const auto folder = folderMan->folder(selectedFolderAlias()); const auto selected = _ui->_folderList->selectionModel()->currentIndex(); if (!selected.isValid() || !folder) { return; } OwncloudWizard::askExperimentalVirtualFilesFeature(this, [folder, this](bool enable) { if (!enable || !folder) { return; } // we might need to add or remove the panel entry as cfapi brings this feature out of the box FolderMan::instance()->navigationPaneHelper().scheduleUpdateCloudStorageRegistry(); // It is unsafe to switch on vfs while a sync is running - wait if necessary. const auto connection = std::make_shared(); const auto switchVfsOn = [folder, connection, this]() { if (*connection) { QObject::disconnect(*connection); } qCInfo(lcAccountSettings) << "Enabling vfs support for folder" << folder->path(); // Wipe selective sync blacklist auto ok = false; const auto oldBlacklist = folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &ok); folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, {}); // Change the folder vfs mode and load the plugin folder->setVirtualFilesEnabled(true); folder->setVfsOnOffSwitchPending(false); // Setting to Unspecified retains existing data. // Selective sync excluded folders become OnlineOnly. folder->setRootPinState(PinState::Unspecified); for (const auto &entry : oldBlacklist) { folder->journalDb()->schedulePathForRemoteDiscovery(entry); if (!folder->vfs().setPinState(entry, PinState::OnlineOnly)) { qCWarning(lcAccountSettings) << "Could not set pin state of" << entry << "to online only"; } } folder->slotNextSyncFullLocalDiscovery(); FolderMan::instance()->scheduleFolder(folder); _ui->_folderList->doItemsLayout(); _ui->selectiveSyncStatus->setVisible(false); }; if (folder->isSyncRunning()) { *connection = connect(folder, &Folder::syncFinished, this, switchVfsOn); folder->setVfsOnOffSwitchPending(true); folder->slotTerminateSync(); _ui->_folderList->doItemsLayout(); } else { switchVfsOn(); } }); } void AccountSettings::slotDisableVfsCurrentFolder() { const auto folderMan = FolderMan::instance(); const auto folder = folderMan->folder(selectedFolderAlias()); const auto selected = _ui->_folderList->selectionModel()->currentIndex(); if (!selected.isValid() || !folder) { return; } const auto msgBox = new QMessageBox( QMessageBox::Question, tr("Disable virtual file support?"), tr("This action will disable virtual file support. As a consequence contents of folders that " "are currently marked as \"available online only\" will be downloaded." "\n\n" "The only advantage of disabling virtual file support is that the selective sync feature " "will become available again." "\n\n" "This action will abort any currently running synchronization.")); const auto acceptButton = msgBox->addButton(tr("Disable support"), QMessageBox::AcceptRole); msgBox->addButton(tr("Cancel"), QMessageBox::RejectRole); connect(msgBox, &QMessageBox::finished, msgBox, [this, msgBox, folder, acceptButton] { msgBox->deleteLater(); if (msgBox->clickedButton() != acceptButton|| !folder) { return; } // we might need to add or remove the panel entry as cfapi brings this feature out of the box FolderMan::instance()->navigationPaneHelper().scheduleUpdateCloudStorageRegistry(); // It is unsafe to switch off vfs while a sync is running - wait if necessary. const auto connection = std::make_shared(); const auto switchVfsOff = [folder, connection, this]() { if (*connection) { QObject::disconnect(*connection); } qCInfo(lcAccountSettings) << "Disabling vfs support for folder" << folder->path(); // Also wipes virtual files, schedules remote discovery folder->setVirtualFilesEnabled(false); folder->setVfsOnOffSwitchPending(false); // Wipe pin states and selective sync db folder->setRootPinState(PinState::AlwaysLocal); folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, {}); // Prevent issues with missing local files folder->slotNextSyncFullLocalDiscovery(); FolderMan::instance()->scheduleFolder(folder); _ui->_folderList->doItemsLayout(); }; if (folder->isSyncRunning()) { *connection = connect(folder, &Folder::syncFinished, this, switchVfsOff); folder->setVfsOnOffSwitchPending(true); folder->slotTerminateSync(); _ui->_folderList->doItemsLayout(); } else { switchVfsOff(); } }); msgBox->open(); } void AccountSettings::slotSetCurrentFolderAvailability(PinState state) { ASSERT(state == PinState::OnlineOnly || state == PinState::AlwaysLocal); const auto folderMan = FolderMan::instance(); const auto folder = folderMan->folder(selectedFolderAlias()); const auto selected = _ui->_folderList->selectionModel()->currentIndex(); if (!selected.isValid() || !folder) { return; } // similar to socket api: sets pin state recursively and sync folder->setRootPinState(state); folder->scheduleThisFolderSoon(); } void AccountSettings::slotSetSubFolderAvailability(Folder *folder, const QString &path, PinState state) { Q_ASSERT(folder && folder->virtualFilesEnabled()); Q_ASSERT(!path.endsWith('/')); // Update the pin state on all items if (!folder->vfs().setPinState(path, state)) { qCWarning(lcAccountSettings) << "Could not set pin state of" << path << "to" << state; } // Trigger sync folder->schedulePathForLocalDiscovery(path); folder->scheduleThisFolderSoon(); } void AccountSettings::displayMnemonic(const QString &mnemonic) { QDialog widget; Ui_Dialog ui{}; ui.setupUi(&widget); widget.setWindowTitle(tr("End-to-end encryption mnemonic")); ui.label->setText( tr("To protect your Cryptographic Identity, we encrypt it with a mnemonic of 12 dictionary words. " "Please note these down and keep them safe. " "They will be needed to add other devices to your account (like your mobile phone or laptop).")); QFont monoFont(QStringLiteral("Monospace")); monoFont.setStyleHint(QFont::TypeWriter); ui.lineEdit->setFont(monoFont); ui.lineEdit->setText(mnemonic); ui.lineEdit->setReadOnly(true); ui.lineEdit->setStyleSheet(QStringLiteral("QLineEdit{ color: black; background: lightgrey; border-style: inset;}")); ui.lineEdit->focusWidget(); ui.lineEdit->selectAll(); ui.lineEdit->setAlignment(Qt::AlignCenter); const QFont font(QStringLiteral(""), 0); QFontMetrics fm(font); ui.lineEdit->setFixedWidth(fm.horizontalAdvance(mnemonic)); widget.resize(widget.sizeHint()); widget.exec(); } void AccountSettings::disableEncryptionForAccount(const AccountPtr &account) const { QMessageBox dialog; dialog.setWindowTitle(tr("Disable end-to-end encryption")); dialog.setText(tr("Disable end-to-end encryption for %1?").arg(account->davUser())); dialog.setInformativeText(tr("Removing end-to-end encryption will remove locally-synced files that are encrypted." "
" "Encrypted files will remain on the server.")); dialog.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); dialog.setDefaultButton(QMessageBox::Ok); dialog.adjustSize(); const auto ret = dialog.exec(); switch(ret) { case QMessageBox::Ok: connect(account->e2e(), &ClientSideEncryption::sensitiveDataForgotten, this, &AccountSettings::resetE2eEncryption); account->e2e()->forgetSensitiveData(account); break; case QMessageBox::Cancel: break; Q_UNREACHABLE(); } } void AccountSettings::showConnectionLabel(const QString &message, QStringList errors) { const auto errStyle = QLatin1String("color:#ffffff; background-color:#bb4d4d;padding:5px;" "border-width: 1px; border-style: solid; border-color: #aaaaaa;" "border-radius:5px;"); if (errors.isEmpty()) { auto msg = message; Theme::replaceLinkColorStringBackgroundAware(msg); _ui->connectLabel->setText(msg); _ui->connectLabel->setToolTip({}); _ui->connectLabel->setStyleSheet({}); } else { errors.prepend(message); auto userFriendlyMsg = errors.join(QLatin1String("
")); qCDebug(lcAccountSettings) << userFriendlyMsg; Theme::replaceLinkColorString(userFriendlyMsg, QColor("#c1c8e6")); _ui->connectLabel->setText(userFriendlyMsg); _ui->connectLabel->setToolTip({}); _ui->connectLabel->setStyleSheet(errStyle); } _ui->accountStatus->setVisible(!message.isEmpty()); } void AccountSettings::slotEnableCurrentFolder(bool terminate) { const auto alias = selectedFolderAlias(); if (!alias.isEmpty()) { const auto folderMan = FolderMan::instance(); qCInfo(lcAccountSettings) << "Application: enable folder with alias " << alias; auto currentlyPaused = false; // this sets the folder status to disabled but does not interrupt it. const auto folder = folderMan->folder(alias); if (!folder) { return; } currentlyPaused = folder->syncPaused(); if (!currentlyPaused && !terminate) { // check if a sync is still running and if so, ask if we should terminate. if (folder->isBusy()) { // its still running const auto msgbox = new QMessageBox(QMessageBox::Question, tr("Sync Running"), tr("The syncing operation is running.
Do you want to terminate it?"), QMessageBox::Yes | QMessageBox::No, this); msgbox->setAttribute(Qt::WA_DeleteOnClose); msgbox->setDefaultButton(QMessageBox::Yes); connect(msgbox, &QMessageBox::accepted, this, [this]{ slotEnableCurrentFolder(true); }); msgbox->open(); return; } } // message box can return at any time while the thread keeps running, // so better check again after the user has responded. if (folder->isBusy() && terminate) { folder->slotTerminateSync(); } folder->setSyncPaused(!currentlyPaused); // keep state for the icon setting. if (currentlyPaused) { _wasDisabledBefore = true; } _model->slotUpdateFolderState(folder); } } void AccountSettings::slotScheduleCurrentFolder() { const auto folderMan = FolderMan::instance(); if (auto folder = folderMan->folder(selectedFolderAlias())) { folderMan->scheduleFolder(folder); } } void AccountSettings::slotScheduleCurrentFolderForceRemoteDiscovery() { const auto folderMan = FolderMan::instance(); if (auto folder = folderMan->folder(selectedFolderAlias())) { folder->slotWipeErrorBlacklist(); folder->journalDb()->forceRemoteDiscoveryNextSync(); folderMan->scheduleFolder(folder); } } void AccountSettings::slotForceSyncCurrentFolder() { FolderMan *folderMan = FolderMan::instance(); auto selectedFolder = folderMan->folder(selectedFolderAlias()); folderMan->forceSyncForFolder(selectedFolder); } void AccountSettings::slotOpenOC() { if (_OCUrl.isValid()) { Utility::openBrowser(_OCUrl); } } void AccountSettings::slotUpdateQuota(qint64 total, qint64 used) { if (total > 0) { _ui->quotaProgressBar->setVisible(true); _ui->quotaProgressBar->setEnabled(true); // workaround the label only accepting ints (which may be only 32 bit wide) const auto percent = used / (double)total * 100; const auto percentInt = qMin(qRound(percent), 100); _ui->quotaProgressBar->setValue(percentInt); const auto usedStr = Utility::octetsToString(used); const auto totalStr = Utility::octetsToString(total); const auto percentStr = Utility::compactFormatDouble(percent, 1); const auto toolTip = tr("%1 (%3%) of %2 in use. Some folders, including network mounted or shared folders, might have different limits.").arg(usedStr, totalStr, percentStr); _ui->quotaInfoLabel->setText(tr("%1 of %2 in use").arg(usedStr, totalStr)); _ui->quotaInfoLabel->setToolTip(toolTip); _ui->quotaProgressBar->setToolTip(toolTip); } else { _ui->quotaProgressBar->setVisible(false); _ui->quotaInfoLabel->setToolTip({}); /* -1 means not computed; -2 means unknown; -3 means unlimited (#owncloud/client/issues/3940)*/ if (total == 0 || total == -1) { _ui->quotaInfoLabel->setText(tr("Currently there is no storage usage information available.")); } else { const auto usedStr = Utility::octetsToString(used); _ui->quotaInfoLabel->setText(tr("%1 in use").arg(usedStr)); } } } void AccountSettings::slotAccountStateChanged() { const auto state = _accountState ? _accountState->state() : AccountState::Disconnected; if (state != AccountState::Disconnected) { _ui->sslButton->updateAccountState(_accountState); const auto account = _accountState->account(); auto safeUrl = account->url(); safeUrl.setPassword({}); // Remove the password from the URL to avoid showing it in the UI const auto folders = FolderMan::instance()->map().values(); for (const auto folder : folders) { _model->slotUpdateFolderState(folder); } const auto server = QString::fromLatin1("%2") .arg(Utility::escape(account->url().toString()), Utility::escape(safeUrl.toString())); auto serverWithUser = server; if (const auto cred = account->credentials()) { auto user = account->davDisplayName(); if (user.isEmpty()) { user = cred->user(); } serverWithUser = tr("%1 as %2").arg(server, Utility::escape(user)); } switch (state) { case AccountState::Connected: { QStringList errors; if (account->serverVersionUnsupported()) { errors << tr("The server version %1 is unsupported! Proceed at your own risk.").arg(account->serverVersion()); } showConnectionLabel(tr("Connected to %1.").arg(serverWithUser), errors); break; } case AccountState::ServiceUnavailable: showConnectionLabel(tr("Server %1 is temporarily unavailable.").arg(server)); break; case AccountState::MaintenanceMode: showConnectionLabel(tr("Server %1 is currently in maintenance mode.").arg(server)); break; case AccountState::RedirectDetected: showConnectionLabel(tr("Server %1 is currently being redirected, or your connection is behind a captive portal.").arg(server)); break; case AccountState::SignedOut: showConnectionLabel(tr("Signed out from %1.").arg(serverWithUser)); break; case AccountState::AskingCredentials: { showConnectionLabel(tr("Connecting to %1 …").arg(serverWithUser)); break; } case AccountState::NetworkError: showConnectionLabel(tr("Unable to connect to %1.") .arg(Utility::escape(Theme::instance()->appNameGUI())), _accountState->connectionErrors()); break; case AccountState::ConfigurationError: showConnectionLabel(tr("Server configuration error: %1 at %2.") .arg(Utility::escape(Theme::instance()->appNameGUI()), server), _accountState->connectionErrors()); break; case AccountState::Disconnected: // we can't end up here as the whole block is ifdeffed Q_UNREACHABLE(); break; } } else { // ownCloud is not yet configured. showConnectionLabel(tr("No %1 connection configured.") .arg(Utility::escape(Theme::instance()->appNameGUI()))); } /* Allow to expand the item if the account is connected. */ _ui->_folderList->setItemsExpandable(state == AccountState::Connected); if (state != AccountState::Connected) { /* check if there are expanded root items, if so, close them */ for (auto i = 0; i < _model->rowCount(); ++i) { if (_ui->_folderList->isExpanded(_model->index(i))) { _ui->_folderList->setExpanded(_model->index(i), false); } } } else if (_model->isDirty()) { // If we connect and have pending changes, show the list. doExpand(); } // Disabling expansion of folders might require hiding the selective // sync user interface buttons. refreshSelectiveSyncStatus(); if (state == AccountState::State::Connected) { checkClientSideEncryptionState(); } } void AccountSettings::checkClientSideEncryptionState() { /* TODO: We should probably do something better here. * Verify if the user has a private key already uploaded to the server, * if it has, do not offer to create one. */ qCInfo(lcAccountSettings) << "Account" << accountsState()->account()->displayName() << "Client Side Encryption" << accountsState()->account()->capabilities().clientSideEncryptionAvailable(); if (_accountState->account()->capabilities().clientSideEncryptionAvailable()) { _ui->encryptionMessage->show(); } } void AccountSettings::slotLinkActivated(const QString &link) { // Parse folder alias and filename from the link, calculate the index // and select it if it exists. const auto li = link.split(QLatin1String("?folder=")); if (li.count() > 1) { auto myFolder = li[0]; const auto alias = li[1]; if (myFolder.endsWith(QLatin1Char('/'))) { myFolder.chop(1); } // Make sure the folder itself is expanded const auto folder = FolderMan::instance()->folder(alias); const auto folderIndx = _model->indexForPath(folder, {}); if (!_ui->_folderList->isExpanded(folderIndx)) { _ui->_folderList->setExpanded(folderIndx, true); } const auto indx = _model->indexForPath(folder, myFolder); if (indx.isValid()) { // make sure all the parents are expanded for (auto i = indx.parent(); i.isValid(); i = i.parent()) { if (!_ui->_folderList->isExpanded(i)) { _ui->_folderList->setExpanded(i, true); } } _ui->_folderList->setSelectionMode(QAbstractItemView::SingleSelection); _ui->_folderList->setCurrentIndex(indx); _ui->_folderList->scrollTo(indx); } else { qCWarning(lcAccountSettings) << "Unable to find a valid index for " << myFolder; } } } AccountSettings::~AccountSettings() { delete _ui; } void AccountSettings::slotHideSelectiveSyncWidget() { _ui->selectiveSyncApply->setEnabled(false); _ui->selectiveSyncStatus->setVisible(false); _ui->selectiveSyncButtons->setVisible(false); _ui->selectiveSyncLabel->hide(); } void AccountSettings::slotSelectiveSyncChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { Q_UNUSED(bottomRight); if (!roles.contains(Qt::CheckStateRole)) { return; } const auto info = _model->infoForIndex(topLeft); if (!info) { return; } const auto showWarning = _model->isDirty() && _accountState->isConnected() && info->_checked == Qt::Unchecked; // FIXME: the model is not precise enough to handle extra cases // e.g. the user clicked on the same checkbox 2x without applying the change in between. // We don't know which checkbox changed to be able to toggle the selectiveSyncLabel display. if (showWarning) { _ui->selectiveSyncLabel->show(); } const auto shouldBeVisible = _model->isDirty(); const auto wasVisible = _ui->selectiveSyncStatus->isVisible(); if (shouldBeVisible) { _ui->selectiveSyncStatus->setVisible(true); } _ui->selectiveSyncApply->setEnabled(true); _ui->selectiveSyncButtons->setVisible(true); if (shouldBeVisible != wasVisible) { const auto hint = _ui->selectiveSyncStatus->sizeHint(); if (shouldBeVisible) { _ui->selectiveSyncStatus->setMaximumHeight(0); } const auto anim = new QPropertyAnimation(_ui->selectiveSyncStatus, "maximumHeight", _ui->selectiveSyncStatus); anim->setEndValue(_model->isDirty() ? hint.height() : 0); anim->start(QAbstractAnimation::DeleteWhenStopped); connect(anim, &QPropertyAnimation::finished, [this, shouldBeVisible]() { _ui->selectiveSyncStatus->setMaximumHeight(QWIDGETSIZE_MAX); if (!shouldBeVisible) { _ui->selectiveSyncStatus->hide(); } }); } } void AccountSettings::slotPossiblyUnblacklistE2EeFoldersAndRestartSync() { if (_accountState->account()->e2e()->_mnemonic.isEmpty()) { return; } disconnect(_accountState->account()->e2e(), &ClientSideEncryption::initializationFinished, this, &AccountSettings::slotPossiblyUnblacklistE2EeFoldersAndRestartSync); for (const auto folder : FolderMan::instance()->map()) { if (folder->accountState() != _accountState) { continue; } bool ok = false; const auto foldersToRemoveFromBlacklist = folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncE2eFoldersToRemoveFromBlacklist, &ok); if (foldersToRemoveFromBlacklist.isEmpty()) { continue; } auto blackList = folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &ok); const auto blackListSize = blackList.size(); if (blackListSize == 0) { continue; } for (const auto &pathToRemoveFromBlackList : foldersToRemoveFromBlacklist) { blackList.removeAll(pathToRemoveFromBlackList); } if (blackList.size() != blackListSize) { if (folder->isSyncRunning()) { folderTerminateSyncAndUpdateBlackList(blackList, folder, foldersToRemoveFromBlacklist); return; } updateBlackListAndScheduleFolderSync(blackList, folder, foldersToRemoveFromBlacklist); } } } void AccountSettings::updateBlackListAndScheduleFolderSync(const QStringList &blackList, OCC::Folder *folder, const QStringList &foldersToRemoveFromBlacklist) const { folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, blackList); folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncE2eFoldersToRemoveFromBlacklist, {}); for (const auto &pathToRemoteDiscover : foldersToRemoveFromBlacklist) { folder->journalDb()->schedulePathForRemoteDiscovery(pathToRemoteDiscover); } FolderMan::instance()->scheduleFolder(folder); } void AccountSettings::folderTerminateSyncAndUpdateBlackList(const QStringList &blackList, OCC::Folder *folder, const QStringList &foldersToRemoveFromBlacklist) { if (_folderConnections.contains(folder->alias())) { qCWarning(lcAccountSettings) << "Folder " << folder->alias() << "is already terminating the sync."; return; } // in case sync is already running - terminate it and start a new one const QMetaObject::Connection syncTerminatedConnection = connect(folder, &Folder::syncFinished, this, [this, blackList, folder, foldersToRemoveFromBlacklist]() { const auto foundConnectionIt = _folderConnections.find(folder->alias()); if (foundConnectionIt != _folderConnections.end()) { disconnect(*foundConnectionIt); _folderConnections.erase(foundConnectionIt); } updateBlackListAndScheduleFolderSync(blackList, folder, foldersToRemoveFromBlacklist); }); _folderConnections.insert(folder->alias(), syncTerminatedConnection); folder->slotTerminateSync(); } void AccountSettings::refreshSelectiveSyncStatus() { QString unsyncedFoldersString; QString becameBigFoldersString; const auto folders = FolderMan::instance()->map().values(); static const auto folderSeparatorString = QStringLiteral(", "); static const auto folderLinkString = [](const QString &slashlessFolderPath, const QString &folderName) { return QStringLiteral("%1").arg(slashlessFolderPath, folderName); }; static const auto appendFolderDisplayString = [](QString &foldersString, const QString &folderDisplayString) { if (!foldersString.isEmpty()) { foldersString += folderSeparatorString; } foldersString += folderDisplayString; }; _ui->bigFolderUi->setVisible(false); for (const auto folder : folders) { if (folder->accountState() != _accountState) { continue; } auto ok = false; auto blacklistOk = false; const auto undecidedList = folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncUndecidedList, &ok); auto blacklist = folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &blacklistOk); blacklist.sort(); for (const auto &it : undecidedList) { // FIXME: add the folder alias in a hoover hint. // folder->alias() + QLatin1String("/") const auto folderTrailingSlash = Utility::trailingSlashPath(it); const auto folderWithoutTrailingSlash = it.endsWith('/') ? it.left(it.length() - 1) : it; const auto escapedFolderString = Utility::escape(folderWithoutTrailingSlash); const auto escapedFolderName = Utility::escape(folder->alias()); const auto folderIdx = _model->indexForPath(folder, folderWithoutTrailingSlash); // If we do not know the index yet then do not provide a link string const auto folderDisplayString = folderIdx.isValid() ? folderLinkString(escapedFolderString, escapedFolderName) : folderWithoutTrailingSlash; // The new big folder procedure automatically places these new big folders in the blacklist. // This is not the case for existing folders discovered to have gone beyond the limit. // So we need to check if the folder is in the blacklist or not and tweak the message accordingly. if (SyncJournalDb::findPathInSelectiveSyncList(blacklist, folderTrailingSlash)) { appendFolderDisplayString(unsyncedFoldersString, folderDisplayString); } else { appendFolderDisplayString(becameBigFoldersString, folderDisplayString); } } } ConfigFile cfg; QString infoString; if (!unsyncedFoldersString.isEmpty()) { infoString += !cfg.confirmExternalStorage() ? tr("There are folders that were not synchronized because they are too big: ") : !cfg.newBigFolderSizeLimit().first ? tr("There are folders that were not synchronized because they are external storages: ") : tr("There are folders that were not synchronized because they are too big or external storages: "); infoString += unsyncedFoldersString; } if (!becameBigFoldersString.isEmpty()) { if (!infoString.isEmpty()) { infoString += QStringLiteral("\n"); } const auto folderSizeLimitString = QString::number(cfg.newBigFolderSizeLimit().second); infoString += tr("There are folders that have grown in size beyond %1MB: %2").arg(folderSizeLimitString, becameBigFoldersString); } _ui->selectiveSyncNotification->setText(infoString); _ui->bigFolderUi->setVisible(!infoString.isEmpty()); } bool AccountSettings::event(QEvent *e) { if (e->type() == QEvent::Hide || e->type() == QEvent::Show) { _userInfo.setActive(isVisible()); } if (e->type() == QEvent::Show) { // Expand the folder automatically only if there's only one, see #4283 // The 2 is 1 folder + 1 'add folder' button if (_model->rowCount() <= 2) { _ui->_folderList->setExpanded(_model->index(0, 0), true); } } return QWidget::event(e); } void AccountSettings::slotStyleChanged() { customizeStyle(); // Notify the other widgets (Dark-/Light-Mode switching) emit styleChanged(); } void AccountSettings::customizeStyle() { auto msg = _ui->connectLabel->text(); Theme::replaceLinkColorStringBackgroundAware(msg); _ui->connectLabel->setText(msg); const auto color = palette().highlight().color(); _ui->quotaProgressBar->setStyleSheet(QString::fromLatin1(progressBarStyleC).arg(color.name())); } void AccountSettings::initializeE2eEncryption() { connect(_accountState->account()->e2e(), &ClientSideEncryption::initializationFinished, this, &AccountSettings::slotPossiblyUnblacklistE2EeFoldersAndRestartSync); if (!_accountState->account()->e2e()->_mnemonic.isEmpty()) { slotE2eEncryptionMnemonicReady(); } else { initializeE2eEncryptionSettingsMessage(); connect(_accountState->account()->e2e(), &ClientSideEncryption::initializationFinished, this, [this] { if (!_accountState->account()->e2e()->_publicKey.isNull()) { _ui->encryptionMessage->setText(tr("End-to-end encryption has been enabled on this account with another device." "
" "It can be enabled on this device by entering your mnemonic." "
" "This will enable synchronisation of existing encrypted folders.")); } }); _accountState->account()->setE2eEncryptionKeysGenerationAllowed(false); _accountState->account()->e2e()->initialize(_accountState->account()); } } void AccountSettings::resetE2eEncryption() { for (const auto action : _ui->encryptionMessage->actions()) { _ui->encryptionMessage->removeAction(action); } _ui->encryptionMessage->setText({}); _ui->encryptionMessage->setIcon({}); initializeE2eEncryptionSettingsMessage(); checkClientSideEncryptionState(); const auto account = _accountState->account(); if (account->e2e()->_mnemonic.isEmpty()) { FolderMan::instance()->removeE2eFiles(account); } } void AccountSettings::removeActionFromEncryptionMessage(const QString &actionId) { const auto foundEnableEncryptionActionIt = std::find_if(std::cbegin(_ui->encryptionMessage->actions()), std::cend(_ui->encryptionMessage->actions()), [&actionId](const QAction *action) { return action->property(e2eUiActionIdKey).toString() == actionId; }); if (foundEnableEncryptionActionIt != std::cend(_ui->encryptionMessage->actions())) { _ui->encryptionMessage->removeAction(*foundEnableEncryptionActionIt); (*foundEnableEncryptionActionIt)->deleteLater(); } } QAction *AccountSettings::addActionToEncryptionMessage(const QString &actionTitle, const QString &actionId) { const auto encryptionActions = _ui->encryptionMessage->actions(); for (const auto &action : encryptionActions) { if (action->property(e2eUiActionIdKey) == actionId) { return action; } } auto *const action = new QAction(actionTitle, this); if (!actionId.isEmpty()) { action->setProperty(e2eUiActionIdKey, actionId); } _ui->encryptionMessage->addAction(action); return action; } void AccountSettings::initializeE2eEncryptionSettingsMessage() { _ui->encryptionMessage->setMessageType(KMessageWidget::Information); _ui->encryptionMessage->setText(tr("This account supports end-to-end encryption")); _ui->encryptionMessage->setIcon(Theme::createColorAwareIcon(QStringLiteral(":/client/theme/black/state-info.svg"))); _ui->encryptionMessage->hide(); auto *const actionEnableE2e = addActionToEncryptionMessage(tr("Set up encryption"), e2EeUiActionEnableEncryptionId); connect(actionEnableE2e, &QAction::triggered, this, &AccountSettings::slotE2eEncryptionGenerateKeys); } } // namespace OCC #include "accountsettings.moc"