1 /*
2 ** Copyright (c) 2008 - present, Alexis Megas.
3 ** All rights reserved.
4 **
5 ** Redistribution and use in source and binary forms, with or without
6 ** modification, are permitted provided that the following conditions
7 ** are met:
8 ** 1. Redistributions of source code must retain the above copyright
9 **    notice, this list of conditions and the following disclaimer.
10 ** 2. Redistributions in binary form must reproduce the above copyright
11 **    notice, this list of conditions and the following disclaimer in the
12 **    documentation and/or other materials provided with the distribution.
13 ** 3. The name of the author may not be used to endorse or promote products
14 **    derived from Dooble without specific prior written permission.
15 **
16 ** DOOBLE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
17 ** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
18 ** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
19 ** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
20 ** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
21 ** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22 ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23 ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
25 ** DOOBLE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 */
27 
28 #include <QDir>
29 #include <QKeyEvent>
30 #include <QSqlQuery>
31 #include <QWebEngineScript>
32 #include <QWebEngineScriptCollection>
33 
34 #include "dooble.h"
35 #include "dooble_cryptography.h"
36 #include "dooble_database_utilities.h"
37 #include "dooble_style_sheet.h"
38 #include "dooble_web_engine_page.h"
39 
40 /*
41 ** body { -webkit-transform: rotate(180deg); }
42 */
43 
44 QMap<QPair<QString, QUrl>, QString> dooble_style_sheet::s_style_sheets;
45 
dooble_style_sheet(dooble_web_engine_page * web_engine_page,QWidget * parent)46 dooble_style_sheet::dooble_style_sheet(dooble_web_engine_page *web_engine_page,
47 				       QWidget *parent):QDialog(parent)
48 {
49   m_ui.setupUi(this);
50   m_web_engine_page = web_engine_page;
51   connect(m_ui.add,
52 	  SIGNAL(clicked(void)),
53 	  this,
54 	  SLOT(slot_add(void)));
55   connect(m_ui.names,
56 	  SIGNAL(itemSelectionChanged(void)),
57 	  this,
58 	  SLOT(slot_item_selection_changed(void)));
59   connect(m_ui.remove,
60 	  SIGNAL(clicked(void)),
61 	  this,
62 	  SLOT(slot_remove(void)));
63   populate();
64   new QShortcut(QKeySequence(tr("Ctrl+W")), this, SLOT(close(void)));
65 }
66 
dooble_style_sheet(void)67 dooble_style_sheet::dooble_style_sheet(void)
68 {
69   m_web_engine_page = nullptr;
70 }
71 
inject(dooble_web_engine_page * web_engine_page)72 void dooble_style_sheet::inject(dooble_web_engine_page *web_engine_page)
73 {
74   if(!web_engine_page)
75     return;
76 
77   QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
78 
79   QMapIterator<QPair<QString, QUrl>, QString> it(s_style_sheets);
80 
81   while(it.hasNext())
82     {
83       it.next();
84 
85       if(it.key().second == web_engine_page->simplified_url())
86 	{
87 	  auto style_sheet
88 	    (QString::fromLatin1("(function() {"
89 				 "css = document.createElement('style');"
90 				 "css.id = '%1';"
91 				 "css.type = 'text/css';"
92 				 "css.innerText = '%2';"
93 				 "document.head.appendChild(css);"
94 				 "})()").
95 	     arg(it.key().first).
96 	     arg(it.value()));
97 	  QWebEngineScript web_engine_script;
98 
99 	  web_engine_script.setInjectionPoint(QWebEngineScript::DocumentReady);
100 	  web_engine_script.setName(it.key().first);
101 	  web_engine_script.setRunsOnSubFrames(true);
102 	  web_engine_script.setSourceCode(style_sheet);
103 	  web_engine_script.setWorldId(QWebEngineScript::ApplicationWorld);
104 	  web_engine_page->runJavaScript
105 	    (style_sheet, QWebEngineScript::ApplicationWorld);
106 	  web_engine_page->scripts().insert(web_engine_script);
107 	}
108       else
109 	{
110 	  auto style_sheet
111 	    (QString::fromLatin1("(function() {"
112 				 "var element = document.getElementById('%1');"
113 				 "if(element) element.outerHTML = '';"
114 				 "delete element;})()").arg(it.key().first));
115 
116 	  web_engine_page->runJavaScript
117 	    (style_sheet, QWebEngineScript::ApplicationWorld);
118 #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
119 	  web_engine_page->scripts().remove
120 	    (web_engine_page->scripts().findScript(it.key().first));
121 #else
122 	  foreach(const auto &script,
123 		  web_engine_page->scripts().find(it.key().first))
124 	    web_engine_page->scripts().remove(script);
125 #endif
126 	}
127     }
128 
129   QApplication::restoreOverrideCursor();
130 }
131 
keyPressEvent(QKeyEvent * event)132 void dooble_style_sheet::keyPressEvent(QKeyEvent *event)
133 {
134   if(event && event->key() == Qt::Key_Escape)
135     close();
136 
137   QDialog::keyPressEvent(event);
138 }
139 
populate(void)140 void dooble_style_sheet::populate(void)
141 {
142   if(!m_web_engine_page)
143     return;
144 
145   QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
146   m_ui.names->clear();
147 
148   QMapIterator<QPair<QString, QUrl>, QString> it(s_style_sheets);
149 
150   while(it.hasNext())
151     {
152       it.next();
153 
154       if(it.key().second == m_web_engine_page->simplified_url())
155 	m_ui.names->addItem(it.key().first);
156     }
157 
158   m_ui.names->sortItems();
159   m_ui.names->setCurrentRow(0);
160   QApplication::restoreOverrideCursor();
161 }
162 
purge(void)163 void dooble_style_sheet::purge(void)
164 {
165   s_style_sheets.clear();
166 
167   auto database_name(dooble_database_utilities::database_name());
168 
169   {
170     auto db = QSqlDatabase::addDatabase("QSQLITE", database_name);
171 
172     db.setDatabaseName(dooble_settings::setting("home_path").toString() +
173 		       QDir::separator() +
174 		       "dooble_style_sheets.db");
175 
176     if(db.open())
177       {
178 	QSqlQuery query(db);
179 
180 	query.exec("PRAGMA synchronous = OFF");
181 	query.exec("DELETE FROM dooble_style_sheets");
182 	query.exec("VACUUM");
183       }
184 
185     db.close();
186   }
187 
188   QSqlDatabase::removeDatabase(database_name);
189 }
190 
slot_add(void)191 void dooble_style_sheet::slot_add(void)
192 {
193   if(!m_web_engine_page)
194     return;
195 
196   auto name(m_ui.name->text().trimmed());
197 
198   if(m_ui.style_sheet->toPlainText().trimmed().isEmpty() || name.isEmpty())
199     return;
200 
201   auto style_sheet
202     (QString::fromLatin1("(function() {"
203 			 "css = document.createElement('style');"
204 			 "css.id = '%1';"
205 			 "css.type = 'text/css';"
206 			 "css.innerText = '%2';"
207 			 "document.head.appendChild(css);"
208 			 "})()").
209      arg(name).
210      arg(m_ui.style_sheet->toPlainText().trimmed()));
211   QWebEngineScript web_engine_script;
212 
213   web_engine_script.setInjectionPoint(QWebEngineScript::DocumentReady);
214   web_engine_script.setName(name);
215   web_engine_script.setRunsOnSubFrames(true);
216   web_engine_script.setSourceCode(style_sheet);
217   web_engine_script.setWorldId(QWebEngineScript::ApplicationWorld);
218 
219   if(m_ui.names->findItems(name, Qt::MatchExactly).isEmpty())
220     {
221       m_ui.names->addItem(name);
222       m_ui.names->sortItems();
223     }
224 
225   m_ui.style_sheet->setPlainText(m_ui.style_sheet->toPlainText().trimmed());
226   m_web_engine_page->runJavaScript
227     (style_sheet, QWebEngineScript::ApplicationWorld);
228 #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
229   m_web_engine_page->scripts().remove
230     (m_web_engine_page->scripts().findScript(name));
231 #else
232   foreach(const auto &script, m_web_engine_page->scripts().find(name))
233     m_web_engine_page->scripts().remove(script);
234 #endif
235   m_web_engine_page->scripts().insert(web_engine_script);
236   s_style_sheets
237     [QPair<QString, QUrl> (name, m_web_engine_page->simplified_url())] =
238     m_ui.style_sheet->toPlainText().trimmed();
239 
240   if(!dooble::s_cryptography || !dooble::s_cryptography->authenticated())
241     return;
242 
243   auto database_name(dooble_database_utilities::database_name());
244 
245   {
246     auto db = QSqlDatabase::addDatabase("QSQLITE", database_name);
247 
248     db.setDatabaseName(dooble_settings::setting("home_path").toString() +
249 		       QDir::separator() +
250 		       "dooble_style_sheets.db");
251 
252     if(db.open())
253       {
254 	QSqlQuery query(db);
255 
256 	query.exec("CREATE TABLE IF NOT EXISTS dooble_style_sheets ("
257 		   "name TEXT NOT NULL, "
258 		   "name_digest TEXT NOT NULL, "
259 		   "style_sheet TEXT NOT NULL, "
260 		   "url TEXT NOT NULL, "
261 		   "url_digest TEXT NOT NULL, "
262 		   "PRIMARY KEY(name_digest, url_digest))");
263 	query.prepare
264 	  ("INSERT OR REPLACE INTO dooble_style_sheets "
265 	   "(name, name_digest, style_sheet, url, url_digest) "
266 	   "VALUES (?, ?, ?, ?, ?)");
267 
268 	QByteArray bytes;
269 
270 	bytes = dooble::s_cryptography->encrypt_then_mac(name.toUtf8());
271 
272 	if(!bytes.isEmpty())
273 	  query.addBindValue(bytes.toBase64());
274 	else
275 	  goto done_label;
276 
277 	query.addBindValue
278 	  (dooble::s_cryptography->hmac(name.toUtf8()).toBase64());
279 	bytes = dooble::s_cryptography->encrypt_then_mac
280 	  (m_ui.style_sheet->toPlainText().trimmed().toLatin1());
281 
282 	if(!bytes.isEmpty())
283 	  query.addBindValue(bytes.toBase64());
284 	else
285 	  goto done_label;
286 
287 	bytes = dooble::s_cryptography->encrypt_then_mac
288 	  (m_web_engine_page->simplified_url().toEncoded());
289 
290 	if(!bytes.isEmpty())
291 	  query.addBindValue(bytes.toBase64());
292 	else
293 	  goto done_label;
294 
295 	query.addBindValue
296 	  (dooble::s_cryptography->
297 	   hmac(m_web_engine_page->simplified_url().toEncoded()).toBase64());
298 	query.exec();
299       }
300 
301   done_label:
302     db.close();
303   }
304 
305   QSqlDatabase::removeDatabase(database_name);
306 }
307 
slot_item_selection_changed(void)308 void dooble_style_sheet::slot_item_selection_changed(void)
309 {
310   auto list(m_ui.names->selectedItems());
311 
312   if(list.isEmpty() || !list.at(0) || !m_web_engine_page)
313     {
314       m_ui.name->clear();
315       m_ui.style_sheet->clear();
316       return;
317     }
318 
319   m_ui.name->setText(list.at(0)->text());
320   m_ui.style_sheet->setPlainText
321     (s_style_sheets.
322      value(QPair<QString, QUrl> (m_ui.name->text(),
323 				 m_web_engine_page->simplified_url())));
324 }
325 
slot_populate(void)326 void dooble_style_sheet::slot_populate(void)
327 {
328   if(!dooble::s_cryptography || !dooble::s_cryptography->authenticated())
329     return;
330 
331   auto database_name(dooble_database_utilities::database_name());
332 
333   {
334     auto db = QSqlDatabase::addDatabase("QSQLITE", database_name);
335 
336     db.setDatabaseName(dooble_settings::setting("home_path").toString() +
337 		       QDir::separator() +
338 		       "dooble_style_sheets.db");
339 
340     if(db.open())
341       {
342 	QSqlQuery query(db);
343 
344 	query.setForwardOnly(true);
345 
346 	if(query.exec("SELECT name, style_sheet, url, OID FROM "
347 		      "dooble_style_sheets"))
348 	  while(query.next())
349 	    {
350 	      auto name
351 		(QByteArray::fromBase64(query.value(0).toByteArray()));
352 	      auto style_sheet
353 		(QByteArray::fromBase64(query.value(1).toByteArray()));
354 	      auto url
355 		(QByteArray::fromBase64(query.value(2).toByteArray()));
356 
357 	      name = dooble::s_cryptography->mac_then_decrypt(name);
358 	      style_sheet = dooble::s_cryptography->mac_then_decrypt
359 		(style_sheet);
360 	      url = dooble::s_cryptography->mac_then_decrypt(url);
361 
362 	      if(name.isEmpty() || style_sheet.isEmpty() || url.isEmpty())
363 		{
364 		  dooble_database_utilities::remove_entry
365 		    (db, "dooble_style_sheets", query.value(3).toLongLong());
366 		  continue;
367 		}
368 
369 	      s_style_sheets
370 		[QPair<QString, QUrl> (name, QUrl::fromEncoded(url))] =
371 		style_sheet;
372 	    }
373       }
374 
375     db.close();
376   }
377 
378   QSqlDatabase::removeDatabase(database_name);
379   emit populated();
380 }
381 
slot_remove(void)382 void dooble_style_sheet::slot_remove(void)
383 {
384   if(!m_web_engine_page)
385     return;
386 
387   auto list(m_ui.names->selectedItems());
388 
389   if(list.isEmpty() || !list.at(0))
390     return;
391 
392   auto name(list.at(0)->text());
393   auto style_sheet
394     (QString::fromLatin1("(function() {"
395 			 "var element = document.getElementById('%1');"
396 			 "if(element) element.outerHTML = '';"
397 			 "delete element;})()").arg(name));
398 
399   delete m_ui.names->takeItem(m_ui.names->row(list.at(0)));
400   m_web_engine_page->runJavaScript
401     (style_sheet, QWebEngineScript::ApplicationWorld);
402 #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
403   m_web_engine_page->scripts().remove
404     (m_web_engine_page->scripts().findScript(name));
405 #else
406   foreach(const auto &script, m_web_engine_page->scripts().find(name))
407     m_web_engine_page->scripts().remove(script);
408 #endif
409   s_style_sheets.remove
410     (QPair<QString, QUrl> (name, m_web_engine_page->simplified_url()));
411 
412   if(!dooble::s_cryptography || !dooble::s_cryptography->authenticated())
413     return;
414 
415   auto database_name(dooble_database_utilities::database_name());
416 
417   {
418     auto db = QSqlDatabase::addDatabase("QSQLITE", database_name);
419 
420     db.setDatabaseName(dooble_settings::setting("home_path").toString() +
421 		       QDir::separator() +
422 		       "dooble_style_sheets.db");
423 
424     if(db.open())
425       {
426 	QSqlQuery query(db);
427 
428 	query.prepare("DELETE FROM dooble_style_sheets WHERE "
429 		      "name_digest = ? AND url_digest = ?");
430 	query.addBindValue
431 	  (dooble::s_cryptography->hmac(name.toUtf8()).toBase64());
432 	query.addBindValue
433 	  (dooble::s_cryptography->
434 	   hmac(m_web_engine_page->simplified_url().toEncoded()).toBase64());
435 	query.exec();
436       }
437 
438     db.close();
439   }
440 
441   QSqlDatabase::removeDatabase(database_name);
442 }
443