1 /**************************************************************************
2 * Otter Browser: Web browser controlled by the user, not vice-versa.
3 * Copyright (C) 2016 - 2019 Michal Dutkiewicz aka Emdek <michal@emdek.pl>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 *
18 **************************************************************************/
19
20 #include "PasswordsContentsWidget.h"
21 #include "../../../core/Application.h"
22 #include "../../../core/HistoryManager.h"
23 #include "../../../core/PasswordsManager.h"
24 #include "../../../core/ThemesManager.h"
25 #include "../../../ui/Action.h"
26 #include "../../../ui/MainWindow.h"
27
28 #include "ui_PasswordsContentsWidget.h"
29
30 #include <QtCore/QTimer>
31 #include <QtGui/QKeyEvent>
32 #include <QtWidgets/QMenu>
33 #include <QtWidgets/QMessageBox>
34
35 namespace Otter
36 {
37
PasswordsContentsWidget(const QVariantMap & parameters,Window * window,QWidget * parent)38 PasswordsContentsWidget::PasswordsContentsWidget(const QVariantMap ¶meters, Window *window, QWidget *parent) : ContentsWidget(parameters, window, parent),
39 m_model(new QStandardItemModel(this)),
40 m_isLoading(true),
41 m_ui(new Ui::PasswordsContentsWidget)
42 {
43 m_ui->setupUi(this);
44 m_ui->filterLineEditWidget->setClearOnEscape(true);
45 m_ui->passwordsViewWidget->installEventFilter(this);
46 m_ui->passwordsViewWidget->setViewMode(ItemViewWidget::TreeView);
47 m_ui->passwordsViewWidget->setModel(m_model);
48
49 m_model->setHeaderData(0, Qt::Horizontal, 500, HeaderViewWidget::WidthRole);
50
51 QTimer::singleShot(100, this, &PasswordsContentsWidget::populatePasswords);
52
53 connect(m_ui->filterLineEditWidget, &LineEditWidget::textChanged, this, &PasswordsContentsWidget::filterPasswords);
54 connect(m_ui->passwordsViewWidget, &ItemViewWidget::customContextMenuRequested, this, &PasswordsContentsWidget::showContextMenu);
55 }
56
~PasswordsContentsWidget()57 PasswordsContentsWidget::~PasswordsContentsWidget()
58 {
59 delete m_ui;
60 }
61
changeEvent(QEvent * event)62 void PasswordsContentsWidget::changeEvent(QEvent *event)
63 {
64 ContentsWidget::changeEvent(event);
65
66 if (event->type() == QEvent::LanguageChange)
67 {
68 m_ui->retranslateUi(this);
69
70 m_model->setHorizontalHeaderLabels({tr("Name"), tr("Value")});
71 }
72 }
73
populatePasswords()74 void PasswordsContentsWidget::populatePasswords()
75 {
76 m_model->clear();
77 m_model->setHorizontalHeaderLabels({tr("Name"), tr("Value")});
78 m_model->setHeaderData(0, Qt::Horizontal, 500, HeaderViewWidget::WidthRole);
79
80 const QStringList hosts(PasswordsManager::getHosts());
81
82 for (int i = 0; i < hosts.count(); ++i)
83 {
84 const QUrl url(QStringLiteral("http://%1/").arg(hosts.at(i)));
85 const QVector<PasswordsManager::PasswordInformation> passwords(PasswordsManager::getPasswords(url));
86 QStandardItem *hostItem(new QStandardItem(HistoryManager::getIcon(url), hosts.at(i)));
87 hostItem->setData(hosts.at(i), HostRole);
88
89 for (int j = 0; j < passwords.count(); ++j)
90 {
91 QStandardItem *setItem(new QStandardItem(tr("Set #%1").arg(j + 1)));
92 setItem->setData(passwords.at(j).url, UrlRole);
93 setItem->setData(((passwords.at(j).type == PasswordsManager::AuthPassword) ? QLatin1String("auth") : QLatin1String("form")), AuthTypeRole);
94
95 for (int k = 0; k < passwords.at(j).fields.count(); ++k)
96 {
97 QList<QStandardItem*> fieldItems({new QStandardItem(passwords.at(j).fields.at(k).name), new QStandardItem((passwords.at(j).fields.at(k).type == PasswordsManager::PasswordField) ? QString(QChar(8226)).repeated(5) : passwords.at(j).fields.at(k).value)});
98 fieldItems[0]->setData(passwords.at(j).fields.at(k).type, FieldTypeRole);
99 fieldItems[0]->setFlags(fieldItems[0]->flags() | Qt::ItemNeverHasChildren);
100 fieldItems[1]->setFlags(fieldItems[1]->flags() | Qt::ItemNeverHasChildren);
101
102 setItem->appendRow(fieldItems);
103 }
104
105 hostItem->appendRow({setItem, new QStandardItem()});
106 }
107
108 hostItem->setText(QStringLiteral("%1 (%2)").arg(hosts.at(i)).arg(hostItem->rowCount()));
109
110 m_model->appendRow(hostItem);
111 }
112
113 m_model->sort(0);
114
115 if (m_isLoading)
116 {
117 m_isLoading = false;
118
119 emit loadingStateChanged(WebWidget::FinishedLoadingState);
120
121 connect(PasswordsManager::getInstance(), &PasswordsManager::passwordsModified, this, &PasswordsContentsWidget::populatePasswords);
122 connect(m_ui->passwordsViewWidget->selectionModel(), &QItemSelectionModel::selectionChanged, [&]()
123 {
124 emit arbitraryActionsStateChanged({ActionsManager::DeleteAction});
125 });
126 }
127 }
128
removePasswords()129 void PasswordsContentsWidget::removePasswords()
130 {
131 const QModelIndexList indexes(m_ui->passwordsViewWidget->selectionModel()->selectedIndexes());
132
133 if (indexes.isEmpty())
134 {
135 return;
136 }
137
138 QVector<PasswordsManager::PasswordInformation> passwords;
139 passwords.reserve(indexes.count());
140
141 for (int i = 0; i < indexes.count(); ++i)
142 {
143 if (!indexes.at(i).isValid() || indexes.at(i).column() > 0)
144 {
145 continue;
146 }
147
148 if (indexes.at(i).parent() == m_model->invisibleRootItem()->index())
149 {
150 const QModelIndex hostIndex(indexes.at(i));
151
152 if (!hostIndex.isValid())
153 {
154 continue;
155 }
156
157 for (int j = 0; j < m_model->rowCount(hostIndex); ++j)
158 {
159 passwords.append(getPassword(hostIndex.child(j, 0)));
160 }
161 }
162 else
163 {
164 const QModelIndex setIndex((indexes.at(i).parent().parent() == m_model->invisibleRootItem()->index()) ? indexes.at(i) : indexes.at(i).parent());
165
166 if (setIndex.isValid())
167 {
168 passwords.append(getPassword(setIndex));
169 }
170 }
171 }
172
173 if (passwords.isEmpty())
174 {
175 return;
176 }
177
178 QMessageBox messageBox;
179 messageBox.setWindowTitle(tr("Question"));
180 messageBox.setText(tr("You are about to delete %n password(s).", "", passwords.count()));
181 messageBox.setInformativeText(tr("Do you want to continue?"));
182 messageBox.setIcon(QMessageBox::Question);
183 messageBox.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel);
184 messageBox.setDefaultButton(QMessageBox::Yes);
185
186 if (messageBox.exec() == QMessageBox::Yes)
187 {
188 for (int i = 0; i < passwords.count(); ++i)
189 {
190 PasswordsManager::removePassword(passwords.at(i));
191 }
192 }
193 }
194
removeHostPasswords()195 void PasswordsContentsWidget::removeHostPasswords()
196 {
197 const QModelIndexList indexes(m_ui->passwordsViewWidget->selectionModel()->selectedIndexes());
198
199 if (indexes.isEmpty())
200 {
201 return;
202 }
203
204 QStringList hosts;
205 int amount(0);
206
207 for (int i = 0; i < indexes.count(); ++i)
208 {
209 QModelIndex hostIndex(indexes.at(i));
210
211 while (hostIndex.parent().isValid() && hostIndex.parent() != m_model->invisibleRootItem()->index())
212 {
213 hostIndex = hostIndex.parent();
214 }
215
216 if (hostIndex.isValid())
217 {
218 const QString host(hostIndex.data(HostRole).toString());
219
220 if (!host.isEmpty() && !hosts.contains(host))
221 {
222 hosts.append(host);
223
224 amount += m_model->rowCount(hostIndex);
225 }
226 }
227 }
228
229 if (hosts.isEmpty())
230 {
231 return;
232 }
233
234 QMessageBox messageBox;
235 messageBox.setWindowTitle(tr("Question"));
236 messageBox.setText(tr("You are about to delete %n password(s).", "", amount));
237 messageBox.setInformativeText(tr("Do you want to continue?"));
238 messageBox.setIcon(QMessageBox::Question);
239 messageBox.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel);
240 messageBox.setDefaultButton(QMessageBox::Yes);
241
242 if (messageBox.exec() == QMessageBox::Yes)
243 {
244 for (int i = 0; i < hosts.count(); ++i)
245 {
246 PasswordsManager::clearPasswords(hosts.at(i));
247 }
248 }
249 }
250
removeAllPasswords()251 void PasswordsContentsWidget::removeAllPasswords()
252 {
253 QMessageBox messageBox;
254 messageBox.setWindowTitle(tr("Question"));
255 messageBox.setText(tr("You are about to delete all passwords."));
256 messageBox.setInformativeText(tr("Do you want to continue?"));
257 messageBox.setIcon(QMessageBox::Question);
258 messageBox.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel);
259 messageBox.setDefaultButton(QMessageBox::Yes);
260
261 if (messageBox.exec() == QMessageBox::Yes)
262 {
263 PasswordsManager::clearPasswords();
264 }
265 }
266
showContextMenu(const QPoint & position)267 void PasswordsContentsWidget::showContextMenu(const QPoint &position)
268 {
269 MainWindow *mainWindow(MainWindow::findMainWindow(this));
270 const QModelIndex index(m_ui->passwordsViewWidget->indexAt(position));
271 QMenu menu(this);
272
273 if (index.isValid())
274 {
275 if (index.parent() != m_model->invisibleRootItem()->index())
276 {
277 menu.addAction(tr("Remove Password"), this, &PasswordsContentsWidget::removePasswords);
278 }
279
280 menu.addAction(tr("Remove All Passwords from This Domain…"), this, &PasswordsContentsWidget::removeHostPasswords);
281 }
282
283 menu.addAction(tr("Remove All Passwords…"), this, &PasswordsContentsWidget::removeAllPasswords)->setEnabled(m_ui->passwordsViewWidget->model()->rowCount() > 0);
284 menu.addSeparator();
285 menu.addAction(new Action(ActionsManager::ClearHistoryAction, {}, ActionExecutor::Object(mainWindow, mainWindow), &menu));
286 menu.exec(m_ui->passwordsViewWidget->mapToGlobal(position));
287 }
288
print(QPrinter * printer)289 void PasswordsContentsWidget::print(QPrinter *printer)
290 {
291 m_ui->passwordsViewWidget->render(printer);
292 }
293
triggerAction(int identifier,const QVariantMap & parameters,ActionsManager::TriggerType trigger)294 void PasswordsContentsWidget::triggerAction(int identifier, const QVariantMap ¶meters, ActionsManager::TriggerType trigger)
295 {
296 switch (identifier)
297 {
298 case ActionsManager::SelectAllAction:
299 m_ui->passwordsViewWidget->selectAll();
300
301 break;
302 case ActionsManager::DeleteAction:
303 removePasswords();
304
305 break;
306 case ActionsManager::FindAction:
307 case ActionsManager::QuickFindAction:
308 m_ui->filterLineEditWidget->setFocus();
309
310 break;
311 case ActionsManager::ActivateContentAction:
312 m_ui->passwordsViewWidget->setFocus();
313
314 break;
315 default:
316 ContentsWidget::triggerAction(identifier, parameters, trigger);
317
318 break;
319 }
320 }
321
filterPasswords(const QString & filter)322 void PasswordsContentsWidget::filterPasswords(const QString &filter)
323 {
324 for (int i = 0; i < m_model->rowCount(); ++i)
325 {
326 const QModelIndex domainIndex(m_model->index(i, 0, m_model->invisibleRootItem()->index()));
327 int foundSets(0);
328 bool hasDomainMatch(filter.isEmpty() || domainIndex.data(Qt::DisplayRole).toString().contains(filter, Qt::CaseInsensitive));
329
330 for (int j = 0; j < m_model->rowCount(domainIndex); ++j)
331 {
332 const QModelIndex setIndex(domainIndex.child(j, 0));
333 bool hasFieldMatch(hasDomainMatch || setIndex.data(Qt::DisplayRole).toString().contains(filter, Qt::CaseInsensitive));
334
335 if (!hasFieldMatch)
336 {
337 for (int k = 0; k < m_model->rowCount(setIndex); ++k)
338 {
339 const QModelIndex fieldIndex(setIndex.child(k, 0));
340
341 if (fieldIndex.data(Qt::DisplayRole).toString().contains(filter, Qt::CaseInsensitive) || (fieldIndex.data(FieldTypeRole).toInt() != PasswordsManager::PasswordField && fieldIndex.sibling(fieldIndex.row(), 1).data(Qt::DisplayRole).toString().contains(filter, Qt::CaseInsensitive)))
342 {
343 hasFieldMatch = true;
344
345 break;
346 }
347 }
348
349 if (hasFieldMatch)
350 {
351 ++foundSets;
352 }
353 }
354
355 m_ui->passwordsViewWidget->setRowHidden(j, domainIndex, (!filter.isEmpty() && !hasFieldMatch));
356 }
357
358 m_ui->passwordsViewWidget->setRowHidden(i, m_model->invisibleRootItem()->index(), (foundSets == 0 && !hasDomainMatch));
359 }
360 }
361
getTitle() const362 QString PasswordsContentsWidget::getTitle() const
363 {
364 return tr("Passwords");
365 }
366
getType() const367 QLatin1String PasswordsContentsWidget::getType() const
368 {
369 return QLatin1String("passwords");
370 }
371
getUrl() const372 QUrl PasswordsContentsWidget::getUrl() const
373 {
374 return QUrl(QLatin1String("about:passwords"));
375 }
376
getIcon() const377 QIcon PasswordsContentsWidget::getIcon() const
378 {
379 return ThemesManager::createIcon(QLatin1String("dialog-password"), false);
380 }
381
getActionState(int identifier,const QVariantMap & parameters) const382 ActionsManager::ActionDefinition::State PasswordsContentsWidget::getActionState(int identifier, const QVariantMap ¶meters) const
383 {
384 ActionsManager::ActionDefinition::State state(ActionsManager::getActionDefinition(identifier).getDefaultState());
385
386 switch (identifier)
387 {
388 case ActionsManager::SelectAllAction:
389 state.isEnabled = true;
390
391 return state;
392 case ActionsManager::DeleteAction:
393 state.isEnabled = (m_ui->passwordsViewWidget->selectionModel() && !m_ui->passwordsViewWidget->selectionModel()->selectedIndexes().isEmpty());
394
395 return state;
396 default:
397 break;
398 }
399
400 return ContentsWidget::getActionState(identifier, parameters);
401 }
402
getPassword(const QModelIndex & index) const403 PasswordsManager::PasswordInformation PasswordsContentsWidget::getPassword(const QModelIndex &index) const
404 {
405 PasswordsManager::PasswordInformation password;
406 password.url = index.data(UrlRole).toString();
407 password.type = ((index.data(AuthTypeRole).toString() == QLatin1String("auth")) ? PasswordsManager::AuthPassword : PasswordsManager::FormPassword);
408
409 for (int i = 0; i < m_model->rowCount(index); ++i)
410 {
411 const QModelIndex nameIndex(index.child(i, 0));
412 PasswordsManager::PasswordInformation::Field field;
413 field.name = nameIndex.data(Qt::DisplayRole).toString();
414 field.value = ((nameIndex.data(FieldTypeRole).toInt() == PasswordsManager::PasswordField) ? QString() : index.child(i, 1).data(Qt::DisplayRole).toString());
415 field.type = static_cast<PasswordsManager::FieldType>(nameIndex.data(FieldTypeRole).toInt());
416
417 password.fields.append(field);
418 }
419
420 return password;
421 }
422
getLoadingState() const423 WebWidget::LoadingState PasswordsContentsWidget::getLoadingState() const
424 {
425 return (m_isLoading ? WebWidget::OngoingLoadingState : WebWidget::FinishedLoadingState);
426 }
427
eventFilter(QObject * object,QEvent * event)428 bool PasswordsContentsWidget::eventFilter(QObject *object, QEvent *event)
429 {
430 if (object == m_ui->passwordsViewWidget && event->type() == QEvent::KeyPress && static_cast<QKeyEvent*>(event)->key() == Qt::Key_Delete)
431 {
432 removePasswords();
433
434 return true;
435 }
436
437 return ContentsWidget::eventFilter(object, event);
438 }
439
440 }
441