File indexing completed on 2024-05-19 05:01:22
0001 /* 0002 This file is part of the KDE project. 0003 0004 SPDX-FileCopyrightText: 2021 Stefano Crocco <stefano.crocco@alice.it> 0005 0006 SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0007 */ 0008 0009 #include "webenginepartcontrols.h" 0010 #include "schemehandlers/errorschemehandler.h" 0011 #include "webengineurlrequestinterceptor.h" 0012 #include "schemehandlers/kiohandler.h" 0013 #include "about/konq_aboutpage.h" 0014 #include "spellcheckermanager.h" 0015 #include "webenginepartdownloadmanager.h" 0016 #include "certificateerrordialogmanager.h" 0017 #include "webenginewallet.h" 0018 #include "webenginepart.h" 0019 #include "webenginepage.h" 0020 #include "navigationrecorder.h" 0021 #include <webenginepart_debug.h> 0022 #include "profile.h" 0023 #include "interfaces/browser.h" 0024 #include "schemehandlers/execschemehandler.h" 0025 0026 #include <KProtocolInfo> 0027 #include <KSharedConfig> 0028 #include <KConfigGroup> 0029 #include <KLocalizedString> 0030 0031 #include <QWebEngineProfile> 0032 #include <QWebEngineUrlScheme> 0033 #include <QWebEngineSettings> 0034 #include <QWebEngineScriptCollection> 0035 #include <QApplication> 0036 #include <QLocale> 0037 #include <QSettings> 0038 #include <QJsonDocument> 0039 #include <QMessageBox> 0040 0041 using namespace KonqInterfaces; 0042 using namespace WebEngine; 0043 0044 WebEnginePartControls::WebEnginePartControls(): QObject(), 0045 m_profile(nullptr), m_cookieJar(nullptr), m_spellCheckerManager(nullptr), m_downloadManager(nullptr), 0046 m_certificateErrorDialogManager(new KonqWebEnginePart::CertificateErrorDialogManager(this)), 0047 m_navigationRecorder(new NavigationRecorder(this)) 0048 { 0049 QVector<QByteArray> localSchemes = {"error", "konq", "tar", "bookmarks"}; 0050 const QStringList protocols = KProtocolInfo::protocols(); 0051 for(const QString &prot : protocols){ 0052 if (KProtocolInfo::defaultMimetype(prot) == "text/html") { 0053 localSchemes.append(QByteArray(prot.toLatin1())); 0054 } 0055 } 0056 for (const QByteArray &name : qAsConst(localSchemes)){ 0057 QWebEngineUrlScheme scheme(name); 0058 scheme.setFlags(QWebEngineUrlScheme::LocalScheme|QWebEngineUrlScheme::LocalAccessAllowed); 0059 scheme.setSyntax(QWebEngineUrlScheme::Syntax::Path); 0060 QWebEngineUrlScheme::registerScheme(scheme); 0061 } 0062 0063 QWebEngineUrlScheme execScheme(QByteArrayLiteral("exec")); 0064 execScheme.setFlags(QWebEngineUrlScheme::LocalScheme); 0065 execScheme.setSyntax(QWebEngineUrlScheme::Syntax::Path); 0066 QWebEngineUrlScheme::registerScheme(execScheme); 0067 0068 Browser *browser = Browser::browser(qApp); 0069 if (browser) { 0070 connect(browser, &Browser::configurationChanged, this, &WebEnginePartControls::reparseConfiguration); 0071 connect(browser, &Browser::userAgentChanged, this, &WebEnginePartControls::setHttpUserAgent); 0072 } 0073 } 0074 0075 WebEnginePartControls::~WebEnginePartControls() 0076 { 0077 } 0078 0079 WebEnginePartControls* WebEnginePartControls::self() 0080 { 0081 static WebEnginePartControls s_self; 0082 return &s_self; 0083 } 0084 0085 bool WebEnginePartControls::isReady() const 0086 { 0087 return m_profile; 0088 } 0089 0090 void WebEnginePartControls::registerScripts() 0091 { 0092 if (!m_profile) { 0093 qCDebug(WEBENGINEPART_LOG) << "Attempting to register scripts before setting the profile"; 0094 return; 0095 } 0096 0097 QFile jsonFile(QStringLiteral(":/scripts.json")); 0098 jsonFile.open(QIODevice::ReadOnly); 0099 QJsonObject obj = QJsonDocument::fromJson(jsonFile.readAll()).object(); 0100 Q_ASSERT(!obj.isEmpty()); 0101 jsonFile.close(); 0102 for (auto it = obj.constBegin(); it != obj.constEnd(); ++it) { 0103 QJsonObject scriptData = it.value().toObject(); 0104 Q_ASSERT(!scriptData.isEmpty()); 0105 QWebEngineScript script = scriptFromJson(it.key(), scriptData); 0106 if (!script.name().isEmpty()) { 0107 m_profile->scripts()->insert(script); 0108 } 0109 } 0110 } 0111 0112 QWebEngineScript WebEnginePartControls::scriptFromJson(const QString& name, const QJsonObject& obj) 0113 { 0114 QWebEngineScript script; 0115 QString file = obj.value(QLatin1String("file")).toString(); 0116 if (file.isEmpty()) { 0117 return script; 0118 } 0119 QFile sourceFile(file); 0120 sourceFile.open(QIODevice::ReadOnly); 0121 Q_ASSERT(sourceFile.isOpen()); 0122 script.setSourceCode(QString(sourceFile.readAll())); 0123 QJsonValue val = obj.value(QLatin1String("injectionPoint")); 0124 if (!val.isNull()) { 0125 int injectionPoint = val.toInt(-1); 0126 //NOTE:keep in sync with the values of QWebEngineScript::InjectionPoint 0127 Q_ASSERT(injectionPoint >=QWebEngineScript::Deferred && injectionPoint <= QWebEngineScript::DocumentCreation); 0128 script.setInjectionPoint(static_cast<QWebEngineScript::InjectionPoint>(injectionPoint)); 0129 } 0130 val = obj.value(QLatin1String("worldId")); 0131 if (!val.isNull()) { 0132 int world= val.toInt(-1); 0133 //NOTE: keep in sync with the values of QWebEngineScript::ScriptWorldId 0134 Q_ASSERT(world >= QWebEngineScript::MainWorld); 0135 script.setWorldId(world); 0136 } 0137 val = obj.value(QStringLiteral("runsOnSubFrames")); 0138 if (!val.isBool()) { 0139 script.setRunsOnSubFrames(val.toBool()); 0140 } 0141 script.setName(name); 0142 return script; 0143 } 0144 0145 void WebEnginePartControls::setup(KonqWebEnginePart::Profile* profile) 0146 { 0147 if (!profile || isReady()) { 0148 return; 0149 } 0150 m_profile = profile; 0151 0152 registerScripts(); 0153 0154 m_profile->installUrlSchemeHandler("error", new ErrorSchemeHandler(m_profile)); 0155 m_profile->installUrlSchemeHandler("konq", new KonqUrlSchemeHandler(m_profile)); 0156 m_profile->installUrlSchemeHandler("help", new KIOHandler(m_profile)); 0157 m_profile->installUrlSchemeHandler("tar", new KIOHandler(m_profile)); 0158 m_profile->installUrlSchemeHandler("bookmarks", new KIOHandler(m_profile)); 0159 m_profile->installUrlSchemeHandler("exec", new ExecSchemeHandler(m_profile)); 0160 0161 m_profile->setUrlRequestInterceptor(new WebEngineUrlRequestInterceptor(this)); 0162 0163 m_cookieJar = new WebEnginePartCookieJar(profile, this); 0164 Browser *browser = Browser::browser(qApp); 0165 if (browser) { 0166 m_profile->setHttpUserAgent(browser->userAgent()); 0167 #ifdef MANAGE_COOKIES_INTERNALLY 0168 browser->setCookieJar(m_cookieJar); 0169 #endif 0170 } 0171 m_spellCheckerManager = new SpellCheckerManager(profile, this); 0172 m_downloadManager= new WebEnginePartDownloadManager(profile, this); 0173 m_profile->settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, true); 0174 QString langHeader = determineHttpAcceptLanguageHeader(); 0175 if (!langHeader.isEmpty()) { 0176 m_profile->setHttpAcceptLanguage(langHeader); 0177 } 0178 0179 reparseConfiguration(); 0180 } 0181 0182 WebEnginePartDownloadManager* WebEnginePartControls::downloadManager() const 0183 { 0184 return m_downloadManager; 0185 } 0186 0187 SpellCheckerManager* WebEnginePartControls::spellCheckerManager() const 0188 { 0189 return m_spellCheckerManager; 0190 } 0191 0192 bool WebEnginePartControls::handleCertificateError(const QWebEngineCertificateError& ce, WebEnginePage* page) 0193 { 0194 return m_certificateErrorDialogManager->handleCertificateError(ce, page); 0195 } 0196 0197 QString WebEnginePartControls::determineHttpAcceptLanguageHeader() const 0198 { 0199 //According to comments in KSwitchLanguageDialog, the settings are stored in an INI format using QSettings, 0200 //rather than using KConfig 0201 QSettings appLangSettings(QStandardPaths::locate(QStandardPaths::GenericConfigLocation, "klanguageoverridesrc"), QSettings::IniFormat); 0202 appLangSettings.beginGroup(QStringLiteral("Language")); 0203 QString lang(appLangSettings.value(QApplication::instance()->applicationName()).toByteArray()); 0204 if (lang.isEmpty()) { 0205 KSharedConfig::Ptr cfg; 0206 cfg = KSharedConfig::openConfig(QStringLiteral("plasma-localerc")); 0207 lang = cfg->group(QStringLiteral("Translations")).readEntry(QStringLiteral("LANGUAGE")); 0208 if (lang.isEmpty()) { 0209 lang = QLocale::system().name(); 0210 } 0211 if (lang.isEmpty()) { 0212 return QString(); 0213 } 0214 } 0215 QStringList languages = lang.split(':'); 0216 QString header = languages.at(0); 0217 //In Qt6, QList::length() returns a qsizetype, which means you can't pass it to std::min. 0218 //Casting it to an int shouldn't be a problem, since the number of languages should be 0219 //small 0220 int max = std::min(static_cast<int>(languages.length()), 10); 0221 //The counter starts from 1 because the first entry has already been inserted above 0222 for (int i = 1; i < max; ++i) { 0223 header.append(QString(", %1;q=0.%2").arg(languages.at(i)).arg(10-i)); 0224 } 0225 return header; 0226 } 0227 0228 NavigationRecorder * WebEnginePartControls::navigationRecorder() const 0229 { 0230 return m_navigationRecorder; 0231 } 0232 0233 void WebEnginePartControls::reparseConfiguration() 0234 { 0235 if (!m_profile) { 0236 return; 0237 } 0238 KSharedConfig::Ptr cfg = KSharedConfig::openConfig(); 0239 KConfigGroup grp = cfg->group(QStringLiteral("Cache")); 0240 if (grp.readEntry(QStringLiteral("CacheEnabled"), true)) { 0241 QWebEngineProfile::HttpCacheType type = grp.readEntry(QStringLiteral("MemoryCache"), false) ? QWebEngineProfile::MemoryHttpCache : QWebEngineProfile::DiskHttpCache; 0242 m_profile->setHttpCacheType(type); 0243 m_profile->setHttpCacheMaximumSize(grp.readEntry(QStringLiteral("MaximumCacheSize"), 0)); 0244 //NOTE: According to the documentation, setCachePath resets the cache path to its default value if the argument is a null QString 0245 //it doesn't specify what it does if the string is empty but not null. Experimenting, it seems the behavior is the same 0246 m_profile->setCachePath(grp.readEntry(QStringLiteral("CustomCacheDir"), QString())); 0247 0248 } else { 0249 m_profile->setHttpCacheType(QWebEngineProfile::NoCache); 0250 } 0251 0252 updateUserStyleSheetScript(); 0253 } 0254 0255 void WebEnginePartControls::updateUserStyleSheetScript() 0256 { 0257 #if QT_VERSION_MAJOR < 6 0258 QList<QWebEngineScript> oldScripts = m_profile->scripts()->findScripts(s_userStyleSheetScriptName); 0259 #else 0260 QList<QWebEngineScript> oldScripts = m_profile->scripts()->find(s_userStyleSheetScriptName); 0261 #endif 0262 bool hadUserStyleSheet = !oldScripts.isEmpty(); 0263 //Remove old style sheets. Note that oldScripts should either be empty or contain only one element 0264 //In theory, we could reuse the previous script, if it turns out to be the same as the new one, but 0265 //I think this way is faster (besides being easier) 0266 for (const QWebEngineScript &s : oldScripts) { 0267 m_profile->scripts()->remove(s); 0268 } 0269 QUrl userStyleSheetUrl(WebEngineSettings::self()->userStyleSheet()); 0270 bool userStyleSheetEnabled = !userStyleSheetUrl.isEmpty(); 0271 //If the user stylesheet is disable and it was already disabled, there's nothing to do; if there was a custom stylesheet in use, 0272 //we'll have to remove it, so we can't return 0273 if (!userStyleSheetEnabled && !hadUserStyleSheet) { 0274 return; 0275 } 0276 0277 QString css; 0278 if (userStyleSheetEnabled) { 0279 //Read the contents of the custom style sheet 0280 QFile cssFile(userStyleSheetUrl.path()); 0281 cssFile.open(QFile::ReadOnly); 0282 if (cssFile.isOpen()) { 0283 css = cssFile.readAll(); 0284 cssFile.close(); 0285 } 0286 else { 0287 auto msg = i18n("Couldn't open the file <tt>%1</tt> containing the user style sheet. The default style sheet will be used", userStyleSheetUrl.path()); 0288 QMessageBox::warning(qApp->activeWindow(), QString(), msg); 0289 //Only return if no custom stylesheet was in use 0290 if (!hadUserStyleSheet) { 0291 return; 0292 } 0293 userStyleSheetEnabled = false; 0294 } 0295 } 0296 0297 //Create the js code 0298 QFile applyUserCssFile(":/applyuserstylesheet.js"); 0299 applyUserCssFile.open(QFile::ReadOnly); 0300 Q_ASSERT(applyUserCssFile.isOpen()); 0301 QString code{QString(applyUserCssFile.readAll()).arg(s_userStyleSheetScriptName).arg(css.simplified())}; 0302 applyUserCssFile.close(); 0303 0304 //Tell pages to update their stylesheet. If `css` is empty, the script will remove the <style> element from the pages; 0305 //if `css` is not empty, it will update or create the appropriate <style> element 0306 emit updateStyleSheet(code); 0307 if (!userStyleSheetEnabled) { 0308 return; 0309 } 0310 0311 //Create a script to inject in new pages 0312 QWebEngineScript applyUserCss; 0313 applyUserCss.setName(s_userStyleSheetScriptName); 0314 applyUserCss.setInjectionPoint(QWebEngineScript::DocumentReady); 0315 applyUserCss.setWorldId(QWebEngineScript::ApplicationWorld); 0316 applyUserCss.setSourceCode(code); 0317 m_profile->scripts()->insert(applyUserCss); 0318 } 0319 0320 0321 QString WebEnginePartControls::httpUserAgent() const 0322 { 0323 return m_profile ? m_profile->httpUserAgent() : QString(); 0324 } 0325 0326 void WebEnginePartControls::setHttpUserAgent(const QString& uaString) 0327 { 0328 if (!m_profile || m_profile->httpUserAgent() == uaString) { 0329 return; 0330 } 0331 m_profile->setHttpUserAgent(uaString); 0332 emit userAgentChanged(uaString); 0333 } 0334 0335 QString WebEnginePartControls::defaultHttpUserAgent() const 0336 { 0337 return m_defaultUserAgent; 0338 }