1 /*
2  * Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful, but
10  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12  * for more details.
13  */
14 
15 
16 #include "accountsettings.h"
17 #include "common/syncjournalfilerecord.h"
18 #include "qmessagebox.h"
19 #include "ui_accountsettings.h"
20 
21 #include "theme.h"
22 #include "foldercreationdialog.h"
23 #include "folderman.h"
24 #include "folderwizard.h"
25 #include "folderstatusmodel.h"
26 #include "folderstatusdelegate.h"
27 #include "common/utility.h"
28 #include "guiutility.h"
29 #include "application.h"
30 #include "configfile.h"
31 #include "account.h"
32 #include "accountstate.h"
33 #include "userinfo.h"
34 #include "accountmanager.h"
35 #include "owncloudsetupwizard.h"
36 #include "creds/abstractcredentials.h"
37 #include "creds/httpcredentialsgui.h"
38 #include "tooltipupdater.h"
39 #include "filesystem.h"
40 #include "encryptfolderjob.h"
41 #include "syncresult.h"
42 #include "ignorelisttablewidget.h"
43 #include "wizard/owncloudwizard.h"
44 
45 #include <cmath>
46 
47 #include <QDesktopServices>
48 #include <QDialogButtonBox>
49 #include <QDir>
50 #include <QListWidgetItem>
51 #include <QMessageBox>
52 #include <QAction>
53 #include <QVBoxLayout>
54 #include <QTreeView>
55 #include <QKeySequence>
56 #include <QIcon>
57 #include <QVariant>
58 #include <QJsonDocument>
59 #include <QToolTip>
60 
61 #include "account.h"
62 
63 namespace {
64 constexpr auto propertyFolder = "folder";
65 constexpr auto propertyPath = "path";
66 }
67 
68 namespace OCC {
69 
70 class AccountSettings;
71 
72 Q_LOGGING_CATEGORY(lcAccountSettings, "nextcloud.gui.account.settings", QtInfoMsg)
73 
74 static const char progressBarStyleC[] =
75     "QProgressBar {"
76     "border: 1px solid grey;"
77     "border-radius: 5px;"
78     "text-align: center;"
79     "}"
80     "QProgressBar::chunk {"
81     "background-color: %1; width: 1px;"
82     "}";
83 
showEnableE2eeWithVirtualFilesWarningDialog(std::function<void (void)> onAccept)84 void showEnableE2eeWithVirtualFilesWarningDialog(std::function<void(void)> onAccept)
85 {
86     const auto messageBox = new QMessageBox;
87     messageBox->setAttribute(Qt::WA_DeleteOnClose);
88     messageBox->setText(AccountSettings::tr("End-to-End Encryption with Virtual Files"));
89     messageBox->setInformativeText(AccountSettings::tr("You seem to have the Virtual Files feature enabled on this folder. "
90                                                        "At the moment, it is not possible to implicitly download virtual files that are "
91                                                        "End-to-End encrypted. To get the best experience with Virtual Files and "
92                                                        "End-to-End Encryption, make sure the encrypted folder is marked with "
93                                                        "\"Make always available locally\"."));
94     messageBox->setIcon(QMessageBox::Warning);
95     const auto dontEncryptButton = messageBox->addButton(QMessageBox::StandardButton::Cancel);
96     Q_ASSERT(dontEncryptButton);
97     dontEncryptButton->setText(AccountSettings::tr("Don't encrypt folder"));
98     const auto encryptButton = messageBox->addButton(QMessageBox::StandardButton::Ok);
99     Q_ASSERT(encryptButton);
100     encryptButton->setText(AccountSettings::tr("Encrypt folder"));
101     QObject::connect(messageBox, &QMessageBox::accepted, onAccept);
102 
103     messageBox->open();
104 }
105 
106 /**
107  * Adjusts the mouse cursor based on the region it is on over the folder tree view.
108  *
109  * Used to show that one can click the red error list box by changing the cursor
110  * to the pointing hand.
111  */
112 class MouseCursorChanger : public QObject
113 {
114     Q_OBJECT
115 public:
MouseCursorChanger(QObject * parent)116     MouseCursorChanger(QObject *parent)
117         : QObject(parent)
118     {
119     }
120 
121     QTreeView *folderList;
122     FolderStatusModel *model;
123 
124 protected:
eventFilter(QObject * watched,QEvent * event)125     bool eventFilter(QObject *watched, QEvent *event) override
126     {
127         if (event->type() == QEvent::HoverMove) {
128             Qt::CursorShape shape = Qt::ArrowCursor;
129             auto pos = folderList->mapFromGlobal(QCursor::pos());
130             auto index = folderList->indexAt(pos);
131             if (model->classify(index) == FolderStatusModel::RootFolder
132                 && (FolderStatusDelegate::errorsListRect(folderList->visualRect(index)).contains(pos)
133                     || FolderStatusDelegate::optionsButtonRect(folderList->visualRect(index),folderList->layoutDirection()).contains(pos))) {
134                 shape = Qt::PointingHandCursor;
135             }
136             folderList->setCursor(shape);
137         }
138         return QObject::eventFilter(watched, event);
139     }
140 };
141 
AccountSettings(AccountState * accountState,QWidget * parent)142 AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent)
143     : QWidget(parent)
144     , _ui(new Ui::AccountSettings)
145     , _wasDisabledBefore(false)
146     , _accountState(accountState)
147     , _userInfo(accountState, false, true)
148     , _menuShown(false)
149 {
150     _ui->setupUi(this);
151 
152     _model = new FolderStatusModel;
153     _model->setAccountState(_accountState);
154     _model->setParent(this);
155     auto *delegate = new FolderStatusDelegate;
156     delegate->setParent(this);
157 
158     // Connect styleChanged events to our widgets, so they can adapt (Dark-/Light-Mode switching)
159     connect(this, &AccountSettings::styleChanged, delegate, &FolderStatusDelegate::slotStyleChanged);
160 
161     _ui->_folderList->header()->hide();
162     _ui->_folderList->setItemDelegate(delegate);
163     _ui->_folderList->setModel(_model);
164 #if defined(Q_OS_MAC)
165     _ui->_folderList->setMinimumWidth(400);
166 #else
167     _ui->_folderList->setMinimumWidth(300);
168 #endif
169     new ToolTipUpdater(_ui->_folderList);
170 
171     auto mouseCursorChanger = new MouseCursorChanger(this);
172     mouseCursorChanger->folderList = _ui->_folderList;
173     mouseCursorChanger->model = _model;
174     _ui->_folderList->setMouseTracking(true);
175     _ui->_folderList->setAttribute(Qt::WA_Hover, true);
176     _ui->_folderList->installEventFilter(mouseCursorChanger);
177 
178     connect(this, &AccountSettings::removeAccountFolders,
179             AccountManager::instance(), &AccountManager::removeAccountFolders);
180     connect(_ui->_folderList, &QWidget::customContextMenuRequested,
181         this, &AccountSettings::slotCustomContextMenuRequested);
182     connect(_ui->_folderList, &QAbstractItemView::clicked,
183         this, &AccountSettings::slotFolderListClicked);
184     connect(_ui->_folderList, &QTreeView::expanded, this, &AccountSettings::refreshSelectiveSyncStatus);
185     connect(_ui->_folderList, &QTreeView::collapsed, this, &AccountSettings::refreshSelectiveSyncStatus);
186     connect(_ui->selectiveSyncNotification, &QLabel::linkActivated,
187         this, &AccountSettings::slotLinkActivated);
188     connect(_model, &FolderStatusModel::suggestExpand, _ui->_folderList, &QTreeView::expand);
189     connect(_model, &FolderStatusModel::dirtyChanged, this, &AccountSettings::refreshSelectiveSyncStatus);
190     refreshSelectiveSyncStatus();
191     connect(_model, &QAbstractItemModel::rowsInserted,
192         this, &AccountSettings::refreshSelectiveSyncStatus);
193 
194     auto *syncNowAction = new QAction(this);
195     syncNowAction->setShortcut(QKeySequence(Qt::Key_F6));
196     connect(syncNowAction, &QAction::triggered, this, &AccountSettings::slotScheduleCurrentFolder);
197     addAction(syncNowAction);
198 
199     auto *syncNowWithRemoteDiscovery = new QAction(this);
200     syncNowWithRemoteDiscovery->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_F6));
201     connect(syncNowWithRemoteDiscovery, &QAction::triggered, this, &AccountSettings::slotScheduleCurrentFolderForceRemoteDiscovery);
202     addAction(syncNowWithRemoteDiscovery);
203 
204 
205     slotHideSelectiveSyncWidget();
206     _ui->bigFolderUi->setVisible(false);
207     connect(_model, &QAbstractItemModel::dataChanged, this, &AccountSettings::slotSelectiveSyncChanged);
208     connect(_ui->selectiveSyncApply, &QAbstractButton::clicked, this, &AccountSettings::slotHideSelectiveSyncWidget);
209     connect(_ui->selectiveSyncCancel, &QAbstractButton::clicked, this, &AccountSettings::slotHideSelectiveSyncWidget);
210 
211     connect(_ui->selectiveSyncApply, &QAbstractButton::clicked, _model, &FolderStatusModel::slotApplySelectiveSync);
212     connect(_ui->selectiveSyncCancel, &QAbstractButton::clicked, _model, &FolderStatusModel::resetFolders);
213     connect(_ui->bigFolderApply, &QAbstractButton::clicked, _model, &FolderStatusModel::slotApplySelectiveSync);
214     connect(_ui->bigFolderSyncAll, &QAbstractButton::clicked, _model, &FolderStatusModel::slotSyncAllPendingBigFolders);
215     connect(_ui->bigFolderSyncNone, &QAbstractButton::clicked, _model, &FolderStatusModel::slotSyncNoPendingBigFolders);
216 
217     connect(FolderMan::instance(), &FolderMan::folderListChanged, _model, &FolderStatusModel::resetFolders);
218     connect(this, &AccountSettings::folderChanged, _model, &FolderStatusModel::resetFolders);
219 
220 
221     // quotaProgressBar style now set in customizeStyle()
222     /*QColor color = palette().highlight().color();
223      _ui->quotaProgressBar->setStyleSheet(QString::fromLatin1(progressBarStyleC).arg(color.name()));*/
224 
225     // Connect E2E stuff
226     connect(this, &AccountSettings::requestMnemonic, _accountState->account()->e2e(), &ClientSideEncryption::slotRequestMnemonic);
227     connect(_accountState->account()->e2e(), &ClientSideEncryption::showMnemonic, this, &AccountSettings::slotShowMnemonic);
228 
229     connect(_accountState->account()->e2e(), &ClientSideEncryption::mnemonicGenerated, this, &AccountSettings::slotNewMnemonicGenerated);
230     if (_accountState->account()->e2e()->newMnemonicGenerated()) {
231         slotNewMnemonicGenerated();
232     } else {
233         _ui->encryptionMessage->setText(tr("This account supports end-to-end encryption"));
234 
235         auto *mnemonic = new QAction(tr("Display mnemonic"), this);
236         connect(mnemonic, &QAction::triggered, this, &AccountSettings::requestMnemonic);
237         _ui->encryptionMessage->addAction(mnemonic);
238         _ui->encryptionMessage->hide();
239     }
240 
241     _ui->connectLabel->setText(tr("No account configured."));
242 
243     connect(_accountState, &AccountState::stateChanged, this, &AccountSettings::slotAccountStateChanged);
244     slotAccountStateChanged();
245 
246     connect(&_userInfo, &UserInfo::quotaUpdated,
247         this, &AccountSettings::slotUpdateQuota);
248 
249     customizeStyle();
250 }
251 
slotNewMnemonicGenerated()252 void AccountSettings::slotNewMnemonicGenerated()
253 {
254     _ui->encryptionMessage->setText(tr("This account supports end-to-end encryption"));
255 
256     auto *mnemonic = new QAction(tr("Enable encryption"), this);
257     connect(mnemonic, &QAction::triggered, this, &AccountSettings::requestMnemonic);
258     connect(mnemonic, &QAction::triggered, _ui->encryptionMessage, &KMessageWidget::hide);
259 
260     _ui->encryptionMessage->addAction(mnemonic);
261     _ui->encryptionMessage->show();
262 }
263 
slotEncryptFolderFinished(int status)264 void AccountSettings::slotEncryptFolderFinished(int status)
265 {
266     qCInfo(lcAccountSettings) << "Current folder encryption status code:" << status;
267     auto job = qobject_cast<EncryptFolderJob*>(sender());
268     Q_ASSERT(job);
269     if (!job->errorString().isEmpty()) {
270         QMessageBox::warning(nullptr, tr("Warning"), job->errorString());
271     }
272 
273     const auto folder = job->property(propertyFolder).value<Folder *>();
274     Q_ASSERT(folder);
275     const auto path = job->property(propertyPath).value<QString>();
276     const auto index = _model->indexForPath(folder, path);
277     Q_ASSERT(index.isValid());
278     _model->resetAndFetch(index.parent());
279 
280     job->deleteLater();
281 }
282 
selectedFolderAlias() const283 QString AccountSettings::selectedFolderAlias() const
284 {
285     QModelIndex selected = _ui->_folderList->selectionModel()->currentIndex();
286     if (!selected.isValid())
287         return "";
288     return _model->data(selected, FolderStatusDelegate::FolderAliasRole).toString();
289 }
290 
slotToggleSignInState()291 void AccountSettings::slotToggleSignInState()
292 {
293     if (_accountState->isSignedOut()) {
294         _accountState->account()->resetRejectedCertificates();
295         _accountState->signIn();
296     } else {
297         _accountState->signOutByUi();
298     }
299 }
300 
doExpand()301 void AccountSettings::doExpand()
302 {
303     // Make sure at least the root items are expanded
304     for (int i = 0; i < _model->rowCount(); ++i) {
305         auto idx = _model->index(i);
306         if (!_ui->_folderList->isExpanded(idx))
307             _ui->_folderList->setExpanded(idx, true);
308     }
309 }
310 
slotShowMnemonic(const QString & mnemonic)311 void AccountSettings::slotShowMnemonic(const QString &mnemonic)
312 {
313     AccountManager::instance()->displayMnemonic(mnemonic);
314 }
315 
canEncryptOrDecrypt(const FolderStatusModel::SubFolderInfo * info)316 bool AccountSettings::canEncryptOrDecrypt (const FolderStatusModel::SubFolderInfo* info) {
317     if (info->_folder->syncResult().status() != SyncResult::Status::Success) {
318         QMessageBox msgBox;
319         msgBox.setText("Please wait for the folder to sync before trying to encrypt it.");
320         msgBox.exec();
321         return false;
322     }
323 
324     // for some reason the actual folder in disk is info->_folder->path + info->_path.
325     QDir folderPath(info->_folder->path() + info->_path);
326     folderPath.setFilter( QDir::AllEntries | QDir::NoDotAndDotDot );
327 
328     if (folderPath.count() != 0) {
329         QMessageBox msgBox;
330         msgBox.setText(tr("You cannot encrypt a folder with contents, please remove the files.\n"
331                        "Wait for the new sync, then encrypt it."));
332         msgBox.exec();
333         return false;
334     }
335     return true;
336 }
337 
slotMarkSubfolderEncrypted(FolderStatusModel::SubFolderInfo * folderInfo)338 void AccountSettings::slotMarkSubfolderEncrypted(FolderStatusModel::SubFolderInfo* folderInfo)
339 {
340     if (!canEncryptOrDecrypt(folderInfo)) {
341         return;
342     }
343 
344     const auto folder = folderInfo->_folder;
345     Q_ASSERT(folder);
346 
347     const auto folderAlias = folder->alias();
348     const auto path = folderInfo->_path;
349     const auto fileId = folderInfo->_fileId;
350     const auto encryptFolder = [this, fileId, path, folderAlias] {
351         const auto folder = FolderMan::instance()->folder(folderAlias);
352         if (!folder) {
353             qCWarning(lcAccountSettings) << "Could not encrypt folder because folder" << folderAlias << "does not exist anymore";
354             QMessageBox::warning(nullptr, tr("Encryption failed"), tr("Could not encrypt folder because the folder does not exist anymore"));
355             return;
356         }
357 
358         // Folder info have directory paths in Foo/Bar/ convention...
359         Q_ASSERT(!path.startsWith('/') && path.endsWith('/'));
360         // But EncryptFolderJob expects directory path Foo/Bar convention
361         const auto choppedPath = path.chopped(1);
362         auto job = new OCC::EncryptFolderJob(accountsState()->account(), folder->journalDb(), choppedPath, fileId, this);
363         job->setProperty(propertyFolder, QVariant::fromValue(folder));
364         job->setProperty(propertyPath, QVariant::fromValue(path));
365         connect(job, &OCC::EncryptFolderJob::finished, this, &AccountSettings::slotEncryptFolderFinished);
366         job->start();
367     };
368 
369     if (folder->virtualFilesEnabled()
370         && folder->vfs().mode() == Vfs::WindowsCfApi) {
371         showEnableE2eeWithVirtualFilesWarningDialog(encryptFolder);
372         return;
373     }
374     encryptFolder();
375 }
376 
slotEditCurrentIgnoredFiles()377 void AccountSettings::slotEditCurrentIgnoredFiles()
378 {
379     Folder *f = FolderMan::instance()->folder(selectedFolderAlias());
380     if (!f)
381         return;
382     openIgnoredFilesDialog(f->path());
383 }
384 
slotOpenMakeFolderDialog()385 void AccountSettings::slotOpenMakeFolderDialog()
386 {
387     const auto &selected = _ui->_folderList->selectionModel()->currentIndex();
388 
389     if (!selected.isValid()) {
390         qCWarning(lcAccountSettings) << "Selection model current folder index is not valid.";
391         return;
392     }
393 
394     const auto &classification = _model->classify(selected);
395 
396     if (classification != FolderStatusModel::SubFolder && classification != FolderStatusModel::RootFolder) {
397         return;
398     }
399 
400     const QString fileName = [this, &selected, &classification] {
401         QString result;
402         if (classification == FolderStatusModel::RootFolder) {
403             const auto alias = _model->data(selected, FolderStatusDelegate::FolderAliasRole).toString();
404             if (const auto folder = FolderMan::instance()->folder(alias)) {
405                 result = folder->path();
406             }
407         } else {
408             result = _model->data(selected, FolderStatusDelegate::FolderPathRole).toString();
409         }
410 
411         if (result.endsWith('/')) {
412             result.chop(1);
413         }
414 
415         return result;
416     }();
417 
418     if (!fileName.isEmpty()) {
419         const auto folderCreationDialog = new FolderCreationDialog(fileName, this);
420         folderCreationDialog->setAttribute(Qt::WA_DeleteOnClose);
421         folderCreationDialog->open();
422     }
423 }
424 
slotEditCurrentLocalIgnoredFiles()425 void AccountSettings::slotEditCurrentLocalIgnoredFiles()
426 {
427     QModelIndex selected = _ui->_folderList->selectionModel()->currentIndex();
428     if (!selected.isValid() || _model->classify(selected) != FolderStatusModel::SubFolder)
429         return;
430     QString fileName = _model->data(selected, FolderStatusDelegate::FolderPathRole).toString();
431     openIgnoredFilesDialog(fileName);
432 }
433 
openIgnoredFilesDialog(const QString & absFolderPath)434 void AccountSettings::openIgnoredFilesDialog(const QString & absFolderPath)
435 {
436     Q_ASSERT(QFileInfo(absFolderPath).isAbsolute());
437 
438     const QString ignoreFile = absFolderPath + ".sync-exclude.lst";
439     auto layout = new QVBoxLayout();
440     auto ignoreListWidget = new IgnoreListTableWidget(this);
441     ignoreListWidget->readIgnoreFile(ignoreFile);
442     layout->addWidget(ignoreListWidget);
443 
444     auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
445     layout->addWidget(buttonBox);
446 
447     auto dialog = new QDialog();
448     dialog->setLayout(layout);
449 
450     connect(buttonBox, &QDialogButtonBox::clicked, [=](QAbstractButton * button) {
451         if (buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole)
452             ignoreListWidget->slotWriteIgnoreFile(ignoreFile);
453         dialog->close();
454     });
455     connect(buttonBox, &QDialogButtonBox::rejected,
456             dialog,    &QDialog::close);
457 
458     dialog->open();
459 }
460 
slotSubfolderContextMenuRequested(const QModelIndex & index,const QPoint & pos)461 void AccountSettings::slotSubfolderContextMenuRequested(const QModelIndex& index, const QPoint& pos)
462 {
463     Q_UNUSED(pos);
464 
465     QMenu menu;
466     auto ac = menu.addAction(tr("Open folder"));
467     connect(ac, &QAction::triggered, this, &AccountSettings::slotOpenCurrentLocalSubFolder);
468 
469     auto fileName = _model->data(index, FolderStatusDelegate::FolderPathRole).toString();
470     if (!QFile::exists(fileName)) {
471         ac->setEnabled(false);
472     }
473     auto info   = _model->infoForIndex(index);
474     auto acc = _accountState->account();
475 
476     if (acc->capabilities().clientSideEncryptionAvailable()) {
477         // Verify if the folder is empty before attempting to encrypt.
478 
479         bool isEncrypted = info->_isEncrypted;
480         bool isParentEncrypted = _model->isAnyAncestorEncrypted(index);
481 
482         if (!isEncrypted && !isParentEncrypted) {
483             ac = menu.addAction(tr("Encrypt"));
484             connect(ac, &QAction::triggered, [this, info] { slotMarkSubfolderEncrypted(info); });
485         } else {
486             // Ingore decrypting for now since it only works with an empty folder
487             // connect(ac, &QAction::triggered, [this, &info] { slotMarkSubfolderDecrypted(info); });
488         }
489     }
490 
491     ac = menu.addAction(tr("Edit Ignored Files"));
492     connect(ac, &QAction::triggered, this, &AccountSettings::slotEditCurrentLocalIgnoredFiles);
493 
494     ac = menu.addAction(tr("Create new folder"));
495     connect(ac, &QAction::triggered, this, &AccountSettings::slotOpenMakeFolderDialog);
496     ac->setEnabled(QFile::exists(fileName));
497 
498     const auto folder = info->_folder;
499     if (folder && folder->virtualFilesEnabled()) {
500         auto availabilityMenu = menu.addMenu(tr("Availability"));
501 
502         // Has '/' suffix convention for paths here but VFS and
503         // sync engine expects no such suffix
504         Q_ASSERT(info->_path.endsWith('/'));
505         const auto remotePath = info->_path.chopped(1);
506 
507         // It might be an E2EE mangled path, so let's try to demangle it
508         const auto journal = folder->journalDb();
509         SyncJournalFileRecord rec;
510         journal->getFileRecordByE2eMangledName(remotePath, &rec);
511 
512         const auto path = rec.isValid() ? rec._path : remotePath;
513 
514         ac = availabilityMenu->addAction(Utility::vfsPinActionText());
515         connect(ac, &QAction::triggered, this, [this, folder, path] { slotSetSubFolderAvailability(folder, path, PinState::AlwaysLocal); });
516 
517         ac = availabilityMenu->addAction(Utility::vfsFreeSpaceActionText());
518         connect(ac, &QAction::triggered, this, [this, folder, path] { slotSetSubFolderAvailability(folder, path, PinState::OnlineOnly); });
519     }
520 
521     menu.exec(QCursor::pos());
522 }
523 
slotCustomContextMenuRequested(const QPoint & pos)524 void AccountSettings::slotCustomContextMenuRequested(const QPoint &pos)
525 {
526     QTreeView *tv = _ui->_folderList;
527     QModelIndex index = tv->indexAt(pos);
528     if (!index.isValid()) {
529         return;
530     }
531 
532     if (_model->classify(index) == FolderStatusModel::SubFolder) {
533         slotSubfolderContextMenuRequested(index, pos);
534         return;
535     }
536 
537     if (_model->classify(index) != FolderStatusModel::RootFolder) {
538         return;
539     }
540 
541     tv->setCurrentIndex(index);
542     QString alias = _model->data(index, FolderStatusDelegate::FolderAliasRole).toString();
543     bool folderPaused = _model->data(index, FolderStatusDelegate::FolderSyncPaused).toBool();
544     bool folderConnected = _model->data(index, FolderStatusDelegate::FolderAccountConnected).toBool();
545     auto folderMan = FolderMan::instance();
546     QPointer<Folder> folder = folderMan->folder(alias);
547     if (!folder)
548         return;
549 
550     auto *menu = new QMenu(tv);
551 
552     menu->setAttribute(Qt::WA_DeleteOnClose);
553 
554     QAction *ac = menu->addAction(tr("Open folder"));
555     connect(ac, &QAction::triggered, this, &AccountSettings::slotOpenCurrentFolder);
556 
557     ac = menu->addAction(tr("Edit Ignored Files"));
558     connect(ac, &QAction::triggered, this, &AccountSettings::slotEditCurrentIgnoredFiles);
559 
560     ac = menu->addAction(tr("Create new folder"));
561     connect(ac, &QAction::triggered, this, &AccountSettings::slotOpenMakeFolderDialog);
562     ac->setEnabled(QFile::exists(folder->path()));
563 
564     if (!_ui->_folderList->isExpanded(index) && folder->supportsSelectiveSync()) {
565         ac = menu->addAction(tr("Choose what to sync"));
566         ac->setEnabled(folderConnected);
567         connect(ac, &QAction::triggered, this, &AccountSettings::doExpand);
568     }
569 
570     if (!folderPaused) {
571         ac = menu->addAction(tr("Force sync now"));
572         if (folder && folder->isSyncRunning()) {
573             ac->setText(tr("Restart sync"));
574         }
575         ac->setEnabled(folderConnected);
576         connect(ac, &QAction::triggered, this, &AccountSettings::slotForceSyncCurrentFolder);
577     }
578 
579     ac = menu->addAction(folderPaused ? tr("Resume sync") : tr("Pause sync"));
580     connect(ac, &QAction::triggered, this, &AccountSettings::slotEnableCurrentFolder);
581 
582     ac = menu->addAction(tr("Remove folder sync connection"));
583     connect(ac, &QAction::triggered, this, &AccountSettings::slotRemoveCurrentFolder);
584 
585     if (folder->virtualFilesEnabled()) {
586         auto availabilityMenu = menu->addMenu(tr("Availability"));
587 
588         ac = availabilityMenu->addAction(Utility::vfsPinActionText());
589         connect(ac, &QAction::triggered, this, [this]() { slotSetCurrentFolderAvailability(PinState::AlwaysLocal); });
590         ac->setDisabled(Theme::instance()->enforceVirtualFilesSyncFolder());
591 
592         ac = availabilityMenu->addAction(Utility::vfsFreeSpaceActionText());
593         connect(ac, &QAction::triggered, this, [this]() { slotSetCurrentFolderAvailability(PinState::OnlineOnly); });
594 
595         ac = menu->addAction(tr("Disable virtual file support …"));
596         connect(ac, &QAction::triggered, this, &AccountSettings::slotDisableVfsCurrentFolder);
597         ac->setDisabled(Theme::instance()->enforceVirtualFilesSyncFolder());
598     }
599 
600     if (Theme::instance()->showVirtualFilesOption()
601         && !folder->virtualFilesEnabled() && Vfs::checkAvailability(folder->path())) {
602         const auto mode = bestAvailableVfsMode();
603         if (mode == Vfs::WindowsCfApi || ConfigFile().showExperimentalOptions()) {
604             ac = menu->addAction(tr("Enable virtual file support %1 …").arg(mode == Vfs::WindowsCfApi ? QString() : tr("(experimental)")));
605             // TODO: remove when UX decision is made
606             ac->setEnabled(!Utility::isPathWindowsDrivePartitionRoot(folder->path()));
607             //
608             connect(ac, &QAction::triggered, this, &AccountSettings::slotEnableVfsCurrentFolder);
609         }
610     }
611 
612 
613     menu->popup(tv->mapToGlobal(pos));
614 }
615 
slotFolderListClicked(const QModelIndex & indx)616 void AccountSettings::slotFolderListClicked(const QModelIndex &indx)
617 {
618     if (indx.data(FolderStatusDelegate::AddButton).toBool()) {
619         // "Add Folder Sync Connection"
620         QTreeView *tv = _ui->_folderList;
621         auto pos = tv->mapFromGlobal(QCursor::pos());
622         QStyleOptionViewItem opt;
623         opt.initFrom(tv);
624         auto btnRect = tv->visualRect(indx);
625         auto btnSize = tv->itemDelegate(indx)->sizeHint(opt, indx);
626         auto actual = QStyle::visualRect(opt.direction, btnRect, QRect(btnRect.topLeft(), btnSize));
627         if (!actual.contains(pos))
628             return;
629 
630         if (indx.flags() & Qt::ItemIsEnabled) {
631             slotAddFolder();
632         } else {
633             QToolTip::showText(
634                 QCursor::pos(),
635                 _model->data(indx, Qt::ToolTipRole).toString(),
636                 this);
637         }
638         return;
639     }
640     if (_model->classify(indx) == FolderStatusModel::RootFolder) {
641         // tries to find if we clicked on the '...' button.
642         QTreeView *tv = _ui->_folderList;
643         auto pos = tv->mapFromGlobal(QCursor::pos());
644         if (FolderStatusDelegate::optionsButtonRect(tv->visualRect(indx), layoutDirection()).contains(pos)) {
645             slotCustomContextMenuRequested(pos);
646             return;
647         }
648         if (FolderStatusDelegate::errorsListRect(tv->visualRect(indx)).contains(pos)) {
649             emit showIssuesList(_accountState);
650             return;
651         }
652 
653         // Expand root items on single click
654         if (_accountState && _accountState->state() == AccountState::Connected) {
655             bool expanded = !(_ui->_folderList->isExpanded(indx));
656             _ui->_folderList->setExpanded(indx, expanded);
657         }
658     }
659 }
660 
slotAddFolder()661 void AccountSettings::slotAddFolder()
662 {
663     FolderMan *folderMan = FolderMan::instance();
664     folderMan->setSyncEnabled(false); // do not start more syncs.
665 
666     auto *folderWizard = new FolderWizard(_accountState->account(), this);
667     folderWizard->setAttribute(Qt::WA_DeleteOnClose);
668 
669     connect(folderWizard, &QDialog::accepted, this, &AccountSettings::slotFolderWizardAccepted);
670     connect(folderWizard, &QDialog::rejected, this, &AccountSettings::slotFolderWizardRejected);
671     folderWizard->open();
672 }
673 
674 
slotFolderWizardAccepted()675 void AccountSettings::slotFolderWizardAccepted()
676 {
677     auto *folderWizard = qobject_cast<FolderWizard *>(sender());
678     FolderMan *folderMan = FolderMan::instance();
679 
680     qCInfo(lcAccountSettings) << "Folder wizard completed";
681 
682     FolderDefinition definition;
683     definition.localPath = FolderDefinition::prepareLocalPath(
684         folderWizard->field(QLatin1String("sourceFolder")).toString());
685     definition.targetPath = FolderDefinition::prepareTargetPath(
686         folderWizard->property("targetPath").toString());
687 
688     if (folderWizard->property("useVirtualFiles").toBool()) {
689         definition.virtualFilesMode = bestAvailableVfsMode();
690     }
691 
692     {
693         QDir dir(definition.localPath);
694         if (!dir.exists()) {
695             qCInfo(lcAccountSettings) << "Creating folder" << definition.localPath;
696             if (!dir.mkpath(".")) {
697                 QMessageBox::warning(this, tr("Folder creation failed"),
698                     tr("<p>Could not create local folder <i>%1</i>.</p>")
699                         .arg(QDir::toNativeSeparators(definition.localPath)));
700                 return;
701             }
702         }
703         FileSystem::setFolderMinimumPermissions(definition.localPath);
704         Utility::setupFavLink(definition.localPath);
705     }
706 
707     /* take the value from the definition of already existing folders. All folders have
708      * the same setting so far.
709      * The default is to sync hidden files
710      */
711     definition.ignoreHiddenFiles = folderMan->ignoreHiddenFiles();
712 
713     if (folderMan->navigationPaneHelper().showInExplorerNavigationPane())
714         definition.navigationPaneClsid = QUuid::createUuid();
715 
716     auto selectiveSyncBlackList = folderWizard->property("selectiveSyncBlackList").toStringList();
717 
718     folderMan->setSyncEnabled(true);
719 
720     Folder *f = folderMan->addFolder(_accountState, definition);
721     if (f) {
722         if (definition.virtualFilesMode != Vfs::Off && folderWizard->property("useVirtualFiles").toBool())
723             f->setRootPinState(PinState::OnlineOnly);
724 
725         f->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, selectiveSyncBlackList);
726 
727         // The user already accepted the selective sync dialog. everything is in the white list
728         f->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncWhiteList,
729             QStringList() << QLatin1String("/"));
730         folderMan->scheduleAllFolders();
731         emit folderChanged();
732     }
733 }
734 
slotFolderWizardRejected()735 void AccountSettings::slotFolderWizardRejected()
736 {
737     qCInfo(lcAccountSettings) << "Folder wizard cancelled";
738     FolderMan *folderMan = FolderMan::instance();
739     folderMan->setSyncEnabled(true);
740 }
741 
slotRemoveCurrentFolder()742 void AccountSettings::slotRemoveCurrentFolder()
743 {
744     auto folder = FolderMan::instance()->folder(selectedFolderAlias());
745     QModelIndex selected = _ui->_folderList->selectionModel()->currentIndex();
746     if (selected.isValid() && folder) {
747         int row = selected.row();
748 
749         qCInfo(lcAccountSettings) << "Remove Folder alias " << folder->alias();
750         QString shortGuiLocalPath = folder->shortGuiLocalPath();
751 
752         auto messageBox = new QMessageBox(QMessageBox::Question,
753             tr("Confirm Folder Sync Connection Removal"),
754             tr("<p>Do you really want to stop syncing the folder <i>%1</i>?</p>"
755                "<p><b>Note:</b> This will <b>not</b> delete any files.</p>")
756                 .arg(shortGuiLocalPath),
757             QMessageBox::NoButton,
758             this);
759         messageBox->setAttribute(Qt::WA_DeleteOnClose);
760         QPushButton *yesButton =
761             messageBox->addButton(tr("Remove Folder Sync Connection"), QMessageBox::YesRole);
762         messageBox->addButton(tr("Cancel"), QMessageBox::NoRole);
763         connect(messageBox, &QMessageBox::finished, this, [messageBox, yesButton, folder, row, this]{
764             if (messageBox->clickedButton() == yesButton) {
765                 Utility::removeFavLink(folder->path());
766                 FolderMan::instance()->removeFolder(folder);
767                 _model->removeRow(row);
768 
769                 // single folder fix to show add-button and hide remove-button
770                 emit folderChanged();
771             }
772         });
773         messageBox->open();
774     }
775 }
776 
slotOpenCurrentFolder()777 void AccountSettings::slotOpenCurrentFolder()
778 {
779     auto alias = selectedFolderAlias();
780     if (!alias.isEmpty()) {
781         emit openFolderAlias(alias);
782     }
783 }
784 
slotOpenCurrentLocalSubFolder()785 void AccountSettings::slotOpenCurrentLocalSubFolder()
786 {
787     QModelIndex selected = _ui->_folderList->selectionModel()->currentIndex();
788     if (!selected.isValid() || _model->classify(selected) != FolderStatusModel::SubFolder)
789         return;
790     QString fileName = _model->data(selected, FolderStatusDelegate::FolderPathRole).toString();
791     QUrl url = QUrl::fromLocalFile(fileName);
792     QDesktopServices::openUrl(url);
793 }
794 
slotEnableVfsCurrentFolder()795 void AccountSettings::slotEnableVfsCurrentFolder()
796 {
797     FolderMan *folderMan = FolderMan::instance();
798     QPointer<Folder> folder = folderMan->folder(selectedFolderAlias());
799     QModelIndex selected = _ui->_folderList->selectionModel()->currentIndex();
800     if (!selected.isValid() || !folder)
801         return;
802 
803     OwncloudWizard::askExperimentalVirtualFilesFeature(this, [folder, this](bool enable) {
804         if (!enable || !folder)
805             return;
806 
807         // we might need to add or remove the panel entry as cfapi brings this feature out of the box
808         FolderMan::instance()->navigationPaneHelper().scheduleUpdateCloudStorageRegistry();
809 
810         // It is unsafe to switch on vfs while a sync is running - wait if necessary.
811         auto connection = std::make_shared<QMetaObject::Connection>();
812         auto switchVfsOn = [folder, connection, this]() {
813             if (*connection)
814                 QObject::disconnect(*connection);
815 
816             qCInfo(lcAccountSettings) << "Enabling vfs support for folder" << folder->path();
817 
818             // Wipe selective sync blacklist
819             bool ok = false;
820             const auto oldBlacklist = folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &ok);
821             folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, {});
822 
823             // Change the folder vfs mode and load the plugin
824             folder->setVirtualFilesEnabled(true);
825             folder->setVfsOnOffSwitchPending(false);
826 
827             // Setting to Unspecified retains existing data.
828             // Selective sync excluded folders become OnlineOnly.
829             folder->setRootPinState(PinState::Unspecified);
830             for (const auto &entry : oldBlacklist) {
831                 folder->journalDb()->schedulePathForRemoteDiscovery(entry);
832                 if (!folder->vfs().setPinState(entry, PinState::OnlineOnly)) {
833                     qCWarning(lcAccountSettings) << "Could not set pin state of" << entry << "to online only";
834                 }
835             }
836             folder->slotNextSyncFullLocalDiscovery();
837 
838             FolderMan::instance()->scheduleFolder(folder);
839 
840             _ui->_folderList->doItemsLayout();
841             _ui->selectiveSyncStatus->setVisible(false);
842         };
843 
844         if (folder->isSyncRunning()) {
845             *connection = connect(folder, &Folder::syncFinished, this, switchVfsOn);
846             folder->setVfsOnOffSwitchPending(true);
847             folder->slotTerminateSync();
848             _ui->_folderList->doItemsLayout();
849         } else {
850             switchVfsOn();
851         }
852     });
853 }
854 
slotDisableVfsCurrentFolder()855 void AccountSettings::slotDisableVfsCurrentFolder()
856 {
857     FolderMan *folderMan = FolderMan::instance();
858     QPointer<Folder> folder = folderMan->folder(selectedFolderAlias());
859     QModelIndex selected = _ui->_folderList->selectionModel()->currentIndex();
860     if (!selected.isValid() || !folder)
861         return;
862 
863     auto msgBox = new QMessageBox(
864         QMessageBox::Question,
865         tr("Disable virtual file support?"),
866         tr("This action will disable virtual file support. As a consequence contents of folders that "
867            "are currently marked as \"available online only\" will be downloaded."
868            "\n\n"
869            "The only advantage of disabling virtual file support is that the selective sync feature "
870            "will become available again."
871            "\n\n"
872            "This action will abort any currently running synchronization."));
873     auto acceptButton = msgBox->addButton(tr("Disable support"), QMessageBox::AcceptRole);
874     msgBox->addButton(tr("Cancel"), QMessageBox::RejectRole);
875     connect(msgBox, &QMessageBox::finished, msgBox, [this, msgBox, folder, acceptButton] {
876         msgBox->deleteLater();
877         if (msgBox->clickedButton() != acceptButton|| !folder)
878             return;
879 
880         // we might need to add or remove the panel entry as cfapi brings this feature out of the box
881         FolderMan::instance()->navigationPaneHelper().scheduleUpdateCloudStorageRegistry();
882 
883         // It is unsafe to switch off vfs while a sync is running - wait if necessary.
884         auto connection = std::make_shared<QMetaObject::Connection>();
885         auto switchVfsOff = [folder, connection, this]() {
886             if (*connection)
887                 QObject::disconnect(*connection);
888 
889             qCInfo(lcAccountSettings) << "Disabling vfs support for folder" << folder->path();
890 
891             // Also wipes virtual files, schedules remote discovery
892             folder->setVirtualFilesEnabled(false);
893             folder->setVfsOnOffSwitchPending(false);
894 
895             // Wipe pin states and selective sync db
896             folder->setRootPinState(PinState::AlwaysLocal);
897             folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, {});
898 
899             // Prevent issues with missing local files
900             folder->slotNextSyncFullLocalDiscovery();
901 
902             FolderMan::instance()->scheduleFolder(folder);
903 
904             _ui->_folderList->doItemsLayout();
905         };
906 
907         if (folder->isSyncRunning()) {
908             *connection = connect(folder, &Folder::syncFinished, this, switchVfsOff);
909             folder->setVfsOnOffSwitchPending(true);
910             folder->slotTerminateSync();
911             _ui->_folderList->doItemsLayout();
912         } else {
913             switchVfsOff();
914         }
915     });
916     msgBox->open();
917 }
918 
slotSetCurrentFolderAvailability(PinState state)919 void AccountSettings::slotSetCurrentFolderAvailability(PinState state)
920 {
921     ASSERT(state == PinState::OnlineOnly || state == PinState::AlwaysLocal);
922 
923     FolderMan *folderMan = FolderMan::instance();
924     QPointer<Folder> folder = folderMan->folder(selectedFolderAlias());
925     QModelIndex selected = _ui->_folderList->selectionModel()->currentIndex();
926     if (!selected.isValid() || !folder)
927         return;
928 
929     // similar to socket api: sets pin state recursively and sync
930     folder->setRootPinState(state);
931     folder->scheduleThisFolderSoon();
932 }
933 
slotSetSubFolderAvailability(Folder * folder,const QString & path,PinState state)934 void AccountSettings::slotSetSubFolderAvailability(Folder *folder, const QString &path, PinState state)
935 {
936     Q_ASSERT(folder && folder->virtualFilesEnabled());
937     Q_ASSERT(!path.endsWith('/'));
938 
939     // Update the pin state on all items
940     if (!folder->vfs().setPinState(path, state)) {
941         qCWarning(lcAccountSettings) << "Could not set pin state of" << path << "to" << state;
942     }
943 
944     // Trigger sync
945     folder->schedulePathForLocalDiscovery(path);
946     folder->scheduleThisFolderSoon();
947 }
948 
showConnectionLabel(const QString & message,QStringList errors)949 void AccountSettings::showConnectionLabel(const QString &message, QStringList errors)
950 {
951     const QString errStyle = QLatin1String("color:#ffffff; background-color:#bb4d4d;padding:5px;"
952                                            "border-width: 1px; border-style: solid; border-color: #aaaaaa;"
953                                            "border-radius:5px;");
954     if (errors.isEmpty()) {
955         QString msg = message;
956         Theme::replaceLinkColorStringBackgroundAware(msg);
957         _ui->connectLabel->setText(msg);
958         _ui->connectLabel->setToolTip(QString());
959         _ui->connectLabel->setStyleSheet(QString());
960     } else {
961         errors.prepend(message);
962         QString msg = errors.join(QLatin1String("\n"));
963         qCDebug(lcAccountSettings) << msg;
964         Theme::replaceLinkColorString(msg, QColor("#c1c8e6"));
965         _ui->connectLabel->setText(msg);
966         _ui->connectLabel->setToolTip(QString());
967         _ui->connectLabel->setStyleSheet(errStyle);
968     }
969     _ui->accountStatus->setVisible(!message.isEmpty());
970 }
971 
slotEnableCurrentFolder(bool terminate)972 void AccountSettings::slotEnableCurrentFolder(bool terminate)
973 {
974     auto alias = selectedFolderAlias();
975 
976     if (!alias.isEmpty()) {
977         FolderMan *folderMan = FolderMan::instance();
978 
979         qCInfo(lcAccountSettings) << "Application: enable folder with alias " << alias;
980         bool currentlyPaused = false;
981 
982         // this sets the folder status to disabled but does not interrupt it.
983         Folder *f = folderMan->folder(alias);
984         if (!f) {
985             return;
986         }
987         currentlyPaused = f->syncPaused();
988         if (!currentlyPaused && !terminate) {
989             // check if a sync is still running and if so, ask if we should terminate.
990             if (f->isBusy()) { // its still running
991                 auto msgbox = new QMessageBox(QMessageBox::Question, tr("Sync Running"),
992                     tr("The syncing operation is running.<br/>Do you want to terminate it?"),
993                     QMessageBox::Yes | QMessageBox::No, this);
994                 msgbox->setAttribute(Qt::WA_DeleteOnClose);
995                 msgbox->setDefaultButton(QMessageBox::Yes);
996                 connect(msgbox, &QMessageBox::accepted, this, [this]{
997                     slotEnableCurrentFolder(true);
998                 });
999                 msgbox->open();
1000                 return;
1001             }
1002         }
1003 
1004         // message box can return at any time while the thread keeps running,
1005         // so better check again after the user has responded.
1006         if (f->isBusy() && terminate) {
1007             f->slotTerminateSync();
1008         }
1009         f->setSyncPaused(!currentlyPaused);
1010 
1011         // keep state for the icon setting.
1012         if (currentlyPaused)
1013             _wasDisabledBefore = true;
1014 
1015         _model->slotUpdateFolderState(f);
1016     }
1017 }
1018 
slotScheduleCurrentFolder()1019 void AccountSettings::slotScheduleCurrentFolder()
1020 {
1021     FolderMan *folderMan = FolderMan::instance();
1022     if (auto folder = folderMan->folder(selectedFolderAlias())) {
1023         folderMan->scheduleFolder(folder);
1024     }
1025 }
1026 
slotScheduleCurrentFolderForceRemoteDiscovery()1027 void AccountSettings::slotScheduleCurrentFolderForceRemoteDiscovery()
1028 {
1029     FolderMan *folderMan = FolderMan::instance();
1030     if (auto folder = folderMan->folder(selectedFolderAlias())) {
1031         folder->slotWipeErrorBlacklist();
1032         folder->journalDb()->forceRemoteDiscoveryNextSync();
1033         folderMan->scheduleFolder(folder);
1034     }
1035 }
1036 
slotForceSyncCurrentFolder()1037 void AccountSettings::slotForceSyncCurrentFolder()
1038 {
1039     FolderMan *folderMan = FolderMan::instance();
1040     if (auto selectedFolder = folderMan->folder(selectedFolderAlias())) {
1041         // Terminate and reschedule any running sync
1042         for (auto f : folderMan->map()) {
1043             if (f->isSyncRunning()) {
1044                 f->slotTerminateSync();
1045                 folderMan->scheduleFolder(f);
1046             }
1047         }
1048 
1049         selectedFolder->slotWipeErrorBlacklist(); // issue #6757
1050 
1051         // Insert the selected folder at the front of the queue
1052         folderMan->scheduleFolderNext(selectedFolder);
1053     }
1054 }
1055 
slotOpenOC()1056 void AccountSettings::slotOpenOC()
1057 {
1058     if (_OCUrl.isValid()) {
1059         Utility::openBrowser(_OCUrl);
1060     }
1061 }
1062 
slotUpdateQuota(qint64 total,qint64 used)1063 void AccountSettings::slotUpdateQuota(qint64 total, qint64 used)
1064 {
1065     if (total > 0) {
1066         _ui->quotaProgressBar->setVisible(true);
1067         _ui->quotaProgressBar->setEnabled(true);
1068         // workaround the label only accepting ints (which may be only 32 bit wide)
1069         const double percent = used / (double)total * 100;
1070         const int percentInt = qMin(qRound(percent), 100);
1071         _ui->quotaProgressBar->setValue(percentInt);
1072         QString usedStr = Utility::octetsToString(used);
1073         QString totalStr = Utility::octetsToString(total);
1074         QString percentStr = Utility::compactFormatDouble(percent, 1);
1075         QString toolTip = tr("%1 (%3%) of %2 in use. Some folders, including network mounted or shared folders, might have different limits.").arg(usedStr, totalStr, percentStr);
1076         _ui->quotaInfoLabel->setText(tr("%1 of %2 in use").arg(usedStr, totalStr));
1077         _ui->quotaInfoLabel->setToolTip(toolTip);
1078         _ui->quotaProgressBar->setToolTip(toolTip);
1079     } else {
1080         _ui->quotaProgressBar->setVisible(false);
1081         _ui->quotaInfoLabel->setToolTip(QString());
1082 
1083         /* -1 means not computed; -2 means unknown; -3 means unlimited  (#owncloud/client/issues/3940)*/
1084         if (total == 0 || total == -1) {
1085             _ui->quotaInfoLabel->setText(tr("Currently there is no storage usage information available."));
1086         } else {
1087             QString usedStr = Utility::octetsToString(used);
1088             _ui->quotaInfoLabel->setText(tr("%1 in use").arg(usedStr));
1089         }
1090     }
1091 }
1092 
slotAccountStateChanged()1093 void AccountSettings::slotAccountStateChanged()
1094 {
1095     const AccountState::State state = _accountState ? _accountState->state() : AccountState::Disconnected;
1096     if (state != AccountState::Disconnected) {
1097         _ui->sslButton->updateAccountState(_accountState);
1098         AccountPtr account = _accountState->account();
1099         QUrl safeUrl(account->url());
1100         safeUrl.setPassword(QString()); // Remove the password from the URL to avoid showing it in the UI
1101         const auto folders = FolderMan::instance()->map().values();
1102         for (Folder *folder : folders) {
1103             _model->slotUpdateFolderState(folder);
1104         }
1105 
1106         const QString server = QString::fromLatin1("<a href=\"%1\">%2</a>")
1107                                    .arg(Utility::escape(account->url().toString()),
1108                                        Utility::escape(safeUrl.toString()));
1109         QString serverWithUser = server;
1110         if (AbstractCredentials *cred = account->credentials()) {
1111             QString user = account->davDisplayName();
1112             if (user.isEmpty()) {
1113                 user = cred->user();
1114             }
1115             serverWithUser = tr("%1 as %2").arg(server, Utility::escape(user));
1116         }
1117 
1118         switch (state) {
1119         case AccountState::Connected: {
1120             QStringList errors;
1121             if (account->serverVersionUnsupported()) {
1122                 errors << tr("The server version %1 is unsupported! Proceed at your own risk.").arg(account->serverVersion());
1123             }
1124             showConnectionLabel(tr("Connected to %1.").arg(serverWithUser), errors);
1125             break;
1126         }
1127         case AccountState::ServiceUnavailable:
1128             showConnectionLabel(tr("Server %1 is temporarily unavailable.").arg(server));
1129             break;
1130         case AccountState::MaintenanceMode:
1131             showConnectionLabel(tr("Server %1 is currently in maintenance mode.").arg(server));
1132             break;
1133         case AccountState::SignedOut:
1134             showConnectionLabel(tr("Signed out from %1.").arg(serverWithUser));
1135             break;
1136         case AccountState::AskingCredentials: {
1137             QUrl url;
1138             if (auto cred = qobject_cast<HttpCredentialsGui *>(account->credentials())) {
1139                 connect(cred, &HttpCredentialsGui::authorisationLinkChanged,
1140                     this, &AccountSettings::slotAccountStateChanged, Qt::UniqueConnection);
1141                 url = cred->authorisationLink();
1142             }
1143             if (url.isValid()) {
1144                 showConnectionLabel(tr("Obtaining authorization from the browser. "
1145                                        "<a href='%1'>Click here</a> to re-open the browser.")
1146                                         .arg(url.toString(QUrl::FullyEncoded)));
1147             } else {
1148                 showConnectionLabel(tr("Connecting to %1 …").arg(serverWithUser));
1149             }
1150             break;
1151         }
1152         case AccountState::NetworkError:
1153             showConnectionLabel(tr("No connection to %1 at %2.")
1154                                     .arg(Utility::escape(Theme::instance()->appNameGUI()), server),
1155                 _accountState->connectionErrors());
1156             break;
1157         case AccountState::ConfigurationError:
1158             showConnectionLabel(tr("Server configuration error: %1 at %2.")
1159                                     .arg(Utility::escape(Theme::instance()->appNameGUI()), server),
1160                 _accountState->connectionErrors());
1161             break;
1162         case AccountState::Disconnected:
1163             // we can't end up here as the whole block is ifdeffed
1164             Q_UNREACHABLE();
1165             break;
1166         }
1167     } else {
1168         // ownCloud is not yet configured.
1169         showConnectionLabel(tr("No %1 connection configured.")
1170                                 .arg(Utility::escape(Theme::instance()->appNameGUI())));
1171     }
1172 
1173     /* Allow to expand the item if the account is connected. */
1174     _ui->_folderList->setItemsExpandable(state == AccountState::Connected);
1175 
1176     if (state != AccountState::Connected) {
1177         /* check if there are expanded root items, if so, close them */
1178         int i = 0;
1179         for (i = 0; i < _model->rowCount(); ++i) {
1180             if (_ui->_folderList->isExpanded(_model->index(i)))
1181                 _ui->_folderList->setExpanded(_model->index(i), false);
1182         }
1183     } else if (_model->isDirty()) {
1184         // If we connect and have pending changes, show the list.
1185         doExpand();
1186     }
1187 
1188     // Disabling expansion of folders might require hiding the selective
1189     // sync user interface buttons.
1190     refreshSelectiveSyncStatus();
1191 
1192     if (state == AccountState::State::Connected) {
1193         /* TODO: We should probably do something better here.
1194          * Verify if the user has a private key already uploaded to the server,
1195          * if it has, do not offer to create one.
1196          */
1197         qCInfo(lcAccountSettings) << "Account" << accountsState()->account()->displayName()
1198             << "Client Side Encryption" << accountsState()->account()->capabilities().clientSideEncryptionAvailable();
1199 
1200         if (_accountState->account()->capabilities().clientSideEncryptionAvailable()) {
1201             _ui->encryptionMessage->show();
1202         }
1203     }
1204 }
1205 
slotLinkActivated(const QString & link)1206 void AccountSettings::slotLinkActivated(const QString &link)
1207 {
1208     // Parse folder alias and filename from the link, calculate the index
1209     // and select it if it exists.
1210     const QStringList li = link.split(QLatin1String("?folder="));
1211     if (li.count() > 1) {
1212         QString myFolder = li[0];
1213         const QString alias = li[1];
1214         if (myFolder.endsWith(QLatin1Char('/')))
1215             myFolder.chop(1);
1216 
1217         // Make sure the folder itself is expanded
1218         Folder *f = FolderMan::instance()->folder(alias);
1219         QModelIndex folderIndx = _model->indexForPath(f, QString());
1220         if (!_ui->_folderList->isExpanded(folderIndx)) {
1221             _ui->_folderList->setExpanded(folderIndx, true);
1222         }
1223 
1224         QModelIndex indx = _model->indexForPath(f, myFolder);
1225         if (indx.isValid()) {
1226             // make sure all the parents are expanded
1227             for (auto i = indx.parent(); i.isValid(); i = i.parent()) {
1228                 if (!_ui->_folderList->isExpanded(i)) {
1229                     _ui->_folderList->setExpanded(i, true);
1230                 }
1231             }
1232             _ui->_folderList->setSelectionMode(QAbstractItemView::SingleSelection);
1233             _ui->_folderList->setCurrentIndex(indx);
1234             _ui->_folderList->scrollTo(indx);
1235         } else {
1236             qCWarning(lcAccountSettings) << "Unable to find a valid index for " << myFolder;
1237         }
1238     }
1239 }
1240 
~AccountSettings()1241 AccountSettings::~AccountSettings()
1242 {
1243     delete _ui;
1244 }
1245 
slotHideSelectiveSyncWidget()1246 void AccountSettings::slotHideSelectiveSyncWidget()
1247 {
1248     _ui->selectiveSyncApply->setEnabled(false);
1249     _ui->selectiveSyncStatus->setVisible(false);
1250     _ui->selectiveSyncButtons->setVisible(false);
1251     _ui->selectiveSyncLabel->hide();
1252 }
1253 
slotSelectiveSyncChanged(const QModelIndex & topLeft,const QModelIndex & bottomRight,const QVector<int> & roles)1254 void AccountSettings::slotSelectiveSyncChanged(const QModelIndex &topLeft,
1255                                                const QModelIndex &bottomRight,
1256                                                const QVector<int> &roles)
1257 {
1258     Q_UNUSED(bottomRight);
1259     if (!roles.contains(Qt::CheckStateRole)) {
1260         return;
1261     }
1262 
1263     const auto info = _model->infoForIndex(topLeft);
1264     if (!info) {
1265         return;
1266     }
1267 
1268     const bool showWarning = _model->isDirty() && _accountState->isConnected() && info->_checked == Qt::Unchecked;
1269 
1270     // FIXME: the model is not precise enough to handle extra cases
1271     // e.g. the user clicked on the same checkbox 2x without applying the change in between.
1272     // We don't know which checkbox changed to be able to toggle the selectiveSyncLabel display.
1273     if (showWarning) {
1274         _ui->selectiveSyncLabel->show();
1275     }
1276 
1277     const bool shouldBeVisible = _model->isDirty();
1278     const bool wasVisible = _ui->selectiveSyncStatus->isVisible();
1279     if (shouldBeVisible) {
1280         _ui->selectiveSyncStatus->setVisible(true);
1281     }
1282 
1283     _ui->selectiveSyncApply->setEnabled(true);
1284     _ui->selectiveSyncButtons->setVisible(true);
1285 
1286     if (shouldBeVisible != wasVisible) {
1287         const auto hint = _ui->selectiveSyncStatus->sizeHint();
1288 
1289         if (shouldBeVisible) {
1290             _ui->selectiveSyncStatus->setMaximumHeight(0);
1291         }
1292 
1293         const auto anim = new QPropertyAnimation(_ui->selectiveSyncStatus, "maximumHeight", _ui->selectiveSyncStatus);
1294         anim->setEndValue(_model->isDirty() ? hint.height() : 0);
1295         anim->start(QAbstractAnimation::DeleteWhenStopped);
1296         connect(anim, &QPropertyAnimation::finished, [this, shouldBeVisible]() {
1297             _ui->selectiveSyncStatus->setMaximumHeight(QWIDGETSIZE_MAX);
1298             if (!shouldBeVisible) {
1299                 _ui->selectiveSyncStatus->hide();
1300             }
1301         });
1302     }
1303 }
1304 
refreshSelectiveSyncStatus()1305 void AccountSettings::refreshSelectiveSyncStatus()
1306 {
1307     QString msg;
1308     int cnt = 0;
1309     const auto folders = FolderMan::instance()->map().values();
1310     _ui->bigFolderUi->setVisible(false);
1311     for (Folder *folder : folders) {
1312         if (folder->accountState() != _accountState) {
1313             continue;
1314         }
1315 
1316         bool ok = false;
1317         const auto undecidedList = folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncUndecidedList, &ok);
1318         for (const auto &it : undecidedList) {
1319             // FIXME: add the folder alias in a hoover hint.
1320             // folder->alias() + QLatin1String("/")
1321             if (cnt++) {
1322                 msg += QLatin1String(", ");
1323             }
1324             QString myFolder = (it);
1325             if (myFolder.endsWith('/')) {
1326                 myFolder.chop(1);
1327             }
1328             QModelIndex theIndx = _model->indexForPath(folder, myFolder);
1329             if (theIndx.isValid()) {
1330                 msg += QString::fromLatin1("<a href=\"%1?folder=%2\">%1</a>")
1331                            .arg(Utility::escape(myFolder), Utility::escape(folder->alias()));
1332             } else {
1333                 msg += myFolder; // no link because we do not know the index yet.
1334             }
1335         }
1336     }
1337 
1338     if (!msg.isEmpty()) {
1339         ConfigFile cfg;
1340         QString info = !cfg.confirmExternalStorage()
1341                 ? tr("There are folders that were not synchronized because they are too big: ")
1342                 : !cfg.newBigFolderSizeLimit().first
1343                   ? tr("There are folders that were not synchronized because they are external storages: ")
1344                   : tr("There are folders that were not synchronized because they are too big or external storages: ");
1345 
1346         _ui->selectiveSyncNotification->setText(info + msg);
1347         _ui->bigFolderUi->setVisible(true);
1348     }
1349 }
1350 
slotDeleteAccount()1351 void AccountSettings::slotDeleteAccount()
1352 {
1353     // Deleting the account potentially deletes 'this', so
1354     // the QMessageBox should be destroyed before that happens.
1355     auto messageBox = new QMessageBox(QMessageBox::Question,
1356         tr("Confirm Account Removal"),
1357         tr("<p>Do you really want to remove the connection to the account <i>%1</i>?</p>"
1358            "<p><b>Note:</b> This will <b>not</b> delete any files.</p>")
1359             .arg(_accountState->account()->displayName()),
1360         QMessageBox::NoButton,
1361         this);
1362     auto yesButton = messageBox->addButton(tr("Remove connection"), QMessageBox::YesRole);
1363     messageBox->addButton(tr("Cancel"), QMessageBox::NoRole);
1364     messageBox->setAttribute(Qt::WA_DeleteOnClose);
1365     connect(messageBox, &QMessageBox::finished, this, [this, messageBox, yesButton]{
1366         if (messageBox->clickedButton() == yesButton) {
1367             // Else it might access during destruction. This should be better handled by it having a QSharedPointer
1368             _model->setAccountState(nullptr);
1369 
1370             auto manager = AccountManager::instance();
1371             manager->deleteAccount(_accountState);
1372             manager->save();
1373         }
1374     });
1375     messageBox->open();
1376 }
1377 
event(QEvent * e)1378 bool AccountSettings::event(QEvent *e)
1379 {
1380     if (e->type() == QEvent::Hide || e->type() == QEvent::Show) {
1381         _userInfo.setActive(isVisible());
1382     }
1383     if (e->type() == QEvent::Show) {
1384         // Expand the folder automatically only if there's only one, see #4283
1385         // The 2 is 1 folder + 1 'add folder' button
1386         if (_model->rowCount() <= 2) {
1387             _ui->_folderList->setExpanded(_model->index(0, 0), true);
1388         }
1389     }
1390     return QWidget::event(e);
1391 }
1392 
slotStyleChanged()1393 void AccountSettings::slotStyleChanged()
1394 {
1395     customizeStyle();
1396 
1397     // Notify the other widgets (Dark-/Light-Mode switching)
1398     emit styleChanged();
1399 }
1400 
customizeStyle()1401 void AccountSettings::customizeStyle()
1402 {
1403     QString msg = _ui->connectLabel->text();
1404     Theme::replaceLinkColorStringBackgroundAware(msg);
1405     _ui->connectLabel->setText(msg);
1406 
1407     QColor color = palette().highlight().color();
1408     _ui->quotaProgressBar->setStyleSheet(QString::fromLatin1(progressBarStyleC).arg(color.name()));
1409 }
1410 
1411 } // namespace OCC
1412 
1413 #include "accountsettings.moc"
1414