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 }