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 }