Warning, file /plasma/libksysguard/processui/scripting.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).
0001 /* 0002 KSysGuard, the KDE System Guard 0003 0004 SPDX-FileCopyrightText: 2009 John Tapsell <john.tapsell@kde.org> 0005 SPDX-FileCopyrightText: 2018 Fabian Vogt <fabian@ritter-vogt.de> 0006 0007 SPDX-License-Identifier: LGPL-2.0-or-later 0008 0009 */ 0010 0011 #include "scripting.h" 0012 0013 #include <QAction> 0014 #include <QApplication> 0015 #include <QDialog> 0016 #include <QDir> 0017 #include <QDirIterator> 0018 #include <QFile> 0019 #include <QFileInfo> 0020 #include <QTextStream> 0021 #include <QUrl> 0022 0023 #include "ksysguardprocesslist.h" 0024 #include "processes.h" 0025 0026 #include <KDesktopFile> 0027 #include <KLocalizedString> 0028 #include <KStandardAction> 0029 #include <QDialogButtonBox> 0030 #include <QMessageBox> 0031 #include <QVBoxLayout> 0032 0033 #if WEBENGINE_SCRIPTING_ENABLED 0034 #include <QWebChannel> 0035 #include <QWebEngineProfile> 0036 #include <QWebEngineScriptCollection> 0037 #include <QWebEngineSettings> 0038 #include <QWebEngineUrlRequestInterceptor> 0039 #include <QWebEngineView> 0040 #include <qtwebenginewidgetsversion.h> 0041 #endif 0042 0043 #if WEBENGINE_SCRIPTING_ENABLED 0044 class RemoteUrlInterceptor : public QWebEngineUrlRequestInterceptor 0045 { 0046 public: 0047 RemoteUrlInterceptor(QObject *parent) 0048 : QWebEngineUrlRequestInterceptor(parent) 0049 { 0050 } 0051 void interceptRequest(QWebEngineUrlRequestInfo &info) override 0052 { 0053 // Block non-GET/HEAD requests 0054 if (!QStringList({QStringLiteral("GET"), QStringLiteral("HEAD")}).contains(QString::fromLatin1(info.requestMethod()))) 0055 info.block(true); 0056 0057 // Block remote URLs 0058 if (!QStringList({QStringLiteral("blob"), QStringLiteral("data"), QStringLiteral("file")}).contains(info.requestUrl().scheme())) 0059 info.block(true); 0060 } 0061 }; 0062 #endif 0063 0064 class ScriptingHtmlDialog : public QDialog 0065 { 0066 public: 0067 ScriptingHtmlDialog(QWidget *parent) 0068 : QDialog(parent) 0069 { 0070 QDialogButtonBox *buttonBox = new QDialogButtonBox(this); 0071 buttonBox->setStandardButtons(QDialogButtonBox::Close); 0072 connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); 0073 connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); 0074 0075 #if WEBENGINE_SCRIPTING_ENABLED 0076 QVBoxLayout *layout = new QVBoxLayout; 0077 layout->addWidget(&m_webView); 0078 layout->addWidget(buttonBox); 0079 setLayout(layout); 0080 layout->setContentsMargins(0, 0, 0, 0); 0081 m_webView.settings()->setAttribute(QWebEngineSettings::PluginsEnabled, false); 0082 m_webView.page()->profile()->setUrlRequestInterceptor(new RemoteUrlInterceptor(this)); 0083 #endif 0084 } 0085 #if WEBENGINE_SCRIPTING_ENABLED 0086 QWebEngineView *webView() 0087 { 0088 return &m_webView; 0089 } 0090 0091 protected: 0092 QWebEngineView m_webView; 0093 #endif 0094 }; 0095 0096 ProcessObject::ProcessObject(ProcessModel *model, int pid) 0097 { 0098 mModel = model; 0099 mPid = pid; 0100 } 0101 0102 bool ProcessObject::fileExists(const QString &filename) 0103 { 0104 QFileInfo fileInfo(filename); 0105 return fileInfo.exists(); 0106 } 0107 QString ProcessObject::readFile(const QString &filename) 0108 { 0109 QFile file(filename); 0110 if (!file.open(QIODevice::ReadOnly)) 0111 return QString(); 0112 QTextStream stream(&file); 0113 QString contents = stream.readAll(); 0114 file.close(); 0115 return contents; 0116 } 0117 0118 Scripting::Scripting(KSysGuardProcessList *parent) 0119 : QWidget(parent) 0120 , mProcessList(parent) 0121 { 0122 mScriptingHtmlDialog = nullptr; 0123 loadContextMenu(); 0124 } 0125 void Scripting::runScript(const QString &path, const QString &name) 0126 { 0127 // Record the script name and path for use in the script helper functions 0128 mScriptPath = path; 0129 mScriptName = name; 0130 0131 #if WEBENGINE_SCRIPTING_ENABLED 0132 QUrl fileName = QUrl::fromLocalFile(path + QStringLiteral("index.html")); 0133 if (!mScriptingHtmlDialog) { 0134 mScriptingHtmlDialog = new ScriptingHtmlDialog(this); 0135 mWebChannel = new QWebChannel(mScriptingHtmlDialog); 0136 connect(mScriptingHtmlDialog, &QDialog::rejected, this, &Scripting::stopAllScripts); 0137 // Only show after page loaded to allow for layouting 0138 mScriptingHtmlDialog->connect(mScriptingHtmlDialog->webView(), &QWebEngineView::loadFinished, mScriptingHtmlDialog, &ScriptingHtmlDialog::show); 0139 0140 QAction *refreshAction = new QAction(QStringLiteral("refresh"), mScriptingHtmlDialog); 0141 refreshAction->setShortcut(QKeySequence::Refresh); 0142 connect(refreshAction, &QAction::triggered, this, &Scripting::refreshScript); 0143 mScriptingHtmlDialog->addAction(refreshAction); 0144 0145 QAction *zoomInAction = KStandardAction::zoomIn(this, SLOT(zoomIn()), mScriptingHtmlDialog); 0146 mScriptingHtmlDialog->addAction(zoomInAction); 0147 0148 QAction *zoomOutAction = KStandardAction::zoomOut(this, SLOT(zoomOut()), mScriptingHtmlDialog); 0149 mScriptingHtmlDialog->addAction(zoomOutAction); 0150 } 0151 0152 // Make the process information available to the script 0153 QWebEngineProfile *profile = mScriptingHtmlDialog->webView()->page()->profile(); 0154 QFile webChannelJsFile(QStringLiteral(":/qtwebchannel/qwebchannel.js")); 0155 webChannelJsFile.open(QIODevice::ReadOnly); 0156 QString webChannelJs = QString::fromUtf8(webChannelJsFile.readAll()); 0157 0158 /* Warning: Awful hack ahead! 0159 * WebChannel does not allow synchronous calls so we need to make 0160 * asynchronous calls synchronous. 0161 * The conversion is achieved by caching the result of all readFile 0162 * and fileExists calls and restarting the script on every result until 0163 * all requests can be fulfilled synchronously. 0164 * Another challenge is that WebEngine does not support reading 0165 * files from /proc over file:// (they are always empty) so we need 0166 * to keep using the ProcessObject helper methods. 0167 */ 0168 webChannelJs.append(QStringLiteral(R"JS( 0169 new QWebChannel(window.qt.webChannelTransport, function(channel) { 0170 window.process = channel.objects.process; 0171 window.process.realReadFile = window.process.readFile; 0172 window.process.realFileExists = window.process.fileExists; 0173 var files = {}; // Map of all read files. null means does not exist 0174 window.process.fileExists = function(name, cb) { 0175 if(cb) return window.process.realFileExists(name, cb); 0176 if (files[name] === null) 0177 return false; // Definitely does not exist 0178 if (typeof(files[name]) == 'string') 0179 return true; // Definitely exists 0180 0181 window.process.realFileExists(name, function(r) { 0182 if(!r) { 0183 files[name] = null; 0184 refresh(); 0185 return; 0186 } 0187 window.process.realReadFile(name, function(r) { 0188 files[name] = r; 0189 refresh(); 0190 }); 0191 }); 0192 0193 return true; // Might exist 0194 }; 0195 window.process.readFile = function(name,cb) { 0196 if(cb) return window.process.realReadFile(name, cb); 0197 if (typeof(files[name]) == 'string') 0198 return files[name]; // From cache 0199 0200 window.process.fileExists(name); // Fill the cache 0201 return ''; 0202 }; 0203 refresh && refresh(); 0204 });)JS")); 0205 0206 QWebEngineScript webChannelScript; 0207 webChannelScript.setSourceCode(webChannelJs); 0208 webChannelScript.setName(QStringLiteral("qwebchannel.js")); 0209 webChannelScript.setWorldId(QWebEngineScript::MainWorld); 0210 webChannelScript.setInjectionPoint(QWebEngineScript::DocumentCreation); 0211 webChannelScript.setRunsOnSubFrames(false); 0212 0213 profile->scripts()->insert(webChannelScript); 0214 0215 // Inject a style sheet that follows system colors, otherwise we might end up with black text on dark gray background 0216 const QString styleSheet = 0217 QStringLiteral( 0218 "body { background: %1; color: %2; }" 0219 "a { color: %3; }" 0220 "a:visited { color: %4; } ") 0221 .arg(palette().window().color().name(), palette().text().color().name(), palette().link().color().name(), palette().linkVisited().color().name()); 0222 0223 QString styleSheetJs = QStringLiteral( 0224 "\nvar node = document.createElement('style');" 0225 "node.innerHTML = '%1';" 0226 "document.body.appendChild(node);") 0227 .arg(styleSheet); 0228 0229 QWebEngineScript styleSheetScript; 0230 styleSheetScript.setSourceCode(styleSheetJs); 0231 styleSheetScript.setName(QStringLiteral("stylesheet.js")); 0232 styleSheetScript.setWorldId(QWebEngineScript::MainWorld); 0233 styleSheetScript.setInjectionPoint(QWebEngineScript::DocumentReady); 0234 styleSheetScript.setRunsOnSubFrames(false); 0235 0236 profile->scripts()->insert(styleSheetScript); 0237 0238 setupJavascriptObjects(); 0239 0240 mScriptingHtmlDialog->webView()->load(fileName); 0241 #else 0242 QMessageBox::critical(this, 0243 i18n("QtWebEngineWidgets not available"), 0244 i18n("KSysGuard library was compiled without QtWebEngineWidgets, please contact your distribution.")); 0245 #endif 0246 } 0247 #if WEBENGINE_SCRIPTING_ENABLED 0248 void Scripting::zoomIn() 0249 { 0250 QWebEngineView *webView = mScriptingHtmlDialog->webView(); 0251 webView->setZoomFactor(webView->zoomFactor() * 1.1); 0252 } 0253 void Scripting::zoomOut() 0254 { 0255 QWebEngineView *webView = mScriptingHtmlDialog->webView(); 0256 if (webView->zoomFactor() > 0.1) // Prevent it getting too small 0257 webView->setZoomFactor(webView->zoomFactor() / 1.1); 0258 } 0259 0260 void Scripting::refreshScript() 0261 { 0262 // Call any refresh function, if it exists 0263 mProcessList->processModel()->update(0, KSysGuard::Processes::XMemory); 0264 mProcessObject->anythingChanged(); 0265 if (mScriptingHtmlDialog && mScriptingHtmlDialog->webView() && mScriptingHtmlDialog->webView()->page()) { 0266 mScriptingHtmlDialog->webView()->page()->runJavaScript(QStringLiteral("refresh && refresh();")); 0267 } 0268 } 0269 void Scripting::setupJavascriptObjects() 0270 { 0271 mProcessList->processModel()->update(0, KSysGuard::Processes::XMemory); 0272 mProcessObject = new ProcessObject(mProcessList->processModel(), mPid); 0273 mWebChannel->registerObject(QStringLiteral("process"), mProcessObject); 0274 mScriptingHtmlDialog->webView()->page()->setWebChannel(mWebChannel); 0275 } 0276 #endif 0277 void Scripting::stopAllScripts() 0278 { 0279 if (mScriptingHtmlDialog) 0280 mScriptingHtmlDialog->deleteLater(); 0281 mScriptingHtmlDialog = nullptr; 0282 mProcessObject = nullptr; 0283 mScriptPath.clear(); 0284 mScriptName.clear(); 0285 } 0286 void Scripting::loadContextMenu() 0287 { 0288 // Clear any existing actions 0289 qDeleteAll(mActions); 0290 mActions.clear(); 0291 0292 QStringList scripts; 0293 const QStringList dirs = 0294 QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("ksysguard/scripts/"), QStandardPaths::LocateDirectory); 0295 for (const QString &dir : dirs) { 0296 QDirIterator it(dir, QStringList() << QStringLiteral("*.desktop"), QDir::NoFilter, QDirIterator::Subdirectories); 0297 while (it.hasNext()) { 0298 scripts.append(it.next()); 0299 } 0300 } 0301 0302 foreach (const QString &script, scripts) { 0303 KDesktopFile desktopFile(script); 0304 if (!desktopFile.name().isEmpty() && !desktopFile.noDisplay()) { 0305 QAction *action = new QAction(desktopFile.readName(), this); 0306 action->setToolTip(desktopFile.readComment()); 0307 action->setIcon(QIcon(desktopFile.readIcon())); 0308 QString scriptPath = script; 0309 scriptPath.truncate(scriptPath.lastIndexOf(QLatin1Char('/'))); 0310 action->setProperty("scriptPath", QString(scriptPath + QLatin1Char('/'))); 0311 connect(action, &QAction::triggered, this, &Scripting::runScriptSlot); 0312 mProcessList->addAction(action); 0313 mActions << action; 0314 } 0315 } 0316 } 0317 0318 void Scripting::runScriptSlot() 0319 { 0320 QAction *action = static_cast<QAction *>(sender()); 0321 // All the files for the script should be in the scriptPath 0322 QString path = action->property("scriptPath").toString(); 0323 0324 QList<KSysGuard::Process *> selectedProcesses = mProcessList->selectedProcesses(); 0325 if (selectedProcesses.isEmpty()) 0326 return; 0327 mPid = selectedProcesses[0]->pid(); 0328 0329 runScript(path, action->text()); 0330 }