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