Changeset View
Changeset View
Standalone View
Standalone View
processui/scripting.cpp
1 | /* | 1 | /* | ||
---|---|---|---|---|---|
2 | KSysGuard, the KDE System Guard | 2 | KSysGuard, the KDE System Guard | ||
3 | 3 | | |||
4 | Copyright (c) 2009 John Tapsell <john.tapsell@kde.org> | 4 | Copyright (c) 2009 John Tapsell <john.tapsell@kde.org> | ||
5 | Copyright (c) 2018 Fabian Vogt <fabian@ritter-vogt.de> | ||||
5 | 6 | | |||
6 | This library is free software; you can redistribute it and/or | 7 | This library is free software; you can redistribute it and/or | ||
7 | modify it under the terms of the GNU Library General Public | 8 | modify it under the terms of the GNU Library General Public | ||
8 | License as published by the Free Software Foundation; either | 9 | License as published by the Free Software Foundation; either | ||
9 | version 2 of the License, or (at your option) any later version. | 10 | version 2 of the License, or (at your option) any later version. | ||
10 | 11 | | |||
11 | This library is distributed in the hope that it will be useful, | 12 | This library is distributed in the hope that it will be useful, | ||
12 | but WITHOUT ANY WARRANTY; without even the implied warranty of | 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
Show All 24 Lines | |||||
37 | 38 | | |||
38 | #include <KDesktopFile> | 39 | #include <KDesktopFile> | ||
39 | #include <KStandardAction> | 40 | #include <KStandardAction> | ||
40 | #include <KLocalizedString> | 41 | #include <KLocalizedString> | ||
41 | #include <QVBoxLayout> | 42 | #include <QVBoxLayout> | ||
42 | #include <QMessageBox> | 43 | #include <QMessageBox> | ||
43 | #include <QDialogButtonBox> | 44 | #include <QDialogButtonBox> | ||
44 | 45 | | |||
45 | #if HAVE_QTWEBKITWIDGETS | 46 | #if HAVE_QTWEBENGINEWIDGETS | ||
46 | #include <QWebView> | 47 | #include <QWebChannel> | ||
47 | #include <QWebFrame> | 48 | #include <QWebEngineSettings> | ||
49 | #include <QWebEngineView> | ||||
50 | #include <QWebEngineProfile> | ||||
51 | #include <QWebEngineScriptCollection> | ||||
52 | #include <QWebEngineUrlRequestInterceptor> | ||||
48 | #endif | 53 | #endif | ||
49 | 54 | | |||
55 | class RemoteUrlInterceptor : public QWebEngineUrlRequestInterceptor { | ||||
56 | public: | ||||
57 | RemoteUrlInterceptor(QObject *parent) : QWebEngineUrlRequestInterceptor(parent) {} | ||||
58 | void interceptRequest(QWebEngineUrlRequestInfo &info) override | ||||
59 | { | ||||
60 | // Block non-GET/HEAD requests | ||||
61 | if(!QStringList({QStringLiteral("GET"), QStringLiteral("HEAD")}) | ||||
62 | .contains(QString::fromLatin1(info.requestMethod()))) | ||||
63 | info.block(true); | ||||
64 | | ||||
65 | // Block remote URLs | ||||
66 | if(!QStringList({QStringLiteral("blob"), QStringLiteral("data"), | ||||
67 | QStringLiteral("file")}).contains(info.requestUrl().scheme())) | ||||
68 | info.block(true); | ||||
69 | } | ||||
70 | }; | ||||
71 | | ||||
50 | class ScriptingHtmlDialog : public QDialog { | 72 | class ScriptingHtmlDialog : public QDialog { | ||
51 | public: | 73 | public: | ||
52 | ScriptingHtmlDialog(QWidget *parent) : QDialog(parent) { | 74 | ScriptingHtmlDialog(QWidget *parent) : QDialog(parent) { | ||
53 | 75 | | |||
54 | QDialogButtonBox *buttonBox = new QDialogButtonBox(this); | 76 | QDialogButtonBox *buttonBox = new QDialogButtonBox(this); | ||
55 | buttonBox->setStandardButtons(QDialogButtonBox::Close); | 77 | buttonBox->setStandardButtons(QDialogButtonBox::Close); | ||
56 | connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); | 78 | connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); | ||
57 | connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); | 79 | connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); | ||
58 | 80 | | |||
59 | #if HAVE_QTWEBKITWIDGETS | 81 | #if HAVE_QTWEBENGINEWIDGETS | ||
60 | QVBoxLayout *layout = new QVBoxLayout; | 82 | QVBoxLayout *layout = new QVBoxLayout; | ||
61 | layout->addWidget(&m_webView); | 83 | layout->addWidget(&m_webView); | ||
62 | layout->addWidget(buttonBox); | 84 | layout->addWidget(buttonBox); | ||
63 | setLayout(layout); | 85 | setLayout(layout); | ||
64 | (void)minimumSizeHint(); //Force the dialog to be laid out now | | |||
65 | layout->setContentsMargins(0,0,0,0); | 86 | layout->setContentsMargins(0,0,0,0); | ||
66 | m_webView.settings()->setOfflineStoragePath(QString()); | 87 | m_webView.settings()->setAttribute(QWebEngineSettings::PluginsEnabled, false); | ||
67 | m_webView.settings()->setObjectCacheCapacities(0,0,0); | 88 | m_webView.page()->profile()->setRequestInterceptor(new RemoteUrlInterceptor(this)); | ||
68 | m_webView.settings()->setAttribute(QWebSettings::PluginsEnabled, false); | | |||
69 | m_webView.settings()->setAttribute(QWebSettings::DeveloperExtrasEnabled, false); | | |||
70 | m_webView.page()->setNetworkAccessManager(nullptr); //Disable talking to remote servers | | |||
71 | m_webView.page()->mainFrame()->setScrollBarPolicy(Qt::Horizontal, Qt::ScrollBarAsNeeded); | | |||
72 | m_webView.page()->mainFrame()->setScrollBarPolicy(Qt::Vertical, Qt::ScrollBarAsNeeded); | | |||
73 | | ||||
74 | // inject a style sheet that follows system colors, otherwise we might end up with black text on dark gray background | | |||
75 | const QString styleSheet = QStringLiteral( | | |||
76 | "body { background: %1; color: %2; }" \ | | |||
77 | "a { color: %3; }" \ | | |||
78 | "a:visited { color: %4; } " | | |||
79 | ).arg(palette().background().color().name(), | | |||
80 | palette().text().color().name(), | | |||
81 | palette().link().color().name(), | | |||
82 | palette().linkVisited().color().name()); | | |||
83 | | ||||
84 | // you can only provide a user style sheet url, so we turn it into a data url here | | |||
85 | const QUrl dataUrl(QStringLiteral("data:text/css;charset=utf-8;base64,") + QString::fromLatin1(styleSheet.toUtf8().toBase64())); | | |||
86 | | ||||
87 | m_webView.settings()->setUserStyleSheetUrl(dataUrl); | | |||
88 | | ||||
89 | #endif | 89 | #endif | ||
90 | } | 90 | } | ||
91 | #if HAVE_QTWEBKITWIDGETS | 91 | #if HAVE_QTWEBENGINEWIDGETS | ||
92 | QWebView *webView() { | 92 | QWebEngineView *webView() { | ||
93 | return &m_webView; | 93 | return &m_webView; | ||
94 | } | 94 | } | ||
95 | protected: | 95 | protected: | ||
96 | QWebView m_webView; | 96 | QWebEngineView m_webView; | ||
97 | #endif | 97 | #endif | ||
98 | }; | 98 | }; | ||
99 | 99 | | |||
100 | ProcessObject::ProcessObject(ProcessModel *model, int pid) { | 100 | ProcessObject::ProcessObject(ProcessModel *model, int pid) { | ||
101 | mModel = model; | 101 | mModel = model; | ||
102 | mPid = pid; | 102 | mPid = pid; | ||
103 | } | 103 | } | ||
104 | 104 | | |||
Show All 17 Lines | 121 | Scripting::Scripting(KSysGuardProcessList * parent) : QWidget(parent), mProcessList(parent) { | |||
122 | mScriptingHtmlDialog = nullptr; | 122 | mScriptingHtmlDialog = nullptr; | ||
123 | loadContextMenu(); | 123 | loadContextMenu(); | ||
124 | } | 124 | } | ||
125 | void Scripting::runScript(const QString &path, const QString &name) { | 125 | void Scripting::runScript(const QString &path, const QString &name) { | ||
126 | //Record the script name and path for use in the script helper functions | 126 | //Record the script name and path for use in the script helper functions | ||
127 | mScriptPath = path; | 127 | mScriptPath = path; | ||
128 | mScriptName = name; | 128 | mScriptName = name; | ||
129 | 129 | | |||
130 | #if HAVE_QTWEBKITWIDGETS | 130 | #if HAVE_QTWEBENGINEWIDGETS | ||
131 | QUrl fileName = QUrl::fromLocalFile(path + QStringLiteral("index.html")); | 131 | QUrl fileName = QUrl::fromLocalFile(path + QStringLiteral("index.html")); | ||
132 | if(!mScriptingHtmlDialog) { | 132 | if(!mScriptingHtmlDialog) { | ||
133 | mScriptingHtmlDialog = new ScriptingHtmlDialog(this); | 133 | mScriptingHtmlDialog = new ScriptingHtmlDialog(this); | ||
134 | mWebChannel = new QWebChannel(mScriptingHtmlDialog); | ||||
134 | connect(mScriptingHtmlDialog, &QDialog::rejected, this, &Scripting::stopAllScripts); | 135 | connect(mScriptingHtmlDialog, &QDialog::rejected, this, &Scripting::stopAllScripts); | ||
136 | // Only show after page loaded to allow for layouting | ||||
137 | mScriptingHtmlDialog->connect(mScriptingHtmlDialog->webView(), &QWebEngineView::loadFinished, | ||||
138 | mScriptingHtmlDialog, &ScriptingHtmlDialog::show); | ||||
135 | 139 | | |||
136 | QAction *refreshAction = new QAction(QStringLiteral("refresh"), mScriptingHtmlDialog); | 140 | QAction *refreshAction = new QAction(QStringLiteral("refresh"), mScriptingHtmlDialog); | ||
137 | refreshAction->setShortcut(QKeySequence::Refresh); | 141 | refreshAction->setShortcut(QKeySequence::Refresh); | ||
138 | connect(refreshAction, &QAction::triggered, this, &Scripting::refreshScript); | 142 | connect(refreshAction, &QAction::triggered, this, &Scripting::refreshScript); | ||
139 | mScriptingHtmlDialog->addAction(refreshAction); | 143 | mScriptingHtmlDialog->addAction(refreshAction); | ||
140 | 144 | | |||
141 | QAction *zoomInAction = KStandardAction::zoomIn(this, SLOT(zoomIn()), mScriptingHtmlDialog); | 145 | QAction *zoomInAction = KStandardAction::zoomIn(this, SLOT(zoomIn()), mScriptingHtmlDialog); | ||
142 | mScriptingHtmlDialog->addAction(zoomInAction); | 146 | mScriptingHtmlDialog->addAction(zoomInAction); | ||
143 | 147 | | |||
144 | QAction *zoomOutAction = KStandardAction::zoomOut(this, SLOT(zoomOut()), mScriptingHtmlDialog); | 148 | QAction *zoomOutAction = KStandardAction::zoomOut(this, SLOT(zoomOut()), mScriptingHtmlDialog); | ||
145 | mScriptingHtmlDialog->addAction(zoomOutAction); | 149 | mScriptingHtmlDialog->addAction(zoomOutAction); | ||
146 | } | 150 | } | ||
147 | 151 | | |||
148 | //Make the process information available to the script | 152 | // Make the process information available to the script | ||
149 | mScriptingHtmlDialog->webView()->load(fileName); | 153 | QWebEngineProfile *profile = mScriptingHtmlDialog->webView()->page()->profile(); | ||
150 | mScriptingHtmlDialog->show(); | 154 | QFile webChannelJsFile(QStringLiteral(":/qtwebchannel/qwebchannel.js")); | ||
151 | connect(mScriptingHtmlDialog->webView()->page()->mainFrame(), &QWebFrame::javaScriptWindowObjectCleared, this, &Scripting::setupJavascriptObjects); | 155 | webChannelJsFile.open(QIODevice::ReadOnly); | ||
156 | QString webChannelJs = QString::fromUtf8(webChannelJsFile.readAll()); | ||||
157 | | ||||
158 | /* Warning: Awful hack ahead! | ||||
159 | * WebChannel does not allow synchonous calls so we need to make | ||||
160 | * asynchronous calls synchronous. | ||||
161 | * The conversion is achieved by caching the result of all readFile | ||||
162 | * and fileExists calls and restarting the script on every result until | ||||
163 | * all requests can be fulfilled synchronously. | ||||
164 | * Another challenge is that WebEngine does not support reading | ||||
165 | * files from /proc over file:// (they are always empty) so we need | ||||
166 | * to keep using the ProcessObject helper methods. | ||||
167 | */ | ||||
168 | webChannelJs.append(QStringLiteral(R"JS( | ||||
169 | new QWebChannel(window.qt.webChannelTransport, function(channel) { | ||||
170 | window.process = channel.objects.process; | ||||
171 | window.process.realReadFile = window.process.readFile; | ||||
172 | window.process.realFileExists = window.process.fileExists; | ||||
173 | var files = {}; // Map of all read files. null means does not exist | ||||
174 | window.process.fileExists = function(name, cb) { | ||||
175 | if(cb) return window.process.realFileExists(name, cb); | ||||
176 | if (files[name] === null) | ||||
177 | return false; // Definitely does not exist | ||||
178 | if (typeof(files[name]) == 'string') | ||||
179 | return true; // Definitely exists | ||||
180 | | ||||
181 | window.process.realFileExists(name, function(r) { | ||||
182 | if(!r) { | ||||
183 | files[name] = null; | ||||
184 | refresh(); | ||||
185 | return; | ||||
186 | } | ||||
187 | window.process.realReadFile(name, function(r) { | ||||
188 | files[name] = r; | ||||
189 | refresh(); | ||||
190 | }); | ||||
191 | }); | ||||
192 | | ||||
193 | return true; // Might exist | ||||
194 | }; | ||||
195 | window.process.readFile = function(name,cb) { | ||||
196 | if(cb) return window.process.realReadFile(name, cb); | ||||
197 | if (typeof(files[name]) == 'string') | ||||
198 | return files[name]; // From cache | ||||
199 | | ||||
200 | window.process.fileExists(name); // Fill the cache | ||||
201 | return ''; | ||||
202 | }; | ||||
203 | refresh && refresh(); | ||||
204 | });)JS")); | ||||
205 | | ||||
206 | QWebEngineScript webChannelScript; | ||||
207 | webChannelScript.setSourceCode(webChannelJs); | ||||
208 | webChannelScript.setName(QStringLiteral("qwebchannel.js")); | ||||
209 | webChannelScript.setWorldId(QWebEngineScript::MainWorld); | ||||
210 | webChannelScript.setInjectionPoint(QWebEngineScript::DocumentCreation); | ||||
211 | webChannelScript.setRunsOnSubFrames(false); | ||||
212 | | ||||
213 | profile->scripts()->insert(webChannelScript); | ||||
214 | | ||||
215 | // Inject a style sheet that follows system colors, otherwise we might end up with black text on dark gray background | ||||
216 | const QString styleSheet = QStringLiteral( | ||||
217 | "body { background: %1; color: %2; }" \ | ||||
218 | "a { color: %3; }" \ | ||||
219 | "a:visited { color: %4; } " | ||||
220 | ).arg(palette().background().color().name(), | ||||
221 | palette().text().color().name(), | ||||
222 | palette().link().color().name(), | ||||
223 | palette().linkVisited().color().name()); | ||||
224 | | ||||
225 | QString styleSheetJs = QStringLiteral("\nvar node = document.createElement('style');" | ||||
226 | "node.innerHTML = '%1';" | ||||
227 | "document.body.appendChild(node);").arg(styleSheet); | ||||
228 | | ||||
229 | QWebEngineScript styleSheetScript; | ||||
230 | styleSheetScript.setSourceCode(styleSheetJs); | ||||
231 | styleSheetScript.setName(QStringLiteral("stylesheet.js")); | ||||
232 | styleSheetScript.setWorldId(QWebEngineScript::MainWorld); | ||||
233 | styleSheetScript.setInjectionPoint(QWebEngineScript::DocumentReady); | ||||
234 | styleSheetScript.setRunsOnSubFrames(false); | ||||
235 | | ||||
236 | profile->scripts()->insert(styleSheetScript); | ||||
237 | | ||||
152 | setupJavascriptObjects(); | 238 | setupJavascriptObjects(); | ||
239 | | ||||
240 | mScriptingHtmlDialog->webView()->load(fileName); | ||||
153 | #else | 241 | #else | ||
154 | QMessageBox::critical(this, i18n("QtWebKitWidgets not available"), | 242 | QMessageBox::critical(this, i18n("QtWebEngineWidgets not available"), | ||
155 | i18n("KSysGuard library was compiled without QtWebKitWidgets, please contact your distribution.")); | 243 | i18n("KSysGuard library was compiled without QtWebEngineWidgets, please contact your distribution.")); | ||
156 | #endif | 244 | #endif | ||
157 | } | 245 | } | ||
158 | #if HAVE_QTWEBKITWIDGETS | 246 | #if HAVE_QTWEBENGINEWIDGETS | ||
159 | void Scripting::zoomIn() { | 247 | void Scripting::zoomIn() { | ||
160 | QWebView *webView = mScriptingHtmlDialog->webView(); | 248 | QWebEngineView *webView = mScriptingHtmlDialog->webView(); | ||
161 | webView->setZoomFactor( webView->zoomFactor() * 1.1 ); | 249 | webView->setZoomFactor( webView->zoomFactor() * 1.1 ); | ||
162 | } | 250 | } | ||
163 | void Scripting::zoomOut() { | 251 | void Scripting::zoomOut() { | ||
164 | QWebView *webView = mScriptingHtmlDialog->webView(); | 252 | QWebEngineView *webView = mScriptingHtmlDialog->webView(); | ||
165 | if(webView->zoomFactor() > 0.1) //Prevent it getting too small | 253 | if(webView->zoomFactor() > 0.1) //Prevent it getting too small | ||
166 | webView->setZoomFactor( webView->zoomFactor() / 1.1 ); | 254 | webView->setZoomFactor( webView->zoomFactor() / 1.1 ); | ||
167 | } | 255 | } | ||
168 | 256 | | |||
169 | void Scripting::refreshScript() { | 257 | void Scripting::refreshScript() { | ||
170 | //Call any refresh function, if it exists | 258 | //Call any refresh function, if it exists | ||
171 | mProcessList->processModel()->update(0, KSysGuard::Processes::XMemory); | 259 | mProcessList->processModel()->update(0, KSysGuard::Processes::XMemory); | ||
172 | if(mScriptingHtmlDialog && mScriptingHtmlDialog->webView() && mScriptingHtmlDialog->webView()->page() && mScriptingHtmlDialog->webView()->page()->mainFrame()) { | 260 | mProcessObject->anythingChanged(); | ||
173 | mScriptingHtmlDialog->webView()->page()->mainFrame()->evaluateJavaScript(QStringLiteral("refresh();")); | 261 | if(mScriptingHtmlDialog && mScriptingHtmlDialog->webView() && mScriptingHtmlDialog->webView()->page()) { | ||
262 | mScriptingHtmlDialog->webView()->page()->runJavaScript(QStringLiteral("refresh && refresh();")); | ||||
174 | } | 263 | } | ||
175 | } | 264 | } | ||
176 | void Scripting::setupJavascriptObjects() { | 265 | void Scripting::setupJavascriptObjects() { | ||
177 | mProcessList->processModel()->update(0, KSysGuard::Processes::XMemory); | 266 | mProcessList->processModel()->update(0, KSysGuard::Processes::XMemory); | ||
178 | mProcessObject = new ProcessObject(mProcessList->processModel(), mPid); | 267 | mProcessObject = new ProcessObject(mProcessList->processModel(), mPid); | ||
179 | mScriptingHtmlDialog->webView()->page()->mainFrame()->addToJavaScriptWindowObject(QStringLiteral("process"), mProcessObject, QWebFrame::ScriptOwnership); | 268 | mWebChannel->registerObject(QStringLiteral("process"), mProcessObject); | ||
269 | mScriptingHtmlDialog->webView()->page()->setWebChannel(mWebChannel); | ||||
180 | } | 270 | } | ||
181 | #endif | 271 | #endif | ||
182 | void Scripting::stopAllScripts() | 272 | void Scripting::stopAllScripts() | ||
183 | { | 273 | { | ||
184 | if (mScriptingHtmlDialog) | 274 | if (mScriptingHtmlDialog) | ||
185 | mScriptingHtmlDialog->deleteLater(); | 275 | mScriptingHtmlDialog->deleteLater(); | ||
186 | mScriptingHtmlDialog = nullptr; | 276 | mScriptingHtmlDialog = nullptr; | ||
187 | mProcessObject = nullptr; | 277 | mProcessObject = nullptr; | ||
▲ Show 20 Lines • Show All 45 Lines • Show Last 20 Lines |