Falkon Develop
Cross-platform Qt-based web browser
profilemanager.cpp
Go to the documentation of this file.
1/* ============================================================
2* Falkon - Qt web browser
3* Copyright (C) 2010-2018 David Rosca <nowrep@gmail.com>
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#include "profilemanager.h"
19#include "mainapplication.h"
20#include "datapaths.h"
21#include "updater.h"
22#include "qztools.h"
23#include "sqldatabase.h"
24#include "sitesettingsmanager.h"
25#include "settings.h"
26
27#include <QDir>
28#include <QSqlError>
29#include <QSqlQuery>
30#include <QSqlDatabase>
31#include <QMessageBox>
32#include <QSettings>
33#include <QStandardPaths>
34#include <QWebEnginePage>
35
36#include <iostream>
37
39= default;
40
42{
44
45 if (!dir.exists()) {
46 migrateFromQupZilla();
47 }
48
49 if (QFileInfo::exists(dir.filePath(QStringLiteral("profiles/profiles.ini")))) {
50 return;
51 }
52
53 std::cout << "Falkon: Creating new profile directory" << std::endl;
54
55 if (!dir.exists()) {
56 dir.mkpath(dir.absolutePath());
57 }
58
59 dir.mkdir(QStringLiteral("profiles"));
60 dir.cd(QStringLiteral("profiles"));
61
62 // $Config/profiles
63 QFile(dir.filePath(QStringLiteral("profiles.ini"))).remove();
64 QFile(QStringLiteral(":data/profiles.ini")).copy(dir.filePath(QStringLiteral("profiles.ini")));
65 QFile(dir.filePath(QStringLiteral("profiles.ini"))).setPermissions(QFile::ReadUser | QFile::WriteUser);
66
67 dir.mkdir(QStringLiteral("default"));
68 dir.cd(QStringLiteral("default"));
69
70 // $Config/profiles/default
71 QFile(QStringLiteral(":data/bookmarks.json")).copy(dir.filePath(QStringLiteral("bookmarks.json")));
72 QFile(dir.filePath(QStringLiteral("bookmarks.json"))).setPermissions(QFile::ReadUser | QFile::WriteUser);
73
74 QFile versionFile(dir.filePath(QStringLiteral("version")));
75 versionFile.open(QFile::WriteOnly);
76 versionFile.write(Qz::VERSION);
77 versionFile.close();
78}
79
80void ProfileManager::initCurrentProfile(const QString &profileName)
81{
82 QString profilePath = DataPaths::path(DataPaths::Profiles) + QLatin1Char('/');
83
84 if (profileName.isEmpty()) {
85 profilePath.append(startingProfile());
86 }
87 else {
88 profilePath.append(profileName);
89 }
90
92
93 updateCurrentProfile();
94 connectDatabase();
95 updateDatabase();
96}
97
98int ProfileManager::createProfile(const QString &profileName)
99{
101
102 if (QDir(dir.absolutePath() + QLatin1Char('/') + profileName).exists()) {
103 return -1;
104 }
105 if (!dir.mkdir(profileName)) {
106 return -2;
107 }
108
109 dir.cd(profileName);
110
111 QFile versionFile(dir.filePath(QStringLiteral("version")));
112 versionFile.open(QFile::WriteOnly);
113 versionFile.write(Qz::VERSION);
114 versionFile.close();
115
116 return 0;
117}
118
119bool ProfileManager::removeProfile(const QString &profileName)
120{
121 QDir dir(DataPaths::path(DataPaths::Profiles) + QLatin1Char('/') + profileName);
122
123 if (!dir.exists()) {
124 return false;
125 }
126
127 QzTools::removeRecursively(dir.absolutePath());
128 return true;
129}
130
131// static
133{
134 QString path = DataPaths::currentProfilePath();
135 return path.mid(path.lastIndexOf(QLatin1Char('/')) + 1);
136}
137
138// static
140{
141 QSettings settings(DataPaths::path(DataPaths::Profiles) + QLatin1String("/profiles.ini"), QSettings::IniFormat);
142 return settings.value(QStringLiteral("Profiles/startProfile"), QLatin1String("default")).toString();
143}
144
145// static
146void ProfileManager::setStartingProfile(const QString &profileName)
147{
148 QSettings settings(DataPaths::path(DataPaths::Profiles) + QLatin1String("/profiles.ini"), QSettings::IniFormat);
149 settings.setValue(QStringLiteral("Profiles/startProfile"), profileName);
150}
151
152// static
154{
156 return dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
157}
158
159void ProfileManager::updateCurrentProfile()
160{
161 QDir profileDir(DataPaths::currentProfilePath());
162
163 if (!profileDir.exists()) {
164 QDir newDir(profileDir.path().remove(profileDir.dirName()));
165 newDir.mkdir(profileDir.dirName());
166 }
167
168 QFile versionFile(profileDir.filePath(QStringLiteral("version")));
169
170 // If file exists, just update the profile to current version
171 if (versionFile.exists()) {
172 versionFile.open(QFile::ReadOnly);
173 profileVersion = QString::fromUtf8(versionFile.readAll()).trimmed();
174 versionFile.close();
175
176 updateProfile(QString::fromLatin1(Qz::VERSION), profileVersion);
177 }
178 else {
179 copyDataToProfile();
180 }
181
182 versionFile.open(QFile::WriteOnly);
183 versionFile.write(Qz::VERSION);
184 versionFile.close();
185}
186
187void ProfileManager::updateProfile(const QString &current, const QString &profile)
188{
189 if (current == profile) {
190 return;
191 }
192
193 Updater::Version prof(profile);
194
195 // Profile is from newer version than running application
196 if (prof > Updater::Version(QString::fromLatin1(Qz::VERSION))) {
197 // Only copy data when profile is not from development version
198 if (prof.revisionNumber != 99) {
199 copyDataToProfile();
200 }
201 return;
202 }
203
204 if (prof < Updater::Version(QStringLiteral("1.9.0"))) {
205 std::cout << "Falkon: Using profile from QupZilla " << qPrintable(profile) << " is not supported!" << std::endl;
206 return;
207 }
208
209 // No change in 2.0
210 if (prof < Updater::Version(QStringLiteral("2.9.99"))) {
211 return;
212 }
213
214 // No change in 3.0
215 if (prof < Updater::Version(QStringLiteral("3.0.99"))) {
216 return;
217 }
218
219 // No change in 3.1
220 if (prof < Updater::Version(QStringLiteral("3.1.99"))) {
221 return;
222 }
223}
224
225void ProfileManager::copyDataToProfile()
226{
227 QDir profileDir(DataPaths::currentProfilePath());
228
229 QFile browseData(profileDir.filePath(QStringLiteral("browsedata.db")));
230
231 if (browseData.exists()) {
232 const QString browseDataBackup = QzTools::ensureUniqueFilename(profileDir.filePath(QStringLiteral("browsedata-backup.db")));
233 browseData.copy(browseDataBackup);
234 browseData.remove();
235
236 QFile settings(profileDir.filePath(QSL("settings.ini")));
237 if (settings.exists()) {
238 const QString settingsBackup = QzTools::ensureUniqueFilename(profileDir.filePath(QSL("settings-backup.ini")));
239 settings.copy(settingsBackup);
240 settings.remove();
241 }
242
243 QFile sessionFile(profileDir.filePath(QSL("session.dat")));
244 if (sessionFile.exists()) {
245 QString oldVersion = QzTools::readAllFileContents(profileDir.filePath(QSL("version"))).trimmed();
246 if (oldVersion.isEmpty()) {
247 oldVersion = QSL("unknown-version");
248 }
249 const QString sessionBackup = QzTools::ensureUniqueFilename(profileDir.filePath(QSL("sessions/backup-%1.dat").arg(oldVersion)));
250 sessionFile.copy(sessionBackup);
251 sessionFile.remove();
252 }
253
254 const QString text = QSL("Incompatible profile version has been detected. To avoid losing your profile data, they were "
255 "backed up in following file:<br/><br/><b>") + browseDataBackup + QSL("<br/></b>");
256 QMessageBox::warning(nullptr, QStringLiteral("Falkon: Incompatible profile version"), text);
257 }
258}
259
260void ProfileManager::migrateFromQupZilla()
261{
262 if (mApp->isPortable()) {
263 return;
264 }
265
266#if defined(Q_OS_WIN)
267 const QString qzConfig = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QL1S("/qupzilla");
268#elif defined(Q_OS_MACOS)
269 const QString qzConfig = QDir::homePath() + QLatin1String("/Library/Application Support/QupZilla");
270#else // Unix
271 const QString qzConfig = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QL1S("/qupzilla");
272#endif
273
274 if (!QFileInfo::exists(qzConfig)) {
275 return;
276 }
277
278 std::cout << "Falkon: Migrating config from QupZilla..." << std::endl;
279
281}
282
283void ProfileManager::connectDatabase()
284{
285 QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"));
286 if (!db.isValid()) {
287 qCritical() << "Qt sqlite database driver is missing! Continuing without database....";
288 return;
289 }
290
291 if (mApp->isPrivate()) {
292 db.setConnectOptions(QStringLiteral("QSQLITE_OPEN_READONLY"));
293 }
294
295 db.setDatabaseName(DataPaths::currentProfilePath() + QLatin1String("/browsedata.db"));
296
297 if (!db.open()) {
298 qCritical() << "Cannot open SQLite database! Continuing without database....";
299 return;
300 }
301
302 if (db.tables().isEmpty()) {
303 const QStringList statements = QzTools::readAllFileContents(QSL(":/data/browsedata.sql")).split(QL1C(';'));
304 for (const QString &statement : statements) {
305 const QString stmt = statement.trimmed();
306 if (stmt.isEmpty()) {
307 continue;
308 }
309 QSqlQuery query;
310 if (!query.exec(stmt)) {
311 qCritical() << "Error creating database schema" << query.lastError().text();
312 }
313 }
314 }
315
317}
318
319void ProfileManager::updateDatabase()
320{
321 if (QString::fromLatin1(Qz::VERSION) == profileVersion) {
322 return;
323 }
324
325 Updater::Version prof(profileVersion);
326
327 /* Profile is from newer version than running application */
328 if (prof > Updater::Version(QString::fromLatin1(Qz::VERSION))) {
329 // Ignore
330 return;
331 }
332
333 /* Do not try to update database of too old profile */
334 if (prof < Updater::Version(QStringLiteral("1.9.0"))) {
335 std::cout << "Falkon: Using profile from QupZilla " << qPrintable(profileVersion) << " is not supported!" << std::endl;
336 return;
337 }
338
339 /* Update in 24.08.00 */
340 if (prof < Updater::Version(QStringLiteral("24.07.70"))) {
341 std::cout << "Falkon: Updating database to version " << qPrintable(QString::fromLatin1(Qz::VERSION)) << std::endl;
342
343 SqlDatabase::instance()->database().transaction();
344
345 QSqlQuery query(SqlDatabase::instance()->database());
346 query.prepare(QStringLiteral(
347 "CREATE TABLE IF NOT EXISTS site_settings ("
348 "id INTEGER PRIMARY KEY,"
349 "server TEXT NOT NULL,"
350
351 "zoom_level INTEGER DEFAULT -1,"
352 "allow_cookies INTEGER DEFAULT 0,"
353
354 "wa_autoload_images INTEGER DEFAULT 0,"
355 "wa_js_enabled INTEGER DEFAULT 0,"
356 "wa_js_open_windows INTEGER DEFAULT 0,"
357 "wa_js_access_clipboard INTEGER DEFAULT 0,"
358 "wa_js_can_paste INTEGER DEFAULT 0,"
359 "wa_js_window_activation INTEGER DEFAULT 0,"
360 "wa_local_storage INTEGER DEFAULT 0,"
361 "wa_fullscreen_support INTEGER DEFAULT 0,"
362 "wa_run_insecure_content INTEGER DEFAULT 0,"
363 "wa_playback_needs_gesture INTEGER DEFAULT 0,"
364 "wa_reading_from_canvas INTEGER DEFAULT 0,"
365 "wa_force_dark_mode INTEGER DEFAULT 0,"
366
367 "f_notifications INTEGER DEFAULT 0,"
368 "f_geolocation INTEGER DEFAULT 0,"
369 "f_media_audio_capture INTEGER DEFAULT 0,"
370 "f_media_video_capture INTEGER DEFAULT 0,"
371 "f_media_audio_video_capture INTEGER DEFAULT 0,"
372 "f_mouse_lock INTEGER DEFAULT 0,"
373 "f_desktop_video_capture INTEGER DEFAULT 0,"
374 "f_desktop_audio_video_capture INTEGER DEFAULT 0"
375 ");"
376 ));
377
378 if (!query.exec()) {
379 qCritical() << "Error while creating table 'site_settings' in database: " << query.lastError().text();
380 qFatal("ProfileManager::updateDatabase Unable to create table 'site_settings' in the database!");
381 }
382
383 query.prepare(QStringLiteral(
384 "CREATE UNIQUE INDEX IF NOT EXISTS site_settings_server_uniqueindex ON site_settings (server);"
385 ));
386
387 if (!query.exec()) {
388 qFatal() << "Error while creating unique index for table 'site_settings': " << query.lastError().text();
389 }
390
391 const QHash<QWebEnginePage::Feature, QString> html5SettingPairs = {
392 {QWebEnginePage::Notifications, QSL("Notifications")},
393 {QWebEnginePage::Geolocation, QSL("Geolocation")},
394 {QWebEnginePage::MediaAudioCapture, QSL("MediaAudioCapture")},
395 {QWebEnginePage::MediaVideoCapture, QSL("MediaVideoCapture")},
396 {QWebEnginePage::MediaAudioVideoCapture, QSL("MediaAudioVideoCapture")},
397 {QWebEnginePage::MouseLock, QSL("MouseLock")},
398 {QWebEnginePage::DesktopVideoCapture, QSL("DesktopVideoCapture")},
399 {QWebEnginePage::DesktopAudioVideoCapture,QSL("DesktopAudioVideoCapture")}
400 };
401 QHash<QString, SiteSettingsManager::SiteSettings> siteSettings;
402
403 /* Copied from mainapplication.cpp */
404 Settings::createSettings(DataPaths::currentProfilePath() + QLatin1String("/settings.ini"));
405 /* End of copied code */
406
407 Settings settings;
408
409 /* HTML5 permissions */
410 settings.beginGroup(QSL("HTML5Notifications"));
411
412 auto loadHtml5Settings = [&](const QString &suflix, const SiteSettingsManager::Permission permission) {
413 for (auto [feature, settingName] : html5SettingPairs.asKeyValueRange()) {
414 auto const serverList = settings.value(settingName + suflix, QStringList()).toStringList();
415
416 for (const auto &server : serverList) {
417 if (!siteSettings.contains(server)) {
418 siteSettings[server] = SiteSettingsManager::SiteSettings();
419 for (auto [f, nameUnused] : html5SettingPairs.asKeyValueRange()) {
420 siteSettings[server].features[f] = SiteSettingsManager::Default;
421 }
422 }
423
424 siteSettings[server].server = server;
425 siteSettings[server].features[feature] = permission;
426 }
427 }
428 };
429
430 loadHtml5Settings(QSL("Granted"), SiteSettingsManager::Allow);
431 loadHtml5Settings(QSL("Denied"), SiteSettingsManager::Deny);
432
433 settings.endGroup();
434
435 /* Cookies white/black lists */
436 settings.beginGroup(QSL("Cookie-Settings"));
437
438 auto loadCookiesSettings = [&](const QString &listName, const SiteSettingsManager::Permission permission) {
439 auto const serverList = settings.value(listName, QStringList()).toStringList();
440
441 for (const auto &server : serverList) {
442 if (!siteSettings.contains(server)) {
443 siteSettings[server] = SiteSettingsManager::SiteSettings();
444 }
445
446 siteSettings[server].server = server;
447 siteSettings[server].AllowCookies = permission;
448 }
449 };
450
451 loadCookiesSettings(QSL("whitelist"), SiteSettingsManager::Allow);
452 loadCookiesSettings(QSL("blacklist"), SiteSettingsManager::Deny);
453
454 settings.endGroup();
455
456 /* Insert SQL for SiteSettings */
457 query.prepare(QSL(
458 "INSERT INTO site_settings ("
459 "server,"
460 "allow_cookies,"
461 "f_notifications,"
462 "f_geolocation,"
463 "f_media_audio_capture,"
464 "f_media_video_capture,"
465 "f_media_audio_video_capture,"
466 "f_mouse_lock,"
467 "f_desktop_video_capture,"
468 "f_desktop_audio_video_capture"
469 ")"
470 "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
471 ));
472
473 for (const auto &siteSetting : std::as_const(siteSettings)) {
474 query.bindValue(0, siteSetting.server);
475 query.bindValue(1, siteSetting.AllowCookies);
476 query.bindValue(2, siteSetting.features[QWebEnginePage::Notifications]);
477 query.bindValue(3, siteSetting.features[QWebEnginePage::Geolocation]);
478 query.bindValue(4, siteSetting.features[QWebEnginePage::MediaAudioCapture]);
479 query.bindValue(5, siteSetting.features[QWebEnginePage::MediaVideoCapture]);
480 query.bindValue(6, siteSetting.features[QWebEnginePage::MediaAudioVideoCapture]);
481 query.bindValue(7, siteSetting.features[QWebEnginePage::MouseLock]);
482 query.bindValue(8, siteSetting.features[QWebEnginePage::DesktopVideoCapture]);
483 query.bindValue(9, siteSetting.features[QWebEnginePage::DesktopAudioVideoCapture]);
484
485 query.exec();
486 }
487
488 if (!SqlDatabase::instance()->database().commit()) {
489 SqlDatabase::instance()->database().rollback();
490
491 qFatal() << "Unable to update database.";
492 }
493 }
494}
static void setCurrentProfilePath(const QString &profilePath)
Definition: datapaths.cpp:40
@ Profiles
Definition: datapaths.h:35
static QString path(Path type)
Definition: datapaths.cpp:66
static QString currentProfilePath()
Definition: datapaths.cpp:95
void initCurrentProfile(const QString &profileName)
static QString startingProfile()
static QString currentProfile()
static int createProfile(const QString &profileName)
static QStringList availableProfiles()
static bool removeProfile(const QString &profileName)
static void setStartingProfile(const QString &profileName)
static QString ensureUniqueFilename(const QString &name, const QString &appendFormat=QSL("(%1)"))
Definition: qztools.cpp:257
static QString readAllFileContents(const QString &filename)
Definition: qztools.cpp:98
static bool copyRecursively(const QString &sourcePath, const QString &targetPath)
Definition: qztools.cpp:167
static bool removeRecursively(const QString &filePath)
Definition: qztools.cpp:139
void beginGroup(const QString &prefix)
Definition: settings.cpp:79
void endGroup()
Definition: settings.cpp:84
QVariant value(const QString &key, const QVariant &defaultValue=QVariant())
Definition: settings.cpp:74
static void createSettings(const QString &fileName)
Definition: settings.cpp:35
static SqlDatabase * instance()
void setDatabase(const QSqlDatabase &database)
QSqlDatabase database()
#define mApp
FALKON_EXPORT const char * VERSION
Definition: qzcommon.cpp:26
#define QL1S(x)
Definition: qzcommon.h:44
#define QL1C(x)
Definition: qzcommon.h:48
#define QSL(x)
Definition: qzcommon.h:40
SiteSettingsManager::SiteSettings SiteSettings