1 /**************************************************************************
2 * Otter Browser: Web browser controlled by the user, not vice-versa.
3 * Copyright (C) 2016 - 2018 Michal Dutkiewicz aka Emdek <michal@emdek.pl>
4 * Copyright (C) 2016 Piotr Wójcik <chocimier@tlen.pl>
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 *
19 **************************************************************************/
20 
21 #include "AddonsContentsWidget.h"
22 #include "../../../core/SessionsManager.h"
23 #include "../../../core/JsonSettings.h"
24 #include "../../../core/ThemesManager.h"
25 #include "../../../core/UserScript.h"
26 #include "../../../core/Utils.h"
27 
28 #include "ui_AddonsContentsWidget.h"
29 
30 #include <QtCore/QFile>
31 #include <QtCore/QJsonObject>
32 #include <QtCore/QStandardPaths>
33 #include <QtCore/QTimer>
34 #include <QtGui/QKeyEvent>
35 #include <QtWidgets/QCheckBox>
36 #include <QtWidgets/QFileDialog>
37 #include <QtWidgets/QMenu>
38 #include <QtWidgets/QMessageBox>
39 
40 namespace Otter
41 {
42 
AddonsContentsWidget(const QVariantMap & parameters,Window * window,QWidget * parent)43 AddonsContentsWidget::AddonsContentsWidget(const QVariantMap &parameters, Window *window, QWidget *parent) : ContentsWidget(parameters, window, parent),
44 	m_model(new QStandardItemModel(this)),
45 	m_isLoading(true),
46 	m_ui(new Ui::AddonsContentsWidget)
47 {
48 	m_ui->setupUi(this);
49 	m_ui->filterLineEditWidget->setClearOnEscape(true);
50 	m_ui->addonsViewWidget->setViewMode(ItemViewWidget::TreeView);
51 	m_ui->addonsViewWidget->installEventFilter(this);
52 
53 	QTimer::singleShot(100, this, &AddonsContentsWidget::populateAddons);
54 
55 	connect(m_ui->filterLineEditWidget, &LineEditWidget::textChanged, m_ui->addonsViewWidget, &ItemViewWidget::setFilterString);
56 	connect(m_ui->addonsViewWidget, &ItemViewWidget::customContextMenuRequested, this, &AddonsContentsWidget::showContextMenu);
57 	connect(m_ui->addonsViewWidget, &ItemViewWidget::clicked, this, &AddonsContentsWidget::save);
58 }
59 
~AddonsContentsWidget()60 AddonsContentsWidget::~AddonsContentsWidget()
61 {
62 	delete m_ui;
63 }
64 
changeEvent(QEvent * event)65 void AddonsContentsWidget::changeEvent(QEvent *event)
66 {
67 	ContentsWidget::changeEvent(event);
68 
69 	if (event->type() == QEvent::LanguageChange)
70 	{
71 		m_ui->retranslateUi(this);
72 
73 		for (int i = 0; i < m_model->rowCount(); ++i)
74 		{
75 			const QModelIndex index(m_model->index(i, 0));
76 
77 			if (static_cast<Addon::AddonType>(index.data(TypeRole).toInt()) == Addon::UserScriptType)
78 			{
79 				m_model->setData(index, tr("User Scripts"), Qt::DisplayRole);
80 			}
81 		}
82 	}
83 }
84 
getSelectedAddons() const85 QVector<Addon*> AddonsContentsWidget::getSelectedAddons() const
86 {
87 	const QModelIndexList indexes(m_ui->addonsViewWidget->selectionModel()->selectedIndexes());
88 	QVector<Addon*> addons;
89 	addons.reserve(indexes.count());
90 
91 	for (int i = 0; i < indexes.count(); ++i)
92 	{
93 		if (indexes.at(i).isValid() && indexes.at(i).parent() != m_model->invisibleRootItem()->index())
94 		{
95 			Addon::AddonType type(static_cast<Addon::AddonType>(indexes.at(i).parent().data(TypeRole).toInt()));
96 
97 			if (type == Addon::UserScriptType)
98 			{
99 				UserScript *script(AddonsManager::getUserScript(indexes.at(i).data(NameRole).toString()));
100 
101 				if (script)
102 				{
103 					addons.append(script);
104 				}
105 			}
106 		}
107 	}
108 
109 	addons.squeeze();
110 
111 	return addons;
112 }
113 
populateAddons()114 void AddonsContentsWidget::populateAddons()
115 {
116 	m_types.clear();
117 	m_types[Addon::UserScriptType] = 0;
118 
119 	QStandardItem *userScriptsItem(new QStandardItem(ThemesManager::createIcon(QLatin1String("addon-user-script"), false), tr("User Scripts")));
120 	userScriptsItem->setData(Addon::UserScriptType, TypeRole);
121 
122 	m_model->appendRow(userScriptsItem);
123 
124 	const QStringList userScripts(AddonsManager::getUserScripts());
125 
126 	for (int i = 0; i < userScripts.count(); ++i)
127 	{
128 		addAddon(AddonsManager::getUserScript(userScripts.at(i)));
129 	}
130 
131 	m_ui->addonsViewWidget->setModel(m_model);
132 	m_ui->addonsViewWidget->expandAll();
133 
134 	m_isLoading = false;
135 
136 	emit loadingStateChanged(WebWidget::FinishedLoadingState);
137 
138 	connect(AddonsManager::getInstance(), &AddonsManager::userScriptModified, this, &AddonsContentsWidget::updateAddon);
139 	connect(m_ui->addonsViewWidget->selectionModel(), &QItemSelectionModel::selectionChanged, [&]()
140 	{
141 		emit arbitraryActionsStateChanged({ActionsManager::DeleteAction});
142 	});
143 }
144 
addAddon()145 void AddonsContentsWidget::addAddon()
146 {
147 	const QStringList sourcePaths(QFileDialog::getOpenFileNames(this, tr("Select Files"), QStandardPaths::standardLocations(QStandardPaths::HomeLocation).value(0), Utils::formatFileTypes({tr("User Script files (*.js)")})));
148 
149 	if (sourcePaths.isEmpty())
150 	{
151 		return;
152 	}
153 
154 	QStringList failedPaths;
155 	ReplaceMode replaceMode(UnknownMode);
156 
157 	for (int i = 0; i < sourcePaths.count(); ++i)
158 	{
159 		if (sourcePaths.at(i).isEmpty())
160 		{
161 			continue;
162 		}
163 
164 		const QString scriptName(QFileInfo(sourcePaths.at(i)).completeBaseName());
165 		const QString targetDirectory(QDir(SessionsManager::getWritableDataPath(QLatin1String("scripts"))).filePath(scriptName));
166 		const QString targetPath(QDir(targetDirectory).filePath(QFileInfo(sourcePaths.at(i)).fileName()));
167 		bool replaceScript(false);
168 
169 		if (QFile::exists(targetPath))
170 		{
171 			if (replaceMode == IgnoreAllMode)
172 			{
173 				continue;
174 			}
175 
176 			if (replaceMode == ReplaceAllMode)
177 			{
178 				replaceScript = true;
179 			}
180 			else
181 			{
182 				QMessageBox messageBox;
183 				messageBox.setWindowTitle(tr("Question"));
184 				messageBox.setText(tr("User Script with this name already exists:\n%1").arg(targetPath));
185 				messageBox.setInformativeText(tr("Do you want to replace it?"));
186 				messageBox.setIcon(QMessageBox::Question);
187 				messageBox.addButton(QMessageBox::Yes);
188 				messageBox.addButton(QMessageBox::No);
189 
190 				if (i < (sourcePaths.count() - 1))
191 				{
192 					messageBox.setCheckBox(new QCheckBox(tr("Apply to all")));
193 				}
194 
195 				messageBox.exec();
196 
197 				replaceScript = (messageBox.standardButton(messageBox.clickedButton()) == QMessageBox::Yes);
198 
199 				if (messageBox.checkBox() && messageBox.checkBox()->isChecked())
200 				{
201 					replaceMode = (replaceScript ? ReplaceAllMode : IgnoreAllMode);
202 				}
203 			}
204 
205 			if (!replaceScript)
206 			{
207 				continue;
208 			}
209 		}
210 
211 		if ((replaceScript && !QDir().remove(targetPath)) || (!replaceScript && !QDir().mkpath(targetDirectory)) || !QFile::copy(sourcePaths.at(i), targetPath))
212 		{
213 			failedPaths.append(sourcePaths.at(i));
214 
215 			continue;
216 		}
217 
218 		if (replaceScript)
219 		{
220 			UserScript *script(AddonsManager::getUserScript(scriptName));
221 
222 			if (script)
223 			{
224 				script->reload();
225 			}
226 		}
227 		else
228 		{
229 			UserScript script(targetPath);
230 
231 			addAddon(&script);
232 		}
233 	}
234 
235 	if (!failedPaths.isEmpty())
236 	{
237 		QMessageBox::critical(this, tr("Error"), tr("Failed to import following User Script file(s):\n%1", "", failedPaths.count()).arg(failedPaths.join(QLatin1Char('\n'))), QMessageBox::Close);
238 	}
239 
240 	save();
241 
242 	AddonsManager::loadUserScripts();
243 }
244 
addAddon(Addon * addon)245 void AddonsContentsWidget::addAddon(Addon *addon)
246 {
247 	if (!addon)
248 	{
249 		return;
250 	}
251 
252 	QStandardItem *typeItem(nullptr);
253 
254 	if (m_types.contains(addon->getType()))
255 	{
256 		typeItem = m_model->item(m_types[addon->getType()]);
257 	}
258 
259 	if (!typeItem)
260 	{
261 		return;
262 	}
263 
264 	QStandardItem *item(new QStandardItem(getAddonIcon(addon), (addon->getVersion().isEmpty() ? addon->getTitle() : QStringLiteral("%1 %2").arg(addon->getTitle()).arg(addon->getVersion()))));
265 	item->setFlags(item->flags() | Qt::ItemNeverHasChildren);
266 	item->setCheckable(true);
267 	item->setCheckState(addon->isEnabled() ? Qt::Checked : Qt::Unchecked);
268 	item->setToolTip(addon->getDescription());
269 
270 	if (addon->getType() == Addon::UserScriptType)
271 	{
272 		const UserScript *script(static_cast<UserScript*>(addon));
273 
274 		if (script)
275 		{
276 			item->setData(script->getName(), NameRole);
277 		}
278 	}
279 
280 	typeItem->appendRow(item);
281 }
282 
updateAddon(const QString & name)283 void AddonsContentsWidget::updateAddon(const QString &name)
284 {
285 	const QStandardItem *userScriptsItem(m_model->item(m_types.value(Addon::UserScriptType)));
286 
287 	if (!userScriptsItem)
288 	{
289 		return;
290 	}
291 
292 	for (int i = 0; i < userScriptsItem->rowCount(); ++i)
293 	{
294 		const QModelIndex index(userScriptsItem->child(i)->index());
295 
296 		if (index.isValid() && index.data(NameRole).toString() == name)
297 		{
298 			UserScript *script(AddonsManager::getUserScript(name));
299 
300 			if (script)
301 			{
302 				m_ui->addonsViewWidget->setData(index, getAddonIcon(script), Qt::DecorationRole);
303 				m_ui->addonsViewWidget->setData(index, (script->getVersion().isEmpty() ? script->getTitle() : QStringLiteral("%1 %2").arg(script->getTitle()).arg(script->getVersion())), Qt::DisplayRole);
304 			}
305 
306 			break;
307 		}
308 	}
309 }
310 
openAddon()311 void AddonsContentsWidget::openAddon()
312 {
313 	const QVector<Addon*> addons(getSelectedAddons());
314 
315 	for (int i = 0; i < addons.count(); ++i)
316 	{
317 		if (addons.at(i)->getType() == Addon::UserScriptType)
318 		{
319 			const UserScript *script(static_cast<UserScript*>(addons.at(i)));
320 
321 			if (script)
322 			{
323 				Utils::runApplication({}, QUrl(script->getPath()));
324 			}
325 		}
326 	}
327 }
328 
reloadAddon()329 void AddonsContentsWidget::reloadAddon()
330 {
331 	const QModelIndexList indexes(m_ui->addonsViewWidget->selectionModel()->selectedIndexes());
332 
333 	for (int i = 0; i < indexes.count(); ++i)
334 	{
335 		if (indexes.at(i).isValid() && indexes.at(i).parent() != m_model->invisibleRootItem()->index())
336 		{
337 			Addon::AddonType type(static_cast<Addon::AddonType>(indexes.at(i).parent().data(TypeRole).toInt()));
338 
339 			if (type == Addon::UserScriptType)
340 			{
341 				UserScript *script(AddonsManager::getUserScript(indexes.at(i).data(NameRole).toString()));
342 
343 				if (script)
344 				{
345 					script->reload();
346 
347 					m_ui->addonsViewWidget->setData(indexes.at(i), getAddonIcon(script), Qt::DecorationRole);
348 					m_ui->addonsViewWidget->setData(indexes.at(i), (script->getVersion().isEmpty() ? script->getTitle() : QStringLiteral("%1 %2").arg(script->getTitle()).arg(script->getVersion())), Qt::DisplayRole);
349 				}
350 			}
351 		}
352 	}
353 }
354 
removeAddons()355 void AddonsContentsWidget::removeAddons()
356 {
357 	const QVector<Addon*> addons(getSelectedAddons());
358 
359 	if (addons.isEmpty())
360 	{
361 		return;
362 	}
363 
364 	QMessageBox messageBox;
365 	messageBox.setWindowTitle(tr("Question"));
366 	messageBox.setText(tr("You are about to irreversibly remove %n addon(s).", "", addons.count()));
367 	messageBox.setInformativeText(tr("Do you want to continue?"));
368 	messageBox.setIcon(QMessageBox::Question);
369 	messageBox.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel);
370 	messageBox.setDefaultButton(QMessageBox::Yes);
371 
372 	if (messageBox.exec() == QMessageBox::Yes)
373 	{
374 		for (int i = 0; i < addons.count(); ++i)
375 		{
376 			if (addons.at(i)->canRemove())
377 			{
378 				addons.at(i)->remove();
379 			}
380 		}
381 	}
382 
383 	AddonsManager::loadUserScripts();
384 
385 	save();
386 }
387 
save()388 void AddonsContentsWidget::save()
389 {
390 	const QStandardItem *userScriptsItem(m_model->item(m_types.value(Addon::UserScriptType)));
391 
392 	if (!userScriptsItem)
393 	{
394 		return;
395 	}
396 
397 	QModelIndexList indexesToRemove;
398 	QJsonObject settingsObject;
399 
400 	for (int i = 0; i < userScriptsItem->rowCount(); ++i)
401 	{
402 		const QModelIndex index(userScriptsItem->child(i)->index());
403 		const QString name(index.data(NameRole).toString());
404 
405 		if (index.isValid())
406 		{
407 			if (!name.isEmpty() && AddonsManager::getUserScript(name))
408 			{
409 				settingsObject.insert(name, QJsonObject({{QLatin1String("isEnabled"), QJsonValue(index.data(Qt::CheckStateRole).toInt() == Qt::Checked)}}));
410 			}
411 			else
412 			{
413 				indexesToRemove.append(index);
414 			}
415 		}
416 	}
417 
418 	JsonSettings settings;
419 	settings.setObject(settingsObject);
420 	settings.save(SessionsManager::getWritableDataPath(QLatin1String("scripts/scripts.json")));
421 
422 	for (int i = (indexesToRemove.count() - 1); i >= 0; --i)
423 	{
424 		m_ui->addonsViewWidget->model()->removeRow(indexesToRemove.at(i).row(), indexesToRemove.at(i).parent());
425 	}
426 }
427 
showContextMenu(const QPoint & position)428 void AddonsContentsWidget::showContextMenu(const QPoint &position)
429 {
430 	const QVector<Addon*> addons(getSelectedAddons());
431 	QMenu menu(this);
432 	menu.addAction(tr("Add Addon…"), this, static_cast<void(AddonsContentsWidget::*)()>(&AddonsContentsWidget::addAddon));
433 
434 	if (!addons.isEmpty())
435 	{
436 		menu.addSeparator();
437 		menu.addAction(tr("Open Addon File"), this, &AddonsContentsWidget::openAddon);
438 		menu.addAction(tr("Reload Addon"), this, &AddonsContentsWidget::reloadAddon);
439 		menu.addSeparator();
440 		menu.addAction(tr("Remove Addon…"), this, &AddonsContentsWidget::removeAddons);
441 	}
442 
443 	menu.exec(m_ui->addonsViewWidget->mapToGlobal(position));
444 }
445 
print(QPrinter * printer)446 void AddonsContentsWidget::print(QPrinter *printer)
447 {
448 	m_ui->addonsViewWidget->render(printer);
449 }
450 
triggerAction(int identifier,const QVariantMap & parameters,ActionsManager::TriggerType trigger)451 void AddonsContentsWidget::triggerAction(int identifier, const QVariantMap &parameters, ActionsManager::TriggerType trigger)
452 {
453 	switch (identifier)
454 	{
455 		case ActionsManager::SelectAllAction:
456 			m_ui->addonsViewWidget->selectAll();
457 
458 			break;
459 		case ActionsManager::DeleteAction:
460 			removeAddons();
461 
462 			break;
463 		case ActionsManager::FindAction:
464 		case ActionsManager::QuickFindAction:
465 			m_ui->filterLineEditWidget->setFocus();
466 
467 			break;
468 		case ActionsManager::ActivateContentAction:
469 			m_ui->addonsViewWidget->setFocus();
470 
471 			break;
472 		default:
473 			ContentsWidget::triggerAction(identifier, parameters, trigger);
474 
475 			break;
476 	}
477 }
478 
getTitle() const479 QString AddonsContentsWidget::getTitle() const
480 {
481 	return tr("Addons");
482 }
483 
getType() const484 QLatin1String AddonsContentsWidget::getType() const
485 {
486 	return QLatin1String("addons");
487 }
488 
getUrl() const489 QUrl AddonsContentsWidget::getUrl() const
490 {
491 	return QUrl(QLatin1String("about:addons"));
492 }
493 
getIcon() const494 QIcon AddonsContentsWidget::getIcon() const
495 {
496 	return ThemesManager::createIcon(QLatin1String("preferences-plugin"), false);
497 }
498 
getAddonIcon(Addon * addon) const499 QIcon AddonsContentsWidget::getAddonIcon(Addon *addon) const
500 {
501 	return ((!addon || addon->getIcon().isNull()) ? ThemesManager::createIcon(QLatin1String("addon-user-script"), false) : addon->getIcon());
502 }
503 
getActionState(int identifier,const QVariantMap & parameters) const504 ActionsManager::ActionDefinition::State AddonsContentsWidget::getActionState(int identifier, const QVariantMap &parameters) const
505 {
506 	ActionsManager::ActionDefinition::State state(ActionsManager::getActionDefinition(identifier).getDefaultState());
507 
508 	switch (identifier)
509 	{
510 		case ActionsManager::SelectAllAction:
511 			state.isEnabled = true;
512 
513 			return state;
514 		case ActionsManager::DeleteAction:
515 			state.isEnabled = (m_ui->addonsViewWidget->selectionModel() && !m_ui->addonsViewWidget->selectionModel()->selectedIndexes().isEmpty());
516 
517 			return state;
518 		default:
519 			break;
520 	}
521 
522 	return ContentsWidget::getActionState(identifier, parameters);
523 }
524 
getLoadingState() const525 WebWidget::LoadingState AddonsContentsWidget::getLoadingState() const
526 {
527 	return (m_isLoading ? WebWidget::OngoingLoadingState : WebWidget::FinishedLoadingState);
528 }
529 
eventFilter(QObject * object,QEvent * event)530 bool AddonsContentsWidget::eventFilter(QObject *object, QEvent *event)
531 {
532 	if (object == m_ui->addonsViewWidget && event->type() == QEvent::KeyPress && static_cast<QKeyEvent*>(event)->key() == Qt::Key_Delete)
533 	{
534 		removeAddons();
535 
536 		return true;
537 	}
538 
539 	return ContentsWidget::eventFilter(object, event);
540 }
541 
542 }
543