1 #include <QDir>
2 #include <QSqlDriver>
3 #include <QSqlQuery>
4 #include <QSqlError>
5 #include <QMultiMap>
6 #include <QUuid>
7 
8 #include "settings.h"
9 #include "options.h"
10 #include "cookiejar.h"
11 #include "qmc2main.h"
12 #include "macros.h"
13 
14 extern MainWindow *qmc2MainWindow;
15 extern Settings *qmc2Config;
16 
CookieJar(QObject * parent)17 CookieJar::CookieJar(QObject *parent) : QNetworkCookieJar(parent)
18 {
19 	QString userScopePath = Options::configPath();
20 	db = QSqlDatabase::addDatabase("QSQLITE", "cookie-db-connection-" + QUuid::createUuid().toString());
21 	db.setDatabaseName(qmc2Config->value(QMC2_FRONTEND_PREFIX + "WebBrowser/CookieDatabase", userScopePath + "/qmc2-" + QMC2_EMU_NAME_VARIANT.toLower() + "-cookies.db").toString());
22 	if ( !db.open() ) {
23 		qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("WARNING: failed to open cookie database: error = '%1'").arg(db.lastError().text()));
24 		return;
25 	}
26 	QStringList tables = db.driver()->tables(QSql::Tables);
27 	if ( tables.count() != 1 || !tables.contains("qmc2_cookies") )
28 		recreateDatabase();
29 	static QStringList dbSyncModes = QStringList() << "OFF" << "NORMAL" << "FULL";
30 	QSqlQuery query(db);
31 	query.exec("PRAGMA synchronous = OFF");
32 	query.exec("PRAGMA journal_mode = MEMORY");
33 }
34 
~CookieJar()35 CookieJar::~CookieJar()
36 {
37 	if ( db.isOpen() ) {
38 		saveCookies();
39 		db.close();
40 	}
41 }
42 
recreateDatabase()43 void CookieJar::recreateDatabase()
44 {
45 	if ( !db.isOpen() )
46 		return;
47 
48 	QSqlQuery query(db);
49 	if ( !query.exec("DROP TABLE IF EXISTS qmc2_cookies") ) {
50 		qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("WARNING: failed to remove cookie database: query = '%1', error = '%2'").arg(query.lastQuery()).arg(query.lastError().text()));
51 		return;
52 	}
53 	query.finish();
54 	// vaccum'ing the database frees all disk-space previously used
55 	query.exec("VACUUM");
56 	query.finish();
57 	if ( !query.exec("CREATE TABLE qmc2_cookies (id INTEGER PRIMARY KEY, domain TEXT, name TEXT, value TEXT, path TEXT, expiry INTEGER, secure INTEGER, http_only INTEGER, CONSTRAINT qmc2_uniqueid UNIQUE (name, domain, path))") )
58 		qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("WARNING: failed to create cookie database: query = '%1', error = '%2'").arg(query.lastQuery()).arg(query.lastError().text()));
59 	cookieMap.clear();
60 	setAllCookies(QList<QNetworkCookie>());
61 }
62 
cookiesForUrl(const QUrl & url) const63 QList<QNetworkCookie> CookieJar::cookiesForUrl(const QUrl &url) const
64 {
65 	QString domain = url.host();
66 	QString path = url.path();
67 	QString defaultPath = path.left(path.lastIndexOf(QLatin1Char('/')) + 1);
68 	if ( defaultPath.isEmpty() )
69 		defaultPath = QLatin1Char('/');
70 	QList<QNetworkCookie> cookieList;
71 	if ( loadCookies(cookieList, domain, defaultPath) )
72 		return cookieList;
73 	else
74 		return QNetworkCookieJar::cookiesForUrl(url);
75 }
76 
setCookiesFromUrl(const QList<QNetworkCookie> & cookieList,const QUrl & url)77 bool CookieJar::setCookiesFromUrl(const QList<QNetworkCookie> &cookieList, const QUrl &url)
78 {
79 	QString domain = url.host();
80 	QString path = url.path();
81 	QString defaultPath = path.left(path.lastIndexOf(QLatin1Char('/')) + 1);
82 	if ( defaultPath.isEmpty() )
83 		defaultPath = QLatin1Char('/');
84 	for (int i = 0; i < cookieList.count(); i++) {
85 		QNetworkCookie cookie = cookieList[i];
86 		cookie.setDomain(domain);
87 		cookie.setPath(defaultPath);
88 		cookieMap.insertMulti(domain + defaultPath, cookie);
89 	}
90 	return QNetworkCookieJar::setCookiesFromUrl(cookieList, url);
91 }
92 
saveCookies()93 void CookieJar::saveCookies()
94 {
95 	if ( !db.isOpen() )
96 		return;
97 
98 	QSqlQuery query(db);
99 	QDateTime now = QDateTime::currentDateTime();
100 
101 	QMapIterator<QString, QNetworkCookie> it(cookieMap);
102 	QStringList cookieKeysProcessed;
103 	db.driver()->beginTransaction();
104 	while ( it.hasNext() ) {
105 		it.next();
106 		QNetworkCookie cookie = it.value();
107 		QString cookieKey = cookie.domain() + cookie.path() + cookie.name();
108 		if ( cookieKeysProcessed.contains(cookieKey) )
109 			continue;
110 		query.prepare("SELECT domain, name, path FROM qmc2_cookies WHERE domain=:domain AND path=:path AND name=:name");
111 		query.bindValue(":domain", cookie.domain());
112 		query.bindValue(":path", cookie.path());
113 		query.bindValue(":name", cookie.name());
114 		if ( query.exec() ) {
115 			if ( query.next() ) {
116 				query.finish();
117 				if ( cookie.value().isEmpty() ) {
118 					query.prepare("DELETE FROM qmc2_cookies WHERE domain=:domain AND path=:path AND name=:name");
119 					query.bindValue(":domain", cookie.domain());
120 					query.bindValue(":path", cookie.path());
121 					query.bindValue(":name", cookie.name());
122 					if ( !query.exec() )
123 						qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("WARNING: failed to remove expired cookie from database: query = '%1', error = '%2'").arg(query.lastQuery()).arg(query.lastError().text()));
124 					query.finish();
125 					cookieKeysProcessed << cookieKey;
126 				} else if ( cookie.expirationDate() < now ) {
127 					query.prepare("DELETE FROM qmc2_cookies WHERE domain=:domain AND path=:path AND name=:name");
128 					query.bindValue(":domain", cookie.domain());
129 					query.bindValue(":path", cookie.path());
130 					query.bindValue(":name", cookie.name());
131 					if ( !query.exec() )
132 						qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("WARNING: failed to remove expired cookie from database: query = '%1', error = '%2'").arg(query.lastQuery()).arg(query.lastError().text()));
133 					query.finish();
134 					cookieKeysProcessed << cookieKey;
135 				} else if ( !cookie.isSessionCookie() ) {
136 					query.prepare("UPDATE qmc2_cookies SET value=:value, expiry=" + QString::number(cookie.expirationDate().toTime_t()) + " WHERE domain=:domain AND path=:path AND name=:name");
137 					query.bindValue(":value", cookie.value());
138 					query.bindValue(":domain", cookie.domain());
139 					query.bindValue(":path", cookie.path());
140 					query.bindValue(":name", cookie.name());
141 					if ( !query.exec() )
142 						qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("WARNING: failed to update cookie in database: query = '%1', error = '%2'").arg(query.lastQuery()).arg(query.lastError().text()));
143 					query.finish();
144 					cookieKeysProcessed << cookieKey;
145 				}
146 			} else {
147 				query.finish();
148 				if ( cookie.expirationDate() > now && !cookie.isSessionCookie() ) {
149 					query.prepare("INSERT INTO qmc2_cookies (domain, name, value, path, expiry, secure, http_only) VALUES (:domain, :name, :value, :path, " + QString::number(cookie.expirationDate().toTime_t()) + ", " + QString(cookie.isSecure() ? "1" : "0") + ", " + QString(cookie.isHttpOnly() ? "1" : "0") + ")");
150 					query.bindValue(":value", cookie.value());
151 					query.bindValue(":domain", cookie.domain());
152 					query.bindValue(":path", cookie.path());
153 					query.bindValue(":name", cookie.name());
154 					if ( !query.exec() )
155 						qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("WARNING: failed to add cookie to database: query = '%1', error = '%2'").arg(query.lastQuery()).arg(query.lastError().text()));
156 					query.finish();
157 					cookieKeysProcessed << cookieKey;
158 				}
159 			}
160 		} else
161 			qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("WARNING: failed to query cookie database: query = '%1', error = '%2'").arg(query.lastQuery()).arg(query.lastError().text()));
162 	}
163 	db.driver()->commitTransaction();
164 }
165 
loadCookies(QList<QNetworkCookie> & cookieList,QString domain,QString path) const166 bool CookieJar::loadCookies(QList<QNetworkCookie> &cookieList, QString domain, QString path) const
167 {
168 	cookieList.clear();
169 
170 	if ( cookieMap.contains(domain + path) ) {
171 		cookieList = cookieMap.values(domain + path);
172 		return !cookieList.isEmpty();
173 	}
174 
175 	if ( !db.isOpen() )
176 		return false;
177 
178 	QSqlQuery query(db);
179 	query.prepare("SELECT domain, name, value, path, expiry, secure, http_only FROM qmc2_cookies WHERE domain=:domain AND path=:path");
180 	query.bindValue(":domain", domain);
181 	query.bindValue(":path", path);
182 	if ( !query.exec() ) {
183 		qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("WARNING: failed to fetch cookies from database: query = '%1', error = '%2'").arg(query.lastQuery()).arg(query.lastError().text()));
184 		return false;
185 	}
186 	QDateTime now = QDateTime::currentDateTime();
187 	QDateTime dt;
188 	db.driver()->beginTransaction();
189 	while ( query.next() ) {
190 		QNetworkCookie cookie;
191 		cookie.setDomain(query.value(0).toString());
192 		cookie.setName(query.value(1).toByteArray());
193 		cookie.setValue(query.value(2).toByteArray());
194 		cookie.setPath(query.value(3).toString());
195 		dt.setTime_t((uint) query.value(4).toULongLong());
196 		cookie.setExpirationDate(dt);
197 		cookie.setSecure(query.value(5).toBool());
198 		cookie.setHttpOnly(query.value(6).toBool());
199 		if ( dt < now ) {
200 			QSqlQuery delquery(db);
201 			delquery.prepare("DELETE FROM qmc2_cookies WHERE domain=:domain AND path=:path AND name=:name");
202 			delquery.bindValue(":domain", cookie.domain());
203 			delquery.bindValue(":path", cookie.path());
204 			delquery.bindValue(":name", cookie.name());
205 			if ( !delquery.exec() )
206 				qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("WARNING: failed to remove expired cookie from database: query = '%1', error = '%2'").arg(delquery.lastQuery()).arg(query.lastError().text()));
207 		} else {
208 			cookieList << cookie;
209 			cookieMap.insertMulti(domain + path, cookie);
210 		}
211 	}
212 	db.driver()->commitTransaction();
213 	return !cookieList.isEmpty();
214 }
215