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 &parameters, 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 &parameters, 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 &parameters) 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