File indexing completed on 2024-04-28 05:31:44

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