1 /*
2 * Copyright (C) by Klaas Freitag <freitag@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 #include <QtGui>
16 #include <QtWidgets>
17
18 #include "issueswidget.h"
19 #include "configfile.h"
20 #include "syncresult.h"
21 #include "syncengine.h"
22 #include "logger.h"
23 #include "theme.h"
24 #include "folderman.h"
25 #include "syncfileitem.h"
26 #include "folder.h"
27 #include "openfilemanager.h"
28 #include "activityitemdelegate.h"
29 #include "protocolwidget.h"
30 #include "accountstate.h"
31 #include "account.h"
32 #include "accountmanager.h"
33 #include "common/syncjournalfilerecord.h"
34 #include "elidedlabel.h"
35
36
37 #include "ui_issueswidget.h"
38
39 #include <climits>
40
41 namespace OCC {
42
43 /**
44 * If more issues are reported than this they will not show up
45 * to avoid performance issues around sorting this many issues.
46 */
47 static const int maxIssueCount = 50000;
48
pathsWithIssuesKey(const ProtocolItem::ExtraData & data)49 static QPair<QString, QString> pathsWithIssuesKey(const ProtocolItem::ExtraData &data)
50 {
51 return qMakePair(data.folderName, data.path);
52 }
53
IssuesWidget(QWidget * parent)54 IssuesWidget::IssuesWidget(QWidget *parent)
55 : QWidget(parent)
56 , _ui(new Ui::IssuesWidget)
57 {
58 _ui->setupUi(this);
59
60 connect(ProgressDispatcher::instance(), &ProgressDispatcher::progressInfo,
61 this, &IssuesWidget::slotProgressInfo);
62 connect(ProgressDispatcher::instance(), &ProgressDispatcher::itemCompleted,
63 this, &IssuesWidget::slotItemCompleted);
64 connect(ProgressDispatcher::instance(), &ProgressDispatcher::syncError,
65 this, &IssuesWidget::addError);
66
67 connect(_ui->_treeWidget, &QTreeWidget::itemActivated, this, &IssuesWidget::slotOpenFile);
68 connect(_ui->copyIssuesButton, &QAbstractButton::clicked, this, &IssuesWidget::copyToClipboard);
69
70 _ui->_treeWidget->setContextMenuPolicy(Qt::CustomContextMenu);
71 connect(_ui->_treeWidget, &QTreeWidget::customContextMenuRequested, this, &IssuesWidget::slotItemContextMenu);
72
73 connect(_ui->showIgnores, &QAbstractButton::toggled, this, &IssuesWidget::slotRefreshIssues);
74 connect(_ui->showWarnings, &QAbstractButton::toggled, this, &IssuesWidget::slotRefreshIssues);
75 connect(_ui->filterAccount, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &IssuesWidget::slotRefreshIssues);
76 connect(_ui->filterAccount, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &IssuesWidget::slotUpdateFolderFilters);
77 connect(_ui->filterFolder, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &IssuesWidget::slotRefreshIssues);
78 for (auto account : AccountManager::instance()->accounts()) {
79 slotAccountAdded(account.data());
80 }
81 connect(AccountManager::instance(), &AccountManager::accountAdded,
82 this, &IssuesWidget::slotAccountAdded);
83 connect(AccountManager::instance(), &AccountManager::accountRemoved,
84 this, &IssuesWidget::slotAccountRemoved);
85 connect(FolderMan::instance(), &FolderMan::folderListChanged,
86 this, &IssuesWidget::slotUpdateFolderFilters);
87
88
89 // Adjust copyToClipboard() when making changes here!
90 QStringList header;
91 header << tr("Time");
92 header << tr("File");
93 header << tr("Folder");
94 header << tr("Issue");
95
96 int timestampColumnExtra = 0;
97 #ifdef Q_OS_WIN
98 timestampColumnExtra = 20; // font metrics are broken on Windows, see #4721
99 #endif
100
101 _ui->_treeWidget->setHeaderLabels(header);
102 int timestampColumnWidth =
103 ActivityItemDelegate::rowHeight() // icon
104 + _ui->_treeWidget->fontMetrics().boundingRect(ProtocolItem::timeString(QDateTime::currentDateTime())).width()
105 + timestampColumnExtra;
106 _ui->_treeWidget->setColumnWidth(0, timestampColumnWidth);
107 _ui->_treeWidget->setColumnWidth(1, 180);
108 _ui->_treeWidget->setColumnCount(4);
109 _ui->_treeWidget->setRootIsDecorated(false);
110 _ui->_treeWidget->setTextElideMode(Qt::ElideMiddle);
111 _ui->_treeWidget->header()->setObjectName("ActivityErrorListHeader");
112 #if defined(Q_OS_MAC)
113 _ui->_treeWidget->setMinimumWidth(400);
114 #endif
115
116 _reenableSorting.setInterval(5000);
117 connect(&_reenableSorting, &QTimer::timeout, this,
118 [this]() { _ui->_treeWidget->setSortingEnabled(true); });
119
120 _ui->_tooManyIssuesWarning->hide();
121 connect(this, &IssuesWidget::issueCountUpdated, this,
122 [this](int count) { _ui->_tooManyIssuesWarning->setVisible(count >= maxIssueCount); });
123
124 _ui->_conflictHelp->hide();
125 _ui->_conflictHelp->setText(
126 tr("There were conflicts. <a href=\"%1\">Check the documentation on how to resolve them.</a>")
127 .arg(Theme::instance()->conflictHelpUrl()));
128 }
129
~IssuesWidget()130 IssuesWidget::~IssuesWidget()
131 {
132 delete _ui;
133 }
134
showEvent(QShowEvent * ev)135 void IssuesWidget::showEvent(QShowEvent *ev)
136 {
137 ConfigFile cfg;
138 cfg.restoreGeometryHeader(_ui->_treeWidget->header());
139
140 // Sorting by section was newly enabled. But if we restore the header
141 // from a state where sorting was disabled, both of these flags will be
142 // false and sorting will be impossible!
143 _ui->_treeWidget->header()->setSectionsClickable(true);
144 _ui->_treeWidget->header()->setSortIndicatorShown(true);
145
146 // Switch back to "first important, then by time" ordering
147 _ui->_treeWidget->sortByColumn(0, Qt::DescendingOrder);
148
149 QWidget::showEvent(ev);
150 }
151
hideEvent(QHideEvent * ev)152 void IssuesWidget::hideEvent(QHideEvent *ev)
153 {
154 ConfigFile cfg;
155 cfg.saveGeometryHeader(_ui->_treeWidget->header());
156 QWidget::hideEvent(ev);
157 }
158
persistsUntilLocalDiscovery(QTreeWidgetItem * item)159 static bool persistsUntilLocalDiscovery(QTreeWidgetItem *item)
160 {
161 const auto data = ProtocolItem::extraData(item);
162 return data.status == SyncFileItem::Conflict
163 || (data.status == SyncFileItem::FileIgnored && data.direction == SyncFileItem::Up);
164 }
165
cleanItems(const std::function<bool (QTreeWidgetItem *)> & shouldDelete)166 void IssuesWidget::cleanItems(const std::function<bool(QTreeWidgetItem *)> &shouldDelete)
167 {
168 _ui->_treeWidget->setSortingEnabled(false);
169
170 // The issue list is a state, clear it and let the next sync fill it
171 // with ignored files and propagation errors.
172 int itemCnt = _ui->_treeWidget->topLevelItemCount();
173 for (int cnt = itemCnt - 1; cnt >= 0; cnt--) {
174 QTreeWidgetItem *item = _ui->_treeWidget->topLevelItem(cnt);
175 if (shouldDelete(item)) {
176 _pathsWithIssues.remove(pathsWithIssuesKey(ProtocolItem::extraData(item)));
177 delete item;
178 }
179 }
180
181 _ui->_treeWidget->setSortingEnabled(true);
182
183 // update the tabtext
184 emit(issueCountUpdated(_ui->_treeWidget->topLevelItemCount()));
185 }
186
addItem(QTreeWidgetItem * item)187 void IssuesWidget::addItem(QTreeWidgetItem *item)
188 {
189 if (!item)
190 return;
191
192 int count = _ui->_treeWidget->topLevelItemCount();
193 if (count >= maxIssueCount) {
194 delete item;
195 return;
196 }
197
198 _ui->_treeWidget->setSortingEnabled(false);
199 _reenableSorting.start();
200
201 // Insert item specific errors behind the others
202 int insertLoc = 0;
203 if (!item->text(1).isEmpty()) {
204 for (int i = 0; i < count; ++i) {
205 if (_ui->_treeWidget->topLevelItem(i)->text(1).isEmpty()) {
206 insertLoc = i + 1;
207 } else {
208 break;
209 }
210 }
211 }
212
213 // Wipe any existing message for the same folder and path
214 auto newData = ProtocolItem::extraData(item);
215 if (_pathsWithIssues.contains(pathsWithIssuesKey(newData))) {
216 for (int i = 0; i < count; ++i) {
217 auto otherItem = _ui->_treeWidget->topLevelItem(i);
218 auto otherData = ProtocolItem::extraData(otherItem);
219 if (otherData.path == newData.path && otherData.folderName == newData.folderName) {
220 delete otherItem;
221 break;
222 }
223 }
224 }
225
226 _ui->_treeWidget->insertTopLevelItem(insertLoc, item);
227 _pathsWithIssues.insert(pathsWithIssuesKey(newData));
228 item->setHidden(!shouldBeVisible(item, currentAccountFilter(), currentFolderFilter()));
229 emit issueCountUpdated(_ui->_treeWidget->topLevelItemCount());
230 }
231
slotOpenFile(QTreeWidgetItem * item,int)232 void IssuesWidget::slotOpenFile(QTreeWidgetItem *item, int)
233 {
234 QString fileName = item->text(1);
235 if (Folder *folder = ProtocolItem::folder(item)) {
236 // folder->path() always comes back with trailing path
237 QString fullPath = folder->path() + fileName;
238 if (QFile(fullPath).exists()) {
239 showInFileManager(fullPath);
240 }
241 }
242 }
243
slotProgressInfo(const QString & folder,const ProgressInfo & progress)244 void IssuesWidget::slotProgressInfo(const QString &folder, const ProgressInfo &progress)
245 {
246 if (progress.status() == ProgressInfo::Reconcile) {
247 // Wipe all non-persistent entries - as well as the persistent ones
248 // in cases where a local discovery was done.
249 auto f = FolderMan::instance()->folder(folder);
250 if (!f)
251 return;
252 const auto &engine = f->syncEngine();
253 const auto style = engine.lastLocalDiscoveryStyle();
254 cleanItems([&](QTreeWidgetItem *item) {
255 if (ProtocolItem::extraData(item).folderName != folder)
256 return false;
257 if (style == LocalDiscoveryStyle::FilesystemOnly)
258 return true;
259 if (!persistsUntilLocalDiscovery(item))
260 return true;
261
262 // Definitely wipe the entry if the file no longer exists
263 if (!QFileInfo(f->path() + ProtocolItem::extraData(item).path).exists())
264 return true;
265
266 auto path = QFileInfo(ProtocolItem::extraData(item).path).dir().path();
267 if (path == ".")
268 path.clear();
269
270 return engine.shouldDiscoverLocally(path);
271 });
272 }
273 if (progress.status() == ProgressInfo::Done) {
274 // We keep track very well of pending conflicts.
275 // Inform other components about them.
276 QStringList conflicts;
277 auto tree = _ui->_treeWidget;
278 for (int i = 0; i < tree->topLevelItemCount(); ++i) {
279 auto item = tree->topLevelItem(i);
280 auto data = ProtocolItem::extraData(item);
281 if (data.folderName == folder
282 && data.status == SyncFileItem::Conflict) {
283 conflicts.append(data.path);
284 }
285 }
286 emit ProgressDispatcher::instance()->folderConflicts(folder, conflicts);
287
288 _ui->_conflictHelp->setHidden(Theme::instance()->conflictHelpUrl().isEmpty() || conflicts.isEmpty());
289 }
290 }
291
slotItemCompleted(const QString & folder,const SyncFileItemPtr & item)292 void IssuesWidget::slotItemCompleted(const QString &folder, const SyncFileItemPtr &item)
293 {
294 if (!item->showInIssuesTab())
295 return;
296 QTreeWidgetItem *line = ProtocolItem::create(folder, *item);
297 if (!line)
298 return;
299 addItem(line);
300 }
301
slotRefreshIssues()302 void IssuesWidget::slotRefreshIssues()
303 {
304 auto tree = _ui->_treeWidget;
305 auto filterFolderAlias = currentFolderFilter();
306 auto filterAccount = currentAccountFilter();
307
308 for (int i = 0; i < tree->topLevelItemCount(); ++i) {
309 auto item = tree->topLevelItem(i);
310 item->setHidden(!shouldBeVisible(item, filterAccount, filterFolderAlias));
311 }
312
313 _ui->_treeWidget->setColumnHidden(2, !filterFolderAlias.isEmpty());
314 }
315
slotAccountAdded(AccountState * account)316 void IssuesWidget::slotAccountAdded(AccountState *account)
317 {
318 _ui->filterAccount->addItem(account->account()->displayName(), QVariant::fromValue(account));
319 updateAccountChoiceVisibility();
320 }
321
slotAccountRemoved(AccountState * account)322 void IssuesWidget::slotAccountRemoved(AccountState *account)
323 {
324 for (int i = _ui->filterAccount->count() - 1; i >= 0; --i) {
325 if (account == _ui->filterAccount->itemData(i).value<AccountState *>())
326 _ui->filterAccount->removeItem(i);
327 }
328 updateAccountChoiceVisibility();
329 }
330
slotItemContextMenu(const QPoint & pos)331 void IssuesWidget::slotItemContextMenu(const QPoint &pos)
332 {
333 auto item = _ui->_treeWidget->itemAt(pos);
334 if (!item)
335 return;
336 auto globalPos = _ui->_treeWidget->viewport()->mapToGlobal(pos);
337 ProtocolItem::openContextMenu(globalPos, item, this);
338 }
339
updateAccountChoiceVisibility()340 void IssuesWidget::updateAccountChoiceVisibility()
341 {
342 bool visible = _ui->filterAccount->count() > 2;
343 _ui->filterAccount->setVisible(visible);
344 _ui->accountLabel->setVisible(visible);
345 slotUpdateFolderFilters();
346 }
347
currentAccountFilter() const348 AccountState *IssuesWidget::currentAccountFilter() const
349 {
350 return _ui->filterAccount->currentData().value<AccountState *>();
351 }
352
currentFolderFilter() const353 QString IssuesWidget::currentFolderFilter() const
354 {
355 return _ui->filterFolder->currentData().toString();
356 }
357
shouldBeVisible(QTreeWidgetItem * item,AccountState * filterAccount,const QString & filterFolderAlias) const358 bool IssuesWidget::shouldBeVisible(QTreeWidgetItem *item, AccountState *filterAccount,
359 const QString &filterFolderAlias) const
360 {
361 bool visible = true;
362 auto data = ProtocolItem::extraData(item);
363 auto status = data.status;
364 visible &= (_ui->showIgnores->isChecked() || status != SyncFileItem::FileIgnored);
365 visible &= (_ui->showWarnings->isChecked()
366 || (status != SyncFileItem::SoftError
367 && status != SyncFileItem::Restoration));
368
369 const auto &folderalias = data.folderName;
370 if (filterAccount) {
371 auto folder = FolderMan::instance()->folder(folderalias);
372 visible &= folder && folder->accountState() == filterAccount;
373 }
374 visible &= (filterFolderAlias.isEmpty() || filterFolderAlias == folderalias);
375
376 return visible;
377 }
378
slotUpdateFolderFilters()379 void IssuesWidget::slotUpdateFolderFilters()
380 {
381 auto account = _ui->filterAccount->currentData().value<AccountState *>();
382
383 // If there is no account selector, show folders for the single
384 // available account
385 if (_ui->filterAccount->isHidden() && _ui->filterAccount->count() > 1) {
386 account = _ui->filterAccount->itemData(1).value<AccountState *>();
387 }
388
389 if (!account) {
390 _ui->filterFolder->setCurrentIndex(0);
391 }
392 _ui->filterFolder->setEnabled(account != nullptr);
393
394 for (int i = _ui->filterFolder->count() - 1; i >= 1; --i) {
395 _ui->filterFolder->removeItem(i);
396 }
397
398 // Find all selectable folders while figuring out if we need a folder
399 // selector in the first place
400 bool anyAccountHasMultipleFolders = false;
401 QSet<AccountState *> accountsWithFolders;
402 for (auto folder : FolderMan::instance()->map().values()) {
403 if (accountsWithFolders.contains(folder->accountState()))
404 anyAccountHasMultipleFolders = true;
405 accountsWithFolders.insert(folder->accountState());
406
407 if (folder->accountState() != account)
408 continue;
409 _ui->filterFolder->addItem(folder->shortGuiLocalPath(), folder->alias());
410 }
411
412 // If we don't need the combo box, hide it.
413 _ui->filterFolder->setVisible(anyAccountHasMultipleFolders);
414 _ui->folderLabel->setVisible(anyAccountHasMultipleFolders);
415
416 // If there's no choice, select the only folder and disable
417 if (_ui->filterFolder->count() == 2 && anyAccountHasMultipleFolders) {
418 _ui->filterFolder->setCurrentIndex(1);
419 _ui->filterFolder->setEnabled(false);
420 }
421 }
422
storeSyncIssues(QTextStream & ts)423 void IssuesWidget::storeSyncIssues(QTextStream &ts)
424 {
425 int topLevelItems = _ui->_treeWidget->topLevelItemCount();
426
427 for (int i = 0; i < topLevelItems; i++) {
428 QTreeWidgetItem *child = _ui->_treeWidget->topLevelItem(i);
429 if (child->isHidden())
430 continue;
431 ts << right
432 // time stamp
433 << qSetFieldWidth(20)
434 << child->data(0, Qt::DisplayRole).toString()
435 // separator
436 << qSetFieldWidth(0) << ","
437
438 // file name
439 << qSetFieldWidth(64)
440 << child->data(1, Qt::DisplayRole).toString()
441 // separator
442 << qSetFieldWidth(0) << ","
443
444 // folder
445 << qSetFieldWidth(30)
446 << child->data(2, Qt::DisplayRole).toString()
447 // separator
448 << qSetFieldWidth(0) << ","
449
450 // action
451 << qSetFieldWidth(15)
452 << child->data(3, Qt::DisplayRole).toString()
453 << qSetFieldWidth(0)
454 << endl;
455 }
456 }
457
showFolderErrors(const QString & folderAlias)458 void IssuesWidget::showFolderErrors(const QString &folderAlias)
459 {
460 auto folder = FolderMan::instance()->folder(folderAlias);
461 if (!folder)
462 return;
463
464 _ui->filterAccount->setCurrentIndex(
465 qMax(0, _ui->filterAccount->findData(QVariant::fromValue(folder->accountState()))));
466 _ui->filterFolder->setCurrentIndex(
467 qMax(0, _ui->filterFolder->findData(folderAlias)));
468 _ui->showIgnores->setChecked(false);
469 _ui->showWarnings->setChecked(false);
470 }
471
addError(const QString & folderAlias,const QString & message,ErrorCategory category)472 void IssuesWidget::addError(const QString &folderAlias, const QString &message,
473 ErrorCategory category)
474 {
475 auto folder = FolderMan::instance()->folder(folderAlias);
476 if (!folder)
477 return;
478
479 QStringList columns;
480 QDateTime timestamp = QDateTime::currentDateTime();
481 const QString timeStr = ProtocolItem::timeString(timestamp);
482 const QString longTimeStr = ProtocolItem::timeString(timestamp, QLocale::LongFormat);
483
484 columns << timeStr;
485 columns << ""; // no "File" entry
486 columns << folder->shortGuiLocalPath();
487 columns << message;
488
489 QIcon icon = Theme::instance()->syncStateIcon(SyncResult::Error);
490
491 QTreeWidgetItem *twitem = new ProtocolItem(columns);
492 twitem->setData(0, Qt::SizeHintRole, QSize(0, ActivityItemDelegate::rowHeight()));
493 twitem->setIcon(0, icon);
494 twitem->setToolTip(0, longTimeStr);
495 twitem->setToolTip(3, message);
496 ProtocolItem::ExtraData data;
497 data.timestamp = timestamp;
498 data.folderName = folderAlias;
499 data.status = SyncFileItem::NormalError;
500 ProtocolItem::setExtraData(twitem, data);
501
502 addItem(twitem);
503 addErrorWidget(twitem, message, category);
504 }
505
addErrorWidget(QTreeWidgetItem * item,const QString & message,ErrorCategory category)506 void IssuesWidget::addErrorWidget(QTreeWidgetItem *item, const QString &message, ErrorCategory category)
507 {
508 QWidget *widget = nullptr;
509 if (category == ErrorCategory::InsufficientRemoteStorage) {
510 widget = new QWidget;
511 auto layout = new QHBoxLayout;
512 widget->setLayout(layout);
513
514 auto label = new ElidedLabel(message, widget);
515 label->setElideMode(Qt::ElideMiddle);
516 layout->addWidget(label);
517
518 auto button = new QPushButton("Retry all uploads", widget);
519 button->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Expanding);
520 auto folderAlias = ProtocolItem::extraData(item).folderName;
521 connect(button, &QPushButton::clicked,
522 this, [this, folderAlias]() { retryInsufficentRemoteStorageErrors(folderAlias); });
523 layout->addWidget(button);
524 }
525
526 if (widget) {
527 item->setText(3, QString());
528 }
529 _ui->_treeWidget->setItemWidget(item, 3, widget);
530 }
531
retryInsufficentRemoteStorageErrors(const QString & folderAlias)532 void IssuesWidget::retryInsufficentRemoteStorageErrors(const QString &folderAlias)
533 {
534 auto folderman = FolderMan::instance();
535 auto folder = folderman->folder(folderAlias);
536 if (!folder)
537 return;
538
539 folder->journalDb()->wipeErrorBlacklistCategory(SyncJournalErrorBlacklistRecord::InsufficientRemoteStorage);
540 folderman->scheduleFolderNext(folder);
541 }
542 }
543