aboutsummaryrefslogtreecommitdiff
path: root/lib/webengine
diff options
context:
space:
mode:
Diffstat (limited to 'lib/webengine')
-rw-r--r--lib/webengine/CMakeLists.txt22
-rw-r--r--lib/webengine/test/form.html10
-rw-r--r--lib/webengine/test/icon.svg7
-rw-r--r--lib/webengine/test/profile.cpp125
-rw-r--r--lib/webengine/test/profilemanager.cpp120
-rw-r--r--lib/webengine/test/sample.html7
-rw-r--r--lib/webengine/test/testing.profile8
-rw-r--r--lib/webengine/test/view.cpp92
-rw-r--r--lib/webengine/urlinterceptor.cpp32
-rw-r--r--lib/webengine/urlinterceptor.h31
-rw-r--r--lib/webengine/webpage.cpp127
-rw-r--r--lib/webengine/webpage.h30
-rw-r--r--lib/webengine/webprofile.cpp138
-rw-r--r--lib/webengine/webprofile.h242
-rw-r--r--lib/webengine/webprofilemanager.cpp83
-rw-r--r--lib/webengine/webprofilemanager.h126
-rw-r--r--lib/webengine/webview.cpp87
-rw-r--r--lib/webengine/webview.h67
-rw-r--r--lib/webengine/webviewcontextmenu.cpp233
-rw-r--r--lib/webengine/webviewcontextmenu.h21
20 files changed, 1608 insertions, 0 deletions
diff --git a/lib/webengine/CMakeLists.txt b/lib/webengine/CMakeLists.txt
new file mode 100644
index 0000000..ba3017f
--- /dev/null
+++ b/lib/webengine/CMakeLists.txt
@@ -0,0 +1,22 @@
+add_library(webengine STATIC
+ webprofile.h webprofile.cpp webprofilemanager.h webprofilemanager.cpp
+ webpage.h webpage.cpp
+ webview.h webview.cpp webviewcontextmenu.h webviewcontextmenu.cpp
+ urlinterceptor.h urlinterceptor.cpp)
+target_include_directories(webengine PUBLIC ${CMAKE_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR})
+# autogen: required for context menu icons; TODO: move context menu to src/
+target_link_libraries(webengine PUBLIC Qt5::WebEngineWidgets autogen fmt)
+
+# tests
+add_executable(profile_test test/profile.cpp)
+target_link_libraries(profile_test PRIVATE webengine Catch2::Catch2)
+#target_sanitize(profile_test)
+
+add_executable(profilemanager_test test/profilemanager.cpp)
+target_link_libraries(profilemanager_test PRIVATE webengine Catch2::Catch2)
+#target_sanitize(profilemanager_test)
+
+add_test(NAME webengine_profile COMMAND profile_test)
+add_test(NAME webengine_profilemanager COMMAND profilemanager_test)
+set_tests_properties(webengine_profile webengine_profilemanager PROPERTIES
+ ENVIRONMENT "PROFILE=${CMAKE_CURRENT_SOURCE_DIR}/test/testing.profile")
diff --git a/lib/webengine/test/form.html b/lib/webengine/test/form.html
new file mode 100644
index 0000000..9d8b19e
--- /dev/null
+++ b/lib/webengine/test/form.html
@@ -0,0 +1,10 @@
+<html>
+
+<head>
+ <title>Form completion test</title>
+</head>
+
+<body>
+<h2>Form completion test</h2>
+</body>
+</html>
diff --git a/lib/webengine/test/icon.svg b/lib/webengine/test/icon.svg
new file mode 100644
index 0000000..a348cab
--- /dev/null
+++ b/lib/webengine/test/icon.svg
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="300" height="300" version="1.1">
+ <circle cx="150" cy="150" r="100" stroke="#000000" stroke-width="6" fill="#e60026"></circle>
+ <circle cx="150" cy="150" r="87" stroke="#000000" stroke-width="4" fill="#e5e4e2"></circle>
+ <path d="M230,150 A80,80 0 0 0 150,70 L150,150 Z" />
+ <path d="M70,150 A80,80 0 0 0 150,230 L150,150 Z" />
+</svg>
+
diff --git a/lib/webengine/test/profile.cpp b/lib/webengine/test/profile.cpp
new file mode 100644
index 0000000..ae3a4e3
--- /dev/null
+++ b/lib/webengine/test/profile.cpp
@@ -0,0 +1,125 @@
+#define CATCH_CONFIG_RUNNER
+
+// clazy:excludeall=non-pod-global-static
+
+#include "webprofilemanager.h"
+#include <QApplication>
+#include <catch2/catch.hpp>
+
+TEST_CASE("loading profile settings")
+{
+ const QString search = GENERATE(as<QString>{}, "https://search.url/t=%1", "https://duckduckgo.com/?q=%1&ia=web", "aaabbbccc");
+ const QUrl homepage = GENERATE(as<QUrl>{}, "https://homepage.net", "about:blank", "aaabbbccc");
+ const QUrl newtab = GENERATE(as<QUrl>{}, "https://newtab.net", "about:blank", "aaabbbccc");
+
+ auto *settings = WebProfile::load(QString(), search, homepage, newtab);
+
+ REQUIRE(settings != nullptr);
+ REQUIRE(settings->value("search").toString() == search);
+ REQUIRE(settings->value("homepage").toUrl() == homepage);
+ REQUIRE(settings->value("newtab").toUrl() == newtab);
+
+ delete settings;
+}
+
+SCENARIO("profile properties")
+{
+ const QString search{ "about:blank" };
+ const QUrl homepage{ "about:blank" };
+ const QUrl newtab{ "about:blank" };
+ const QString id{ "id" };
+
+ REQUIRE(qEnvironmentVariableIsSet("PROFILE"));
+
+ // create an empty settings object
+ const QString settings_path = GENERATE(as<QString>{}, QString(), qgetenv("PROFILE"));
+ auto *settings = WebProfile::load(settings_path, search, homepage, newtab);
+ // create the actual profile
+ auto *profile = WebProfile::load(id, settings, true);
+
+ REQUIRE(profile != nullptr);
+ REQUIRE(profile->isOffTheRecord());
+
+ WHEN("id constant")
+ {
+ REQUIRE(profile->getId() == id);
+ REQUIRE(profile->property("id").toString() == id);
+ }
+
+ WHEN("changing profile name")
+ {
+ const QString name = GENERATE(as<QString>{}, "a", "bb", "ccc");
+ profile->setName(name);
+ THEN("the name changes")
+ {
+ REQUIRE(profile->name() == name);
+ REQUIRE(settings->value("name").toString() == name);
+ }
+ }
+ WHEN("changing search")
+ {
+ const QString search = GENERATE(as<QString>{}, "a", "bb", "ccc");
+ profile->setSearch(search);
+ THEN("the search url changes")
+ {
+ REQUIRE(profile->search() == search);
+ REQUIRE(settings->value("search").toString() == search);
+ }
+ }
+ WHEN("changing homepage url")
+ {
+ const QUrl url = GENERATE(as<QUrl>{}, "a", "bb", "ccc");
+ profile->setHomepage(url);
+ THEN("homepage changes")
+ {
+ REQUIRE(profile->homepage() == url);
+ REQUIRE(settings->value("homepage").toUrl() == url);
+ }
+ }
+ WHEN("changing newtab url")
+ {
+ const QUrl url = GENERATE(as<QUrl>{}, "a", "bb", "ccc");
+ profile->setNewtab(url);
+ THEN("newtab changes")
+ {
+ REQUIRE(profile->newtab() == url);
+ REQUIRE(settings->value("newtab").toUrl() == url);
+ }
+ }
+
+ WHEN("changing cookies")
+ {
+ auto list = profile->cookies();
+ REQUIRE(list.isEmpty());
+ list.append(QNetworkCookie("name", "value").toRawForm());
+ profile->setCookies(list);
+ THEN("new cookie list gets applied")
+ {
+ // There is no event loop, so the signals cannot fire and update the cookie list
+ //REQUIRE(list == profile->cookies());
+ }
+ }
+
+ WHEN("changing headers")
+ {
+ QMap<QString, QVariant> headers;
+ headers.insert("Dnt", "1");
+ headers.insert("unknown", "pair");
+
+ profile->setHeaders(headers);
+ THEN("headers change")
+ {
+ REQUIRE(profile->headers() == headers);
+ }
+ }
+
+ delete settings;
+ delete profile;
+}
+
+int main(int argc, char **argv)
+{
+ QApplication app(argc, argv);
+ const auto r = Catch::Session().run(argc, argv);
+ return r;
+}
diff --git a/lib/webengine/test/profilemanager.cpp b/lib/webengine/test/profilemanager.cpp
new file mode 100644
index 0000000..8f6a34f
--- /dev/null
+++ b/lib/webengine/test/profilemanager.cpp
@@ -0,0 +1,120 @@
+#define CATCH_CONFIG_RUNNER
+
+// clazy:excludeall=non-pod-global-static
+
+#include "webprofilemanager.h"
+#include <QApplication>
+#include <catch2/catch.hpp>
+
+SCENARIO("WebProfileManager")
+{
+ const QString search{ "https://search.url/t=%1" };
+ const QUrl homepage{ "https://homepage.net" };
+ const QUrl newtab{ "https://newtab.net" };
+ const QString default_id{ "default" };
+
+ GIVEN("an empty profile list")
+ {
+ WebProfileManager<false> profiles({}, default_id, search, homepage, newtab);
+
+ REQUIRE(profiles.idList().count() == 1);
+ REQUIRE(profiles.profile(default_id) == WebProfile::defaultProfile());
+ REQUIRE(profiles.profile("not-in-list") == nullptr);
+
+ WHEN("adding a new profile")
+ {
+ const QString id{ "id" };
+ auto *settings = WebProfile::load(QString(), search, homepage, newtab);
+ auto *profile = WebProfile::load(id, settings, true);
+
+ THEN("doesn't add profile without settings")
+ {
+ profiles.add(id, profile, nullptr);
+ REQUIRE(profiles.idList().count() == 1);
+ }
+
+ THEN("doesn't add settings without profile")
+ {
+ profiles.add(id, nullptr, settings);
+ REQUIRE(profiles.idList().count() == 1);
+ }
+
+ THEN("adds new profile with settings")
+ {
+ profiles.add(id, profile, settings);
+ REQUIRE(profiles.idList().count() == 2);
+ }
+ }
+
+ WHEN("moving")
+ {
+ WebProfileManager<false> other(std::move(profiles));
+ THEN("moved has the same number of profiles")
+ {
+ REQUIRE(other.idList().count() == 1);
+ REQUIRE(other.profile(default_id) == WebProfile::defaultProfile());
+ REQUIRE(other.profile("not-in-list") == nullptr);
+ }
+ }
+ }
+
+ GIVEN("a number of profiles, default undefined")
+ {
+ REQUIRE(qEnvironmentVariableIsSet("PROFILE"));
+
+ WebProfileManager<false> profiles(QString::fromLatin1(qgetenv("PROFILE")).split(';'), default_id, search, homepage, newtab);
+
+ REQUIRE(profiles.idList().count() == 2);
+ REQUIRE(profiles.profile(default_id) == WebProfile::defaultProfile());
+ REQUIRE(profiles.profile("testing") != nullptr);
+ REQUIRE(profiles.profile("not-in-list") == nullptr);
+
+ WHEN("making global")
+ {
+ profiles.make_global();
+ WebProfileManager other;
+
+ THEN("global has the same number of profiles")
+ {
+ REQUIRE(other.idList().count() == 2);
+ REQUIRE(other.profile(default_id) == WebProfile::defaultProfile());
+ REQUIRE(other.profile("testing") != nullptr);
+ REQUIRE(other.profile("not-in-list") == nullptr);
+ }
+
+ THEN("walking has no nullptrs")
+ {
+ other.walk([](const QString &, WebProfile *profile, const QSettings *settings) {
+ REQUIRE(profile != nullptr);
+ REQUIRE(settings != nullptr);
+ });
+ }
+ }
+ }
+
+ GIVEN("a number of profiles, default defined")
+ {
+ REQUIRE(qEnvironmentVariableIsSet("PROFILE"));
+
+ WebProfileManager<false> profiles(QString::fromLatin1(qgetenv("PROFILE")).split(';'), "testing", search, homepage, newtab);
+
+ REQUIRE(profiles.idList().count() == 1);
+ REQUIRE(profiles.profile("testing") == WebProfile::defaultProfile());
+ REQUIRE(profiles.profile("not-in-list") == nullptr);
+
+ WHEN("walking")
+ {
+ profiles.walk([](const QString &, WebProfile *profile, const QSettings *settings) {
+ REQUIRE(profile != nullptr);
+ REQUIRE(settings != nullptr);
+ });
+ }
+ }
+}
+
+int main(int argc, char **argv)
+{
+ QApplication app(argc, argv);
+ const auto r = Catch::Session().run(argc, argv);
+ return r;
+}
diff --git a/lib/webengine/test/sample.html b/lib/webengine/test/sample.html
new file mode 100644
index 0000000..54746d5
--- /dev/null
+++ b/lib/webengine/test/sample.html
@@ -0,0 +1,7 @@
+<html>
+<head><title>sample page</title></head>
+<body>
+ <h2><img src="icon.svg" />This is a sample page</h2>
+ <iframe width="100%" height="80%" src="form.html"></iframe>
+</body>
+</html>
diff --git a/lib/webengine/test/testing.profile b/lib/webengine/test/testing.profile
new file mode 100644
index 0000000..e345a3e
--- /dev/null
+++ b/lib/webengine/test/testing.profile
@@ -0,0 +1,8 @@
+name=Test Profile
+otr=true
+search=https://duckduckgo.com/?q=%1&ia=web
+homepage=about:blank
+newtab=about:blank
+
+[headers]
+Dnt=1
diff --git a/lib/webengine/test/view.cpp b/lib/webengine/test/view.cpp
new file mode 100644
index 0000000..8aa639a
--- /dev/null
+++ b/lib/webengine/test/view.cpp
@@ -0,0 +1,92 @@
+#define CATCH_CONFIG_RUNNER
+
+// clazy:excludeall=non-pod-global-static
+
+#include "webprofile.h"
+#include "webview.h"
+#include <QApplication>
+#include <QMainWindow>
+#include <QTimer>
+#include <QtWebEngine>
+#include <catch2/catch.hpp>
+
+SCENARIO("WebView")
+{
+ const QString profile_id{ "default" };
+ auto *settings = WebProfile::load(qgetenv("PROFILE"), "about:blank", QUrl{ "about:blank" }, QUrl{ "about:blank" });
+ auto *profile = WebProfile::load(profile_id, settings, true);
+
+ QMainWindow window;
+ auto *view = new WebView(profile, nullptr);
+ window.setCentralWidget(view);
+ window.show();
+ window.resize(800, 600);
+
+ WHEN("created")
+ {
+ THEN("using the default profile")
+ {
+ REQUIRE(view->profile() == profile);
+ }
+ THEN("serialized using default profile")
+ {
+ const auto data = view->serialize();
+ REQUIRE(data.profile == profile_id);
+ REQUIRE(data.url.isEmpty());
+ REQUIRE(!data.history.isEmpty());
+ }
+ THEN("loading a url")
+ {
+ // block until a loadFinished signal
+ QEventLoop pause;
+ QObject::connect(view, &WebView::loadFinished, &pause, &QEventLoop::quit);
+ view->load(QUrl{ qgetenv("URL") });
+ pause.exec();
+
+ REQUIRE(view->isLoaded());
+ }
+ }
+
+ WHEN("changing profiles")
+ {
+ const QString swap_profile_id{ "swap_profile" };
+ auto *swap_settings = WebProfile::load(QString(), "about:blank", QUrl{ "about:blank" }, QUrl{ "about:blank" });
+ auto *swap_profile = WebProfile::load(swap_profile_id, swap_settings, true);
+
+ view->setProfile(swap_profile);
+ THEN("using the swap profile")
+ {
+ REQUIRE(view->profile() == swap_profile);
+ }
+ THEN("serialized using swap profile")
+ {
+ const auto data = view->serialize();
+ REQUIRE(data.profile == swap_profile_id);
+ REQUIRE(data.url.isEmpty());
+ REQUIRE(!data.history.isEmpty());
+ }
+
+ view->setProfile(profile);
+ delete swap_settings;
+ delete swap_profile;
+ }
+
+ // cleanup
+ window.close();
+ delete view;
+ delete settings;
+ delete profile;
+}
+
+int main(int argc, char **argv)
+{
+ QtWebEngine::initialize();
+ QApplication app(argc, argv);
+
+ QTimer::singleShot(0, &app, [argc, argv, &app]() {
+ const auto n_failed = Catch::Session().run(argc, argv);
+ app.exit(n_failed);
+ });
+
+ return app.exec();
+}
diff --git a/lib/webengine/urlinterceptor.cpp b/lib/webengine/urlinterceptor.cpp
new file mode 100644
index 0000000..047cad4
--- /dev/null
+++ b/lib/webengine/urlinterceptor.cpp
@@ -0,0 +1,32 @@
+/*
+ * This file is part of smolbote. It's copyrighted by the contributors recorded
+ * in the version control history of the file, available from its original
+ * location: https://neueland.iserlohn-fortress.net/gitea/aqua/smolbote
+ *
+ * SPDX-License-Identifier: GPL-3.0
+ */
+
+#include "urlinterceptor.h"
+#include "webprofile.h"
+
+// test DNT on https://browserleaks.com/donottrack
+
+UrlRequestInterceptor::UrlRequestInterceptor(WebProfile* profile)
+ : QWebEngineUrlRequestInterceptor(profile)
+{
+ Q_CHECK_PTR(profile);
+ m_profile = profile;
+}
+
+void UrlRequestInterceptor::interceptRequest(QWebEngineUrlRequestInfo &info)
+{
+ for(auto *filter : qAsConst(m_profile->m_filters)) {
+ filter->interceptRequest(info);
+ }
+
+ // set headers
+ for(auto i = m_profile->m_headers.constBegin(); i != m_profile->m_headers.constEnd(); ++i) {
+ info.setHttpHeader(i.key(), i.value());
+ }
+}
+
diff --git a/lib/webengine/urlinterceptor.h b/lib/webengine/urlinterceptor.h
new file mode 100644
index 0000000..eb3ce67
--- /dev/null
+++ b/lib/webengine/urlinterceptor.h
@@ -0,0 +1,31 @@
+/*
+ * This file is part of smolbote. It's copyrighted by the contributors recorded
+ * in the version control history of the file, available from its original
+ * location: https://neueland.iserlohn-fortress.net/gitea/aqua/smolbote
+ *
+ * SPDX-License-Identifier: GPL-3.0
+ */
+
+#ifndef SMOLBOTE_URLREQUESTINTERCEPTOR_H
+#define SMOLBOTE_URLREQUESTINTERCEPTOR_H
+
+#include <QWebEngineUrlRequestInterceptor>
+
+class WebProfile;
+class UrlRequestInterceptor : public QWebEngineUrlRequestInterceptor
+{
+ friend class WebProfile;
+
+public:
+ ~UrlRequestInterceptor() override = default;
+
+ void interceptRequest(QWebEngineUrlRequestInfo &info) override;
+
+protected:
+ explicit UrlRequestInterceptor(WebProfile *profile);
+
+private:
+ WebProfile *m_profile;
+};
+
+#endif // SMOLBOTE_URLREQUESTINTERCEPTOR_H
diff --git a/lib/webengine/webpage.cpp b/lib/webengine/webpage.cpp
new file mode 100644
index 0000000..b2b19b5
--- /dev/null
+++ b/lib/webengine/webpage.cpp
@@ -0,0 +1,127 @@
+/*
+ * This file is part of smolbote. It's copyrighted by the contributors recorded
+ * in the version control history of the file, available from its original
+ * location: https://neueland.iserlohn-fortress.net/gitea/aqua/smolbote
+ *
+ * SPDX-License-Identifier: GPL-3.0
+ */
+
+#include "webpage.h"
+#include <QLayout>
+#include <QMessageBox>
+#include <QTimer>
+#include <QWebEngineFullScreenRequest>
+#include <QWebEngineCertificateError>
+
+[[nodiscard]] inline QString tr_terminationStatus(QWebEnginePage::RenderProcessTerminationStatus status)
+{
+ switch(status) {
+ case QWebEnginePage::NormalTerminationStatus:
+ return QObject::tr("The render process terminated normally.");
+ case QWebEnginePage::AbnormalTerminationStatus:
+ return QObject::tr("The render process terminated with with a non-zero exit status.");
+ case QWebEnginePage::CrashedTerminationStatus:
+ return QObject::tr("The render process crashed, for example because of a segmentation fault.");
+ case QWebEnginePage::KilledTerminationStatus:
+ return QObject::tr("The render process was killed, for example by SIGKILL or task manager kill.");
+ }
+
+ return QObject::tr("The render process was terminated with an unknown status.");
+}
+
+[[nodiscard]] inline QString feature_toString(QWebEnginePage::Feature feature)
+{
+ switch(feature) {
+ case QWebEnginePage::Notifications:
+ return QObject::tr("Notifications");
+ case QWebEnginePage::Geolocation:
+ return QObject::tr("Geolocation");
+ case QWebEnginePage::MediaAudioCapture:
+ return QObject::tr("Audio Capture");
+ case QWebEnginePage::MediaVideoCapture:
+ return QObject::tr("Video Capture");
+ case QWebEnginePage::MediaAudioVideoCapture:
+ return QObject::tr("Audio and Video Capture");
+ case QWebEnginePage::MouseLock:
+ return QObject::tr("Mouse Lock");
+ case QWebEnginePage::DesktopVideoCapture:
+ return QObject::tr("Desktop Video Capture");
+ case QWebEnginePage::DesktopAudioVideoCapture:
+ return QObject::tr("Desktop Audio and Video Capture");
+ }
+
+ return QObject::tr("Unknown feature");
+}
+
+WebPage::WebPage(QWebEngineProfile *profile, QObject *parent)
+ : QWebEnginePage(profile, parent)
+{
+ connect(this, &WebPage::fullScreenRequested, this, [](QWebEngineFullScreenRequest request) {
+ request.accept();
+ });
+
+ connect(this, &QWebEnginePage::featurePermissionRequested, this, &WebPage::featurePermissionDialog);
+ connect(this, &QWebEnginePage::renderProcessTerminated, this, &WebPage::renderProcessCrashed);
+}
+
+bool WebPage::certificateError(const QWebEngineCertificateError &certificateError)
+{
+ QMessageBox messageBox;
+
+ messageBox.setWindowTitle(tr("SSL Error"));
+ if(certificateError.isOverridable())
+ messageBox.setIcon(QMessageBox::Warning);
+ else
+ messageBox.setIcon(QMessageBox::Critical);
+
+ messageBox.setText(tr("An SSL error has occurred on <strong>%1</strong>").arg(certificateError.url().toString()));
+ messageBox.setInformativeText(tr("<p>%1</p>"
+ "<p>This error %2 be overridden.</p>")
+ .arg(certificateError.errorDescription(),
+ certificateError.isOverridable() ? tr("can") : tr("cannot")));
+ messageBox.setDetailedText(tr("Error code: %1").arg(certificateError.error()));
+
+ if(certificateError.isOverridable()) {
+ messageBox.setStandardButtons(QMessageBox::Ignore | QMessageBox::Abort);
+ messageBox.setDefaultButton(QMessageBox::Ignore);
+ } else
+ messageBox.setStandardButtons(QMessageBox::Abort);
+
+ auto resp = messageBox.exec();
+
+ return resp == QMessageBox::Ignore;
+}
+
+void WebPage::featurePermissionDialog(const QUrl &securityOrigin, QWebEnginePage::Feature feature)
+{
+ QMessageBox messageBox;
+
+ messageBox.setWindowTitle(tr("Feature permission request"));
+ messageBox.setIcon(QMessageBox::Question);
+ messageBox.setText(tr("<p>The webpage <strong>%1</strong> has requested permission to access: %2</p>"
+ "<p>Allow this feature?</p>")
+ .arg(securityOrigin.toString(), feature_toString(feature)));
+
+ messageBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
+ messageBox.setDefaultButton(QMessageBox::No);
+
+ if(messageBox.exec() == QMessageBox::Yes) {
+ setFeaturePermission(securityOrigin, feature, QWebEnginePage::PermissionGrantedByUser);
+ } else {
+ setFeaturePermission(securityOrigin, feature, QWebEnginePage::PermissionDeniedByUser);
+ }
+}
+
+void WebPage::renderProcessCrashed(QWebEnginePage::RenderProcessTerminationStatus terminationStatus, int exitCode)
+{
+ if(terminationStatus != QWebEnginePage::NormalTerminationStatus) {
+ QString page = "<html><body><h1>This tab has crashed!</h1>%message%</body></html>";
+ page.replace(QLatin1String("%message%"), QString("<p>%1<br>Exit code is %2.</p>"
+ "<p>Press <a href='%3'>here</a> to reload this tab.</p>")
+ .arg(tr_terminationStatus(terminationStatus), QString::number(exitCode), this->url().toEncoded()));
+
+ QTimer::singleShot(0, this, [this, page]() {
+ setHtml(page.toUtf8(), url());
+ });
+ }
+}
diff --git a/lib/webengine/webpage.h b/lib/webengine/webpage.h
new file mode 100644
index 0000000..91ae4f3
--- /dev/null
+++ b/lib/webengine/webpage.h
@@ -0,0 +1,30 @@
+/*
+ * This file is part of smolbote. It's copyrighted by the contributors recorded
+ * in the version control history of the file, available from its original
+ * location: https://neueland.iserlohn-fortress.net/gitea/aqua/smolbote
+ *
+ * SPDX-License-Identifier: GPL-3.0
+ */
+
+#ifndef SMOLBOTE_WEBPAGE_H
+#define SMOLBOTE_WEBPAGE_H
+
+#include <QWebEnginePage>
+
+class WebPage : public QWebEnginePage
+{
+ Q_OBJECT
+
+public:
+ WebPage(QWebEngineProfile *profile, QObject *parent = nullptr);
+ ~WebPage() override = default;
+
+protected:
+ bool certificateError(const QWebEngineCertificateError &certificateError) override;
+
+protected slots:
+ void featurePermissionDialog(const QUrl &securityOrigin, QWebEnginePage::Feature feature);
+ void renderProcessCrashed(QWebEnginePage::RenderProcessTerminationStatus terminationStatus, int exitCode);
+};
+
+#endif // SMOLBOTE_WEBPAGE_H
diff --git a/lib/webengine/webprofile.cpp b/lib/webengine/webprofile.cpp
new file mode 100644
index 0000000..f1e71fb
--- /dev/null
+++ b/lib/webengine/webprofile.cpp
@@ -0,0 +1,138 @@
+/*
+ * This file is part of smolbote. It's copyrighted by the contributors recorded
+ * in the version control history of the file, available from its original
+ * location: https://neueland.iserlohn-fortress.net/gitea/aqua/smolbote
+ *
+ * SPDX-License-Identifier: GPL-3.0
+ */
+
+#include "webprofile.h"
+#include "urlinterceptor.h"
+#include <QFileInfo>
+#include <QSettings>
+#include <QWebEngineCookieStore>
+#include <QWebEngineSettings>
+#include <spdlog/spdlog.h>
+
+static WebProfile *s_profile = nullptr;
+
+void WebProfile::setDefaultProfile(WebProfile *profile)
+{
+ s_profile = profile;
+}
+WebProfile *WebProfile::defaultProfile()
+{
+ return s_profile;
+}
+
+QSettings *WebProfile::load(const QString &path, const QString &search, const QUrl &homepage, const QUrl &newtab)
+{
+ auto *settings = new QSettings(path, QSettings::IniFormat);
+
+ if(!settings->contains("search")) {
+ settings->setValue("search", search);
+ }
+ if(!settings->contains("homepage")) {
+ settings->setValue("homepage", homepage);
+ }
+ if(!settings->contains("newtab")) {
+ settings->setValue("newtab", newtab);
+ }
+
+ return settings;
+}
+
+WebProfile *WebProfile::load(const QString &id, QSettings *settings, bool isOffTheRecord)
+{
+ WebProfile *profile = nullptr;
+
+ if(settings->value("otr", isOffTheRecord).toBool()) {
+ profile = new WebProfile(id, nullptr);
+ } else {
+ profile = new WebProfile(id, id, nullptr);
+ }
+
+ profile->m_name = settings->value("name", id).toString();
+ connect(profile, &WebProfile::nameChanged, profile, [settings](const QString &name) { settings->setValue("name", name); });
+ profile->m_search = settings->value("search", "").toString();
+ connect(profile, &WebProfile::searchChanged, settings, [settings](const QString &url) { settings->setValue("search", url); });
+ profile->m_homepage = settings->value("homepage", "").toUrl();
+ connect(profile, &WebProfile::homepageChanged, settings, [settings](const QUrl &url) { settings->setValue("homepage", url); });
+ profile->m_newtab = settings->value("newtab", "").toUrl();
+ connect(profile, &WebProfile::newtabChanged, settings, [settings](const QUrl &url) { settings->setValue("newtab", url); });
+
+ {
+ settings->beginGroup("properties");
+ const auto keys = settings->childKeys();
+ for(const QString &key : keys) {
+ profile->setProperty(qUtf8Printable(key), settings->value(key));
+ }
+ settings->endGroup(); // properties
+ connect(profile, &WebProfile::propertyChanged, [settings](const QString &property, const QVariant &value) {
+ settings->setValue("properties/" + property, value);
+ });
+ }
+ {
+ settings->beginGroup("attributes");
+ const auto keys = settings->childKeys();
+ auto *s = profile->settings();
+ for(const QString &key : keys) {
+ auto attribute = static_cast<QWebEngineSettings::WebAttribute>(key.toInt());
+ s->setAttribute(attribute, settings->value(key).toBool());
+ }
+ settings->endGroup();
+ connect(profile, &WebProfile::attributeChanged, [settings](const QWebEngineSettings::WebAttribute attr, const bool value) {
+ settings->setValue("attributes/" + QString::number(attr), value);
+ });
+ }
+ {
+ // headers
+ settings->beginGroup("headers");
+ const auto keys = settings->childKeys();
+ for(const QString &key : keys) {
+ profile->setHttpHeader(key.toLatin1(), settings->value(key).toString().toLatin1());
+ }
+ settings->endGroup();
+ connect(profile, &WebProfile::headerChanged, [settings](const QString &name, const QString &value) {
+ settings->setValue("headers/" + name, value);
+ });
+ connect(profile, &WebProfile::headerRemoved, [settings](const QString &name) {
+ settings->remove("headers/" + name);
+ });
+ }
+ return profile;
+}
+
+// off-the-record constructor
+WebProfile::WebProfile(const QString &id, QObject *parent)
+ : QWebEngineProfile(parent)
+ , m_id(id)
+{
+ QWebEngineProfile::setUrlRequestInterceptor(new UrlRequestInterceptor(this));
+ connect(this->cookieStore(), &QWebEngineCookieStore::cookieAdded, this, [this](const QNetworkCookie &cookie) {
+ spdlog::debug("[{}]: +cookie {}", qUtf8Printable(m_name), qUtf8Printable(cookie.name()));
+ m_cookies.append(cookie);
+ });
+ connect(this->cookieStore(), &QWebEngineCookieStore::cookieRemoved, this, [this](const QNetworkCookie &cookie) {
+ spdlog::debug("[{}]: -cookie {}", qUtf8Printable(m_name), qUtf8Printable(cookie.name()));
+ m_cookies.removeOne(cookie);
+ });
+ cookieStore()->loadAllCookies();
+}
+
+// default constructor
+WebProfile::WebProfile(const QString &id, const QString &storageName, QObject *parent)
+ : QWebEngineProfile(storageName, parent)
+ , m_id(id)
+{
+ QWebEngineProfile::setUrlRequestInterceptor(new UrlRequestInterceptor(this));
+ connect(this->cookieStore(), &QWebEngineCookieStore::cookieAdded, this, [this](const QNetworkCookie &cookie) {
+ spdlog::debug("[{}]: +cookie {}", qUtf8Printable(m_name), qUtf8Printable(cookie.name()));
+ m_cookies.append(cookie);
+ });
+ connect(this->cookieStore(), &QWebEngineCookieStore::cookieRemoved, this, [this](const QNetworkCookie &cookie) {
+ spdlog::debug("[{}]: -cookie {}", qUtf8Printable(m_name), qUtf8Printable(cookie.name()));
+ m_cookies.removeOne(cookie);
+ });
+ cookieStore()->loadAllCookies();
+}
diff --git a/lib/webengine/webprofile.h b/lib/webengine/webprofile.h
new file mode 100644
index 0000000..894463f
--- /dev/null
+++ b/lib/webengine/webprofile.h
@@ -0,0 +1,242 @@
+/*
+ * This file is part of smolbote. It's copyrighted by the contributors recorded
+ * in the version control history of the file, available from its original
+ * location: https://neueland.iserlohn-fortress.net/gitea/aqua/smolbote
+ *
+ * SPDX-License-Identifier: GPL-3.0
+ */
+
+#ifndef SMOLBOTE_WEBENGINEPROFILE_H
+#define SMOLBOTE_WEBENGINEPROFILE_H
+
+#include <QMap>
+#include <QNetworkCookie>
+#include <QSettings>
+#include <QString>
+#include <QUrl>
+#include <QVariant>
+#include <QVector>
+#include <QWebEngineCookieStore>
+#include <QWebEngineProfile>
+#include <QWebEngineSettings>
+
+class UrlRequestInterceptor;
+class WebProfile : public QWebEngineProfile
+{
+ friend class UrlRequestInterceptor;
+
+ Q_OBJECT
+
+ Q_PROPERTY(QString id READ getId CONSTANT)
+ Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
+ Q_PROPERTY(QString search READ search WRITE setSearch NOTIFY searchChanged)
+ Q_PROPERTY(QUrl homepage READ homepage WRITE setHomepage NOTIFY homepageChanged)
+ Q_PROPERTY(QUrl newtab READ newtab WRITE setNewtab NOTIFY newtabChanged)
+
+ // QWebEngineProfile should-be properties
+ Q_PROPERTY(QString cachePath READ cachePath WRITE setCachePath NOTIFY propertyChanged)
+ Q_PROPERTY(QString persistentStoragePath READ persistentStoragePath WRITE setPersistentStoragePath NOTIFY propertyChanged)
+ Q_PROPERTY(int persistentCookiesPolicy READ persistentCookiesPolicy WRITE setPersistentCookiesPolicy NOTIFY propertyChanged)
+
+ Q_PROPERTY(QString httpAcceptLanguage READ httpAcceptLanguage WRITE setHttpAcceptLanguage NOTIFY propertyChanged)
+ Q_PROPERTY(int httpCacheMaximumSize READ httpCacheMaximumSize WRITE setHttpCacheMaximumSize NOTIFY propertyChanged)
+ Q_PROPERTY(int httpCacheType READ httpCacheType WRITE setHttpCacheType NOTIFY propertyChanged)
+ Q_PROPERTY(QString httpUserAgent READ httpUserAgent WRITE setHttpUserAgent NOTIFY propertyChanged)
+
+ Q_PROPERTY(bool spellCheckEnabled READ isSpellCheckEnabled WRITE setSpellCheckEnabled NOTIFY propertyChanged)
+
+ // more custom properties
+ Q_PROPERTY(QList<QVariant> cookies READ cookies WRITE setCookies NOTIFY cookiesChanged)
+ Q_PROPERTY(QMap<QString, QVariant> headers READ headers WRITE setHeaders NOTIFY headersChanged)
+
+signals:
+ void nameChanged(const QString &name);
+ void searchChanged(const QString &url);
+ void homepageChanged(const QUrl &url);
+ void newtabChanged(const QUrl &url);
+
+ void propertyChanged(const QString &name, const QVariant &value);
+ void attributeChanged(QWebEngineSettings::WebAttribute attribute, bool value);
+
+ void cookiesChanged();
+ void headersChanged();
+ void headerChanged(const QString &name, const QString &value);
+ void headerRemoved(const QString &name);
+
+public:
+ [[nodiscard]] static QSettings *load(const QString &path, const QString &search = QString(), const QUrl &homepage = QUrl(), const QUrl &newtab = QUrl());
+ [[nodiscard]] static WebProfile *load(const QString &id, QSettings *settings, bool isOffTheRecord = true);
+
+ WebProfile(const WebProfile &) = delete;
+ WebProfile &operator=(const WebProfile &) = delete;
+ WebProfile(WebProfile &&) = delete;
+ WebProfile &operator=(WebProfile &&) = delete;
+
+ static WebProfile *defaultProfile();
+ static void setDefaultProfile(WebProfile *profile);
+
+ ~WebProfile() override = default;
+
+ [[nodiscard]] QString getId() const
+ {
+ return m_id;
+ }
+ [[nodiscard]] QString name() const
+ {
+ return m_name;
+ }
+ void setName(const QString &name)
+ {
+ m_name = name;
+ emit nameChanged(name);
+ }
+
+ [[nodiscard]] QList<QVariant> cookies() const
+ {
+ QList<QVariant> r;
+ for(const auto &cookie : m_cookies) {
+ r.append(cookie.toRawForm());
+ }
+ return r;
+ }
+
+ [[nodiscard]] QString search() const
+ {
+ return m_search;
+ }
+ void setSearch(const QString &url)
+ {
+ m_search = url;
+ emit searchChanged(m_search);
+ }
+
+ [[nodiscard]] QUrl homepage() const
+ {
+ return m_homepage;
+ }
+ void setHomepage(const QUrl &url)
+ {
+ m_homepage = url;
+ emit homepageChanged(m_homepage);
+ }
+
+ [[nodiscard]] QUrl newtab() const
+ {
+ return m_newtab;
+ }
+ void setNewtab(const QUrl &url)
+ {
+ m_newtab = url;
+ emit newtabChanged(m_newtab);
+ }
+
+ void setCachePath(const QString &path)
+ {
+ QWebEngineProfile::setCachePath(path);
+ emit propertyChanged("cachePath", path);
+ }
+ void setPersistentStoragePath(const QString &path)
+ {
+ QWebEngineProfile::setPersistentStoragePath(path);
+ emit propertyChanged("persistentStoragePath", path);
+ }
+ void setPersistentCookiesPolicy(int policy)
+ {
+ QWebEngineProfile::setPersistentCookiesPolicy(static_cast<QWebEngineProfile::PersistentCookiesPolicy>(policy));
+ emit propertyChanged("persistentCookiesPolicy", policy);
+ }
+
+ void setHttpAcceptLanguage(const QString &httpAcceptLanguage)
+ {
+ QWebEngineProfile::setHttpAcceptLanguage(httpAcceptLanguage);
+ emit propertyChanged("httpAcceptLanguage", httpAcceptLanguage);
+ }
+ void setHttpCacheMaximumSize(int maxSize)
+ {
+ QWebEngineProfile::setHttpCacheMaximumSize(maxSize);
+ emit propertyChanged("httpCacheMaximumSize", maxSize);
+ }
+ void setHttpCacheType(int type)
+ {
+ QWebEngineProfile::setHttpCacheType(static_cast<QWebEngineProfile::HttpCacheType>(type));
+ emit propertyChanged("httpCacheType", type);
+ }
+ void setHttpUserAgent(const QString &userAgent)
+ {
+ QWebEngineProfile::setHttpUserAgent(userAgent);
+ emit propertyChanged("httpUserAgent", userAgent);
+ }
+ void setHttpHeader(const QString &name, const QString &value)
+ {
+ m_headers[name.toLatin1()] = value.toLatin1();
+ emit headerChanged(name, value);
+ }
+ void removeHttpHeader(const QString &name)
+ {
+ if(m_headers.contains(name.toLatin1())) {
+ m_headers.remove(name.toLatin1());
+ emit headerRemoved(name);
+ }
+ }
+ [[nodiscard]] QMap<QString, QVariant> headers() const
+ {
+ QMap<QString, QVariant> r;
+ auto it = m_headers.constBegin();
+ while(it != m_headers.constEnd()) {
+ r.insert(QString(it.key()), QVariant(it.value()));
+ ++it;
+ }
+ return r;
+ }
+ void setSpellCheckEnabled(bool enable)
+ {
+ QWebEngineProfile::setSpellCheckEnabled(enable);
+ emit propertyChanged("spellCheckEnabed", enable);
+ }
+
+ void setUrlRequestInterceptor(QWebEngineUrlRequestInterceptor *interceptor)
+ {
+ m_filters.append(interceptor);
+ }
+
+public slots:
+ void setCookies(const QList<QVariant> &cookies)
+ {
+ auto *store = cookieStore();
+ store->deleteAllCookies();
+ for(const auto &data : cookies) {
+ for(auto &cookie : QNetworkCookie::parseCookies(data.toByteArray())) {
+ store->setCookie(cookie);
+ }
+ }
+ emit cookiesChanged();
+ }
+ void setHeaders(const QMap<QString, QVariant> &headers)
+ {
+ m_headers.clear();
+ auto it = headers.constBegin();
+ while(it != headers.constEnd()) {
+ m_headers.insert(it.key().toLatin1(), it.value().toByteArray());
+ ++it;
+ }
+ emit headersChanged();
+ }
+
+protected:
+ // off-the-record constructor
+ explicit WebProfile(const QString &id, QObject *parent = nullptr);
+ // default constructor
+ explicit WebProfile(const QString &id, const QString &storageName, QObject *parent = nullptr);
+
+ const QString m_id;
+ QString m_name;
+ QString m_search = QString("about:blank");
+ QUrl m_homepage = QUrl("about:blank");
+ QUrl m_newtab = QUrl("about:blank");
+
+ QVector<QWebEngineUrlRequestInterceptor *> m_filters;
+ QList<QNetworkCookie> m_cookies;
+ QMap<QByteArray, QByteArray> m_headers;
+};
+
+#endif // SMOLBOTE_WEBENGINEPROFILE_H
diff --git a/lib/webengine/webprofilemanager.cpp b/lib/webengine/webprofilemanager.cpp
new file mode 100644
index 0000000..5cc83f8
--- /dev/null
+++ b/lib/webengine/webprofilemanager.cpp
@@ -0,0 +1,83 @@
+/*
+ * This file is part of smolbote. It's copyrighted by the contributors recorded
+ * in the version control history of the file, available from its original
+ * location: https://neueland.iserlohn-fortress.net/gitea/aqua/smolbote
+ *
+ * SPDX-License-Identifier: GPL-3.0
+ */
+
+#include "webprofilemanager.h"
+#include "webprofile.h"
+
+static WebProfileManager<false> *s_instance = nullptr;
+
+template <>
+void WebProfileManager<false>::make_global()
+{
+ if(s_instance == nullptr) {
+ s_instance = this;
+ }
+}
+
+template <>
+WebProfileManager<true>::~WebProfileManager() = default;
+
+template <>
+WebProfileManager<false>::~WebProfileManager()
+{
+ for(Profile p : qAsConst(profiles)) {
+ if(p.selfDestruct && p.settings != nullptr) {
+ if(!p.ptr->isOffTheRecord()) {
+ if(!p.ptr->persistentStoragePath().isEmpty())
+ QDir(p.ptr->persistentStoragePath()).removeRecursively();
+ if(!p.ptr->cachePath().isEmpty())
+ QDir(p.ptr->cachePath()).removeRecursively();
+ }
+ const QString filename = p.settings->fileName();
+ delete p.settings;
+ QFile::remove(filename);
+ } else if(p.settings != nullptr) {
+ p.settings->sync();
+ delete p.settings;
+ }
+ }
+}
+
+template <>
+WebProfile *WebProfileManager<true>::profile(const QString &id) const
+{
+ return s_instance->profile(id);
+}
+
+template <>
+QStringList WebProfileManager<true>::idList() const
+{
+ return s_instance->idList();
+}
+
+template <>
+void WebProfileManager<false>::walk(std::function<void(const QString &id, WebProfile *profile, QSettings *settings)> f) const
+{
+ for(auto iter = profiles.begin(); iter != profiles.end(); ++iter) {
+ f(iter.key(), iter.value().ptr, iter.value().settings);
+ }
+}
+
+template <>
+void WebProfileManager<true>::walk(std::function<void(const QString &id, WebProfile *profile, QSettings *settings)> f) const
+{
+ s_instance->walk(f);
+}
+
+void profileMenu(QMenu *menu, const std::function<void(WebProfile *)> &callback, WebProfile *current, bool checkable)
+{
+ auto *group = new QActionGroup(menu);
+ QObject::connect(menu, &QMenu::aboutToHide, group, &QActionGroup::deleteLater);
+
+ s_instance->walk([=](const QString &, WebProfile *profile, const QSettings *) {
+ auto *action = menu->addAction(profile->name(), profile, [=]() { callback(profile); });
+ action->setCheckable(checkable);
+ action->setChecked(profile == current);
+ group->addAction(action);
+ });
+}
diff --git a/lib/webengine/webprofilemanager.h b/lib/webengine/webprofilemanager.h
new file mode 100644
index 0000000..e5df6d5
--- /dev/null
+++ b/lib/webengine/webprofilemanager.h
@@ -0,0 +1,126 @@
+/*
+ * This file is part of smolbote. It's copyrighted by the contributors recorded
+ * in the version control history of the file, available from its original
+ * location: https://neueland.iserlohn-fortress.net/gitea/aqua/smolbote
+ *
+ * SPDX-License-Identifier: GPL-3.0
+ */
+
+#ifndef SMOLBOTE_WEBPROFILEMANAGER_H
+#define SMOLBOTE_WEBPROFILEMANAGER_H
+
+#include "webprofile.h"
+#include <QDir>
+#include <QFile>
+#include <QFileInfo>
+#include <QMap>
+#include <QMenu>
+#include <functional>
+
+#if defined(__clang__)
+#define consumable(X) [[clang::consumable(X)]]
+#define return_typestate(X) [[clang::return_typestate(X)]]
+#define set_typestate(X) [[clang::set_typestate(X)]]
+#define callable_when(X) [[clang::callable_when(X)]]
+#define param_typestate(X) [[clang::param_typestate(X)]]
+#else
+#define consumable(X)
+#define return_typestate(X)
+#define set_typestate(X)
+#define callable_when(X)
+#define param_typestate(X)
+#endif
+
+void profileMenu(QMenu *menu, const std::function<void(WebProfile *)> &callback, WebProfile *current = nullptr, bool checkable = false);
+
+template <bool use_global = true>
+class consumable(unconsumed) WebProfileManager
+{
+public:
+ return_typestate(unconsumed) WebProfileManager()
+ {
+ static_assert(use_global);
+ }
+ return_typestate(unconsumed) WebProfileManager(const QStringList &paths, const QString &default_id,
+ const QString &search = QString(), const QUrl &homepage = QUrl(), const QUrl &newtab = QUrl())
+ {
+ static_assert(!use_global);
+ for(const auto &path : paths) {
+ const auto id = QFileInfo(path).baseName();
+ Profile profile;
+ profile.settings = WebProfile::load(path, search, homepage, newtab);
+ profile.ptr = WebProfile::load(id, profile.settings, true);
+ profiles[id] = profile;
+ }
+
+ if(!profiles.contains(default_id)) {
+ auto *settings = WebProfile::load(QString(), search, homepage, newtab);
+ profiles[default_id] = Profile{
+ .settings = settings,
+ .ptr = WebProfile::load(default_id, settings, true),
+ };
+ }
+ WebProfile::setDefaultProfile(profiles[default_id].ptr);
+ }
+ ~WebProfileManager();
+
+ WebProfileManager(const WebProfileManager &) = delete;
+ WebProfileManager &operator=(const WebProfileManager &) = delete;
+
+ return_typestate(unconsumed) WebProfileManager(WebProfileManager<false> && other param_typestate(unconsumed))
+ {
+ static_assert(!use_global);
+ profiles = std::move(other.profiles);
+ other.consume();
+ }
+ WebProfileManager &operator=(WebProfileManager &&) = delete;
+
+ callable_when(unconsumed) [[nodiscard]] WebProfile *profile(const QString &id) const
+ {
+ return profiles.value(id).ptr;
+ }
+
+ callable_when(unconsumed) void add(const QString &id, WebProfile *profile, QSettings *settings)
+ {
+ if constexpr(use_global) {
+ return;
+ }
+
+ if(profile != nullptr && settings != nullptr) {
+ profiles[id] = Profile{ settings, profile, false };
+ }
+ }
+
+ callable_when(unconsumed) void deleteProfile(const QString &id)
+ {
+ if constexpr(use_global) {
+ return;
+ }
+
+ if(profiles.contains(id)) {
+ profiles[id].selfDestruct = true;
+ }
+ }
+
+ callable_when(unconsumed) [[nodiscard]] QStringList idList() const
+ {
+ return profiles.keys();
+ }
+
+ callable_when(unconsumed) void walk(std::function<void(const QString &id, WebProfile *profile, QSettings *settings)>) const;
+
+ callable_when(unconsumed) void make_global();
+
+private:
+ set_typestate(consumed) void consume() {}
+
+ struct Profile {
+ QSettings *settings = nullptr;
+ WebProfile *ptr = nullptr;
+ bool selfDestruct = false;
+ };
+
+ QMap<QString, Profile> profiles;
+};
+
+#endif // SMOLBOTE_PROFILEMANAGER_H
diff --git a/lib/webengine/webview.cpp b/lib/webengine/webview.cpp
new file mode 100644
index 0000000..bc52102
--- /dev/null
+++ b/lib/webengine/webview.cpp
@@ -0,0 +1,87 @@
+/*
+ * This file is part of smolbote. It's copyrighted by the contributors recorded
+ * in the version control history of the file, available from its original
+ * location: https://neueland.iserlohn-fortress.net/gitea/aqua/smolbote
+ *
+ * SPDX-License-Identifier: GPL-3.0
+ */
+
+#include "webview.h"
+#include "webpage.h"
+#include "webprofile.h"
+#include "webprofilemanager.h"
+#include "webviewcontextmenu.h"
+#include <QContextMenuEvent>
+#include <QWebEngineHistoryItem>
+
+WebView::WebView(QWidget *parent)
+ : QWebEngineView(parent)
+{
+ // load status and progress
+ connect(this, &QWebEngineView::loadStarted, this, [this]() {
+ m_loaded = false;
+ });
+ connect(this, &QWebEngineView::loadFinished, this, [this]() {
+ m_loaded = true;
+ });
+
+ // TODO for Qt 5.15, check for fix on QTBUG 65223
+ connect(this, &QWebEngineView::loadProgress, this, [this](int progress) {
+ if(progress == 100) {
+ emit loadFinished(true);
+ }
+ });
+}
+
+WebView::WebView(WebProfile *profile, cb_createWindow_t cb, QWidget *parent)
+ : WebView(parent)
+{
+ cb_createWindow = cb;
+ setProfile(profile);
+}
+
+WebView::WebView(const Session::WebView &webview_data, cb_createWindow_t cb, QWidget *parent)
+ : WebView(parent)
+{
+ cb_createWindow = cb;
+ WebProfileManager profileManager;
+ setProfile(profileManager.profile(webview_data.profile));
+
+ if(!webview_data.url.isEmpty())
+ load(QUrl::fromUserInput(webview_data.url));
+ else {
+ QByteArray copy(webview_data.history);
+ QDataStream historyStream(&copy, QIODevice::ReadOnly);
+ historyStream >> *history();
+ }
+}
+
+void WebView::setProfile(WebProfile *profile)
+{
+ m_profile = (profile == nullptr) ? WebProfile::defaultProfile() : profile;
+ const auto url = this->url();
+ setPage(new WebPage(m_profile, this));
+ this->load(url);
+}
+
+Session::WebView WebView::serialize() const
+{
+ QByteArray historyData;
+ QDataStream historyStream(&historyData, QIODevice::WriteOnly);
+ historyStream << *history();
+
+ return { profile()->getId(), QString(), historyData };
+}
+
+void WebView::search(const QString &term)
+{
+ const QString searchUrl = m_profile->search().arg(QString(QUrl::toPercentEncoding(term)));
+ load(searchUrl);
+}
+
+void WebView::contextMenuEvent(QContextMenuEvent *event)
+{
+ auto *menu = new WebViewContextMenu(this);
+ //const auto ctxdata = page()->contextMenuData();
+ menu->exec(event->globalPos());
+}
diff --git a/lib/webengine/webview.h b/lib/webengine/webview.h
new file mode 100644
index 0000000..538ffa9
--- /dev/null
+++ b/lib/webengine/webview.h
@@ -0,0 +1,67 @@
+/*
+ * This file is part of smolbote. It's copyrighted by the contributors recorded
+ * in the version control history of the file, available from its original
+ * location: https://neueland.iserlohn-fortress.net/gitea/aqua/smolbote
+ *
+ * SPDX-License-Identifier: GPL-3.0
+ */
+
+#ifndef SMOLBOTE_WEBVIEW_H
+#define SMOLBOTE_WEBVIEW_H
+
+#include "smolbote/session.hpp"
+#include "webpage.h"
+#include <QWebEngineView>
+#include <functional>
+
+class WebProfile;
+class WebViewContextMenu;
+class WebView final : public QWebEngineView
+{
+ friend class WebViewContextMenu;
+
+ Q_OBJECT
+
+ explicit WebView(QWidget *parent = nullptr);
+
+public:
+ typedef std::function<WebView *(QWebEnginePage::WebWindowType)> cb_createWindow_t;
+
+ WebView(WebProfile *profile, cb_createWindow_t cb, QWidget *parent = nullptr);
+ WebView(const Session::WebView &webview_data, cb_createWindow_t cb, QWidget *parent = nullptr);
+ ~WebView() = default;
+
+ [[nodiscard]] WebProfile *profile() const
+ {
+ return m_profile;
+ }
+ void setProfile(WebProfile *profile);
+
+ [[nodiscard]] Session::WebView serialize() const;
+
+ bool isLoaded() const
+ {
+ return m_loaded;
+ }
+
+public slots:
+ void search(const QString &term);
+
+signals:
+ void newBookmark(const QString &title, const QUrl &url);
+
+protected:
+ WebView *createWindow(QWebEnginePage::WebWindowType type) override
+ {
+ return cb_createWindow ? cb_createWindow(type) : nullptr;
+ }
+ void contextMenuEvent(QContextMenuEvent *event) override;
+
+private:
+ cb_createWindow_t cb_createWindow;
+ WebProfile *m_profile = nullptr;
+
+ bool m_loaded = false;
+};
+
+#endif // SMOLBOTE_WEBVIEW_H
diff --git a/lib/webengine/webviewcontextmenu.cpp b/lib/webengine/webviewcontextmenu.cpp
new file mode 100644
index 0000000..c9d809f
--- /dev/null
+++ b/lib/webengine/webviewcontextmenu.cpp
@@ -0,0 +1,233 @@
+/*
+ * This file is part of smolbote. It's copyrighted by the contributors recorded
+ * in the version control history of the file, available from its original
+ * location: https://neueland.iserlohn-fortress.net/gitea/aqua/smolbote
+ *
+ * SPDX-License-Identifier: GPL-3.0
+ */
+
+#include "webviewcontextmenu.h"
+#include "webprofilemanager.h"
+#include "webview.h"
+#include "util.h"
+#include <QContextMenuEvent>
+#include <QDialog>
+#include <QMenu>
+#include <QSlider>
+#include <QStyle>
+#include <QToolButton>
+#include <QVBoxLayout>
+#include <QWebEngineContextMenuData>
+#include <QWebEngineHistory>
+#include <QWidgetAction>
+
+constexpr int min_width = 250;
+constexpr QSize button_size(32, 32);
+
+inline QAction *historyAction(QWebEngineView *view, const QWebEngineHistoryItem &item)
+{
+ auto *action = new QAction(view);
+ if(item.title().isEmpty()) {
+ action->setText(item.url().toString());
+ } else {
+ action->setText(QObject::tr("%1 (%2)").arg(item.title(), item.url().toString()));
+ }
+
+ QObject::connect(action, &QAction::triggered, view, [view, item]() {
+ view->history()->goToItem(item);
+ });
+ return action;
+}
+
+WebViewContextMenu::WebViewContextMenu(WebView *view)
+ : QMenu(view)
+{
+ setMinimumWidth(min_width);
+
+ auto *navButtons = new QWidgetAction(this);
+
+ auto *buttons = new QWidget(this);
+ auto *buttonsLayout = new QHBoxLayout;
+ buttonsLayout->setContentsMargins(8, 0, 8, 0);
+ buttonsLayout->setSpacing(2);
+
+ auto *backButton = new QToolButton(this);
+ backButton->setMinimumSize(button_size);
+ backButton->setEnabled(view->history()->canGoBack());
+ backButton->setIcon(Util::icon<QStyle::SP_ArrowBack>());
+ connect(backButton, &QToolButton::clicked, view, [this, view]() {
+ view->back();
+ this->close();
+ });
+ buttonsLayout->addWidget(backButton);
+
+ auto *forwardButton = new QToolButton(this);
+ forwardButton->setMinimumSize(button_size);
+ forwardButton->setEnabled(view->history()->canGoForward());
+ forwardButton->setIcon(Util::icon<QStyle::SP_ArrowForward>());
+ connect(forwardButton, &QToolButton::clicked, view, [this, view]() {
+ view->forward();
+ this->close();
+ });
+ buttonsLayout->addWidget(forwardButton);
+
+ auto *refreshButton = new QToolButton(this);
+ refreshButton->setMinimumSize(button_size);
+ refreshButton->setIcon(Util::icon<QStyle::SP_BrowserReload>());
+ connect(refreshButton, &QToolButton::clicked, view, [view, this]() {
+ view->reload();
+ this->close();
+ });
+ buttonsLayout->addWidget(refreshButton);
+
+ buttonsLayout->addStretch();
+
+ auto *muteButton = new QToolButton(this);
+ muteButton->setMinimumSize(button_size);
+ muteButton->setCheckable(true);
+ muteButton->setChecked(view->page()->isAudioMuted());
+ muteButton->setIcon(Util::icon<QStyle::SP_MediaVolume>());
+ connect(muteButton, &QToolButton::clicked, view, [view, this](bool checked) {
+ view->page()->setAudioMuted(checked);
+ this->close();
+ });
+ buttonsLayout->addWidget(muteButton);
+
+ buttons->setLayout(buttonsLayout);
+ navButtons->setDefaultWidget(buttons);
+
+ this->addAction(navButtons);
+ this->addSeparator();
+
+ const auto ctxdata = view->page()->contextMenuData();
+
+ if(ctxdata.mediaType() == QWebEngineContextMenuData::MediaTypeNone) {
+ auto *backMenu = this->addMenu(tr("Back"));
+ if(!view->history()->canGoBack()) {
+ backMenu->setEnabled(false);
+ } else {
+ connect(backMenu, &QMenu::aboutToShow, view, [view, backMenu]() {
+ backMenu->clear();
+ const auto backItems = view->history()->backItems(10);
+ for(const QWebEngineHistoryItem &item : backItems) {
+ backMenu->addAction(historyAction(view, item));
+ }
+ });
+ }
+
+ auto *forwardMenu = this->addMenu(tr("Forward"));
+ if(!view->history()->canGoForward()) {
+ forwardMenu->setEnabled(false);
+ } else {
+ connect(forwardMenu, &QMenu::aboutToShow, view, [view, forwardMenu]() {
+ forwardMenu->clear();
+ const auto forwardItems = view->history()->forwardItems(10);
+ for(const QWebEngineHistoryItem &item : forwardItems) {
+ forwardMenu->addAction(historyAction(view, item));
+ }
+ });
+ }
+
+ connect(this->addAction(tr("Reload")), &QAction::triggered, view, [view]() {
+ view->page()->triggerAction(QWebEnginePage::Reload);
+ });
+ connect(this->addAction(tr("Reload and bypass Cache")), &QAction::triggered, view, [view]() {
+ view->page()->triggerAction(QWebEnginePage::ReloadAndBypassCache);
+ });
+
+ this->addSeparator();
+
+ connect(this->addAction(tr("Select All")), &QAction::triggered, view, [view]() {
+ view->page()->triggerAction(QWebEnginePage::SelectAll);
+ });
+ connect(this->addAction(tr("Clear Selection")), &QAction::triggered, view, [view]() {
+ view->page()->triggerAction(QWebEnginePage::Unselect);
+ });
+ connect(this->addAction(tr("Copy to clipboard")), &QAction::triggered, view, [view]() {
+ view->page()->triggerAction(QWebEnginePage::Copy);
+ });
+
+ } else if(ctxdata.mediaType() == QWebEngineContextMenuData::MediaTypeImage) {
+ connect(this->addAction(tr("Copy image to clipboard")), &QAction::triggered, view, [view]() {
+ view->page()->triggerAction(QWebEnginePage::CopyImageToClipboard);
+ });
+ connect(this->addAction(tr("Copy image URL to clipboard")), &QAction::triggered, view, [view]() {
+ view->page()->triggerAction(QWebEnginePage::CopyImageUrlToClipboard);
+ });
+ if(!ctxdata.mediaUrl().isEmpty()) {
+ if(view->url() != ctxdata.mediaUrl()) {
+ connect(this->addAction(tr("Open image")), &QAction::triggered, view, [view, ctxdata]() {
+ view->load(ctxdata.mediaUrl());
+ });
+ connect(this->addAction(tr("Open image in new tab")), &QAction::triggered, view, [view, ctxdata]() {
+ view->createWindow(QWebEnginePage::WebBrowserTab)->load(ctxdata.mediaUrl());
+ });
+ }
+ connect(this->addAction(tr("Save image")), &QAction::triggered, view, [view, ctxdata]() {
+ view->page()->download(ctxdata.mediaUrl());
+ });
+ }
+
+ } else {
+ addMenu(view->page()->createStandardContextMenu());
+ }
+
+ if(!ctxdata.linkUrl().isEmpty()) {
+ this->addSeparator();
+ connect(this->addAction(tr("Open link in new tab")), &QAction::triggered, view, [view, ctxdata]() {
+ view->createWindow(QWebEnginePage::WebBrowserTab)->load(ctxdata.linkUrl());
+ });
+
+ auto *newTabMenu = this->addMenu(tr("Open link in new tab with profile"));
+ profileMenu(newTabMenu, [view, ctxdata](WebProfile *profile) {
+ auto *v = view->createWindow(QWebEnginePage::WebBrowserTab);
+ v->setProfile(profile);
+ v->load(ctxdata.linkUrl());
+ });
+
+ connect(this->addAction(tr("Open link in new window")), &QAction::triggered, view, [view, ctxdata]() {
+ view->createWindow(QWebEnginePage::WebBrowserWindow)->load(ctxdata.linkUrl());
+ });
+
+ connect(this->addAction(tr("Copy link address")), &QAction::triggered, view, [view]() {
+ view->page()->triggerAction(QWebEnginePage::CopyLinkToClipboard);
+ });
+ }
+
+ // zoom widget
+ {
+ this->addSeparator();
+
+ auto *zoomSlider = new QSlider(Qt::Horizontal);
+ zoomSlider->setMinimum(5);
+ zoomSlider->setMaximum(50);
+ zoomSlider->setValue(static_cast<int>(view->zoomFactor() * 10));
+
+ auto *zoomAction = this->addAction(tr("Zoom: %1x").arg(view->zoomFactor()));
+ connect(zoomAction, &QAction::triggered, view, [zoomSlider]() {
+ zoomSlider->setValue(10);
+ });
+
+ connect(zoomSlider, &QSlider::valueChanged, view, [view, zoomAction](int value) {
+ zoomAction->setText(tr("Zoom: %1x").arg(static_cast<qreal>(value) / 10));
+ view->setZoomFactor(static_cast<qreal>(value) / 10);
+ });
+
+ auto *zoomWidgetAction = new QWidgetAction(this);
+ zoomWidgetAction->setDefaultWidget(zoomSlider);
+
+ this->addAction(zoomWidgetAction);
+ }
+
+#ifndef NDEBUG
+ /*
+ {
+ this->addSeparator();
+ auto *autofillAction = this->addAction(tr("Autofill form"));
+ connect(autofillAction, &QAction::triggered, view, [view]() {
+ Wallet::autocompleteForm(view);
+ });
+ };
+ */
+#endif
+}
diff --git a/lib/webengine/webviewcontextmenu.h b/lib/webengine/webviewcontextmenu.h
new file mode 100644
index 0000000..881670a
--- /dev/null
+++ b/lib/webengine/webviewcontextmenu.h
@@ -0,0 +1,21 @@
+/*
+ * This file is part of smolbote. It's copyrighted by the contributors recorded
+ * in the version control history of the file, available from its original
+ * location: https://neueland.iserlohn-fortress.net/gitea/aqua/smolbote
+ *
+ * SPDX-License-Identifier: GPL-3.0
+ */
+
+#ifndef SMOLBOTE_WEBVIEWCONTEXTMENU_H
+#define SMOLBOTE_WEBVIEWCONTEXTMENU_H
+
+#include <QMenu>
+
+class WebView;
+class WebViewContextMenu : public QMenu
+{
+public:
+ explicit WebViewContextMenu(WebView *view);
+};
+
+#endif // SMOLBOTE_WEBVIEWCONTEXTMENU_H