File indexing completed on 2024-05-12 17:10:23

0001 /*
0002     SPDX-FileCopyrightText: 2009 Aaron Seigo <aseigo@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "scriptengine.h"
0008 #include "debug.h"
0009 #include "scriptengine_v1.h"
0010 
0011 #include <QDir>
0012 #include <QDirIterator>
0013 #include <QFile>
0014 #include <QFileInfo>
0015 #include <QFutureWatcher>
0016 #include <QJSValueIterator>
0017 #include <QStandardPaths>
0018 
0019 #include <KLocalizedContext>
0020 #include <QDebug>
0021 #include <klocalizedstring.h>
0022 #include <kmimetypetrader.h>
0023 #include <kshell.h>
0024 
0025 #include <KPackage/Package>
0026 #include <KPackage/PackageLoader>
0027 #include <Plasma/Applet>
0028 #include <Plasma/Containment>
0029 #include <Plasma/PluginLoader>
0030 #include <qstandardpaths.h>
0031 
0032 #include "../screenpool.h"
0033 #include "../standaloneappcorona.h"
0034 #include "appinterface.h"
0035 #include "configgroup.h"
0036 #include "containment.h"
0037 #include "panel.h"
0038 #include "widget.h"
0039 
0040 namespace WorkspaceScripting
0041 {
0042 ScriptEngine::ScriptEngine(Plasma::Corona *corona, QObject *parent)
0043     : QJSEngine(parent)
0044     , m_corona(corona)
0045 {
0046     Q_ASSERT(m_corona);
0047     m_appInterface = new AppInterface(this);
0048     connect(m_appInterface, &AppInterface::print, this, &ScriptEngine::print);
0049     m_scriptSelf = globalObject();
0050     m_globalScriptEngineObject = new ScriptEngine::V1(this);
0051     m_localizedContext = new KLocalizedContext(this);
0052     setupEngine();
0053 }
0054 
0055 ScriptEngine::~ScriptEngine()
0056 {
0057 }
0058 
0059 QString ScriptEngine::errorString() const
0060 {
0061     return m_errorString;
0062 }
0063 
0064 QJSValue ScriptEngine::wrap(Plasma::Applet *w)
0065 {
0066     Widget *wrapper = new Widget(w, this);
0067     return newQObject(wrapper);
0068 }
0069 
0070 QJSValue ScriptEngine::wrap(Plasma::Containment *c)
0071 {
0072     Containment *wrapper = isPanel(c) ? new Panel(c, this) : new Containment(c, this);
0073     return newQObject(wrapper);
0074 }
0075 
0076 int ScriptEngine::defaultPanelScreen() const
0077 {
0078     return 1;
0079 }
0080 
0081 QJSValue ScriptEngine::newError(const QString &message)
0082 {
0083     return evaluate(QStringLiteral("new Error('%1');").arg(message));
0084 }
0085 
0086 QString ScriptEngine::onlyExec(const QString &commandLine)
0087 {
0088     if (commandLine.isEmpty()) {
0089         return commandLine;
0090     }
0091 
0092     return KShell::splitArgs(commandLine, KShell::TildeExpand).first();
0093 }
0094 
0095 void ScriptEngine::setupEngine()
0096 {
0097     QJSValue globalScriptEngineObject = newQObject(m_globalScriptEngineObject);
0098     QJSValue localizedContext = newQObject(m_localizedContext);
0099     QJSValue appInterface = newQObject(m_appInterface);
0100 
0101     // AppInterface stuff
0102     // FIXME: this line doesn't have much effect for now, if QTBUG-68397 gets fixed,
0103     // all the connects to rewrite the properties won't be necessary anymore
0104     // globalObject().setPrototype(appInterface);
0105     // FIXME: remove __AppInterface if QTBUG-68397 gets solved
0106     // as workaround we build manually a js object with getters and setters
0107     m_scriptSelf.setProperty(QStringLiteral("__AppInterface"), appInterface);
0108     QJSValue res = evaluate(
0109         "__proto__ = {\
0110                 get locked() {return __AppInterface.locked;},\
0111                 get hasBattery() {return __AppInterface.hasBattery;},\
0112                 get screenCount() {return __AppInterface.screenCount;},\
0113                 get activityIds() {return __AppInterface.activityIds;},\
0114                 get panelIds() {return __AppInterface.panelIds;},\
0115                 get knownPanelTypes() {return __AppInterface.knownPanelTypes;},\
0116                 get knownActivityTypes() {return __AppInterface.knownActivityTypes;},\
0117                 get knownWidgetTypes() {return __AppInterface.knownWidgetTypes;},\
0118                 get theme() {return __AppInterface.theme;},\
0119                 set theme(name) {__AppInterface.theme = name;},\
0120                 get applicationVersion() {return __AppInterface.applicationVersion;},\
0121                 get platformVersion() {return __AppInterface.platformVersion;},\
0122                 get scriptingVersion() {return __AppInterface.scriptingVersion;},\
0123                 get multihead() {return __AppInterface.multihead;},\
0124                 get multiheadScreen() {return __AppInterface.multihead;},\
0125                 get locale() {return __AppInterface.locale;},\
0126                 get language() {return __AppInterface.language;},\
0127                 get languageId() {return __AppInterface.languageId;},\
0128             }");
0129     Q_ASSERT(!res.isError());
0130     // methods from AppInterface
0131     m_scriptSelf.setProperty(QStringLiteral("screenGeometry"), appInterface.property("screenGeometry"));
0132     m_scriptSelf.setProperty(QStringLiteral("lockCorona"), appInterface.property("lockCorona"));
0133     m_scriptSelf.setProperty(QStringLiteral("sleep"), appInterface.property("sleep"));
0134     m_scriptSelf.setProperty(QStringLiteral("print"), appInterface.property("print"));
0135 
0136     m_scriptSelf.setProperty(QStringLiteral("getApiVersion"), globalScriptEngineObject.property("getApiVersion"));
0137 
0138     // Constructors: prefer them js based as they make the c++ code of panel et al way simpler without hacks to get the engine
0139     m_scriptSelf.setProperty(QStringLiteral("__newPanel"), globalScriptEngineObject.property("newPanel"));
0140     m_scriptSelf.setProperty(QStringLiteral("__newConfigFile"), globalScriptEngineObject.property("configFile"));
0141     // definitions of qrectf properties from documentation
0142     // only properties/functions which were already binded are.
0143     // TODO KF6: just a plain QRectF binding
0144     res = evaluate(
0145         "function QRectF(x,y,w,h) {\
0146                 return {x: x, y: y, width: w, height: h,\
0147                         get left() {return this.x},\
0148                         get top() {return this.y},\
0149                         get right() {return this.x + this.width},\
0150                         get bottom() {return this.y + this.height},\
0151                         get empty() {return this.width <= 0 || this.height <= 0},\
0152                         get null() {return this.width == 0 || this.height == 0},\
0153                         get valid() {return !this.empty},\
0154                         adjust: function(dx1, dy1, dx2, dy2) {\
0155                             this.x += dx1; this.y += dy1;\
0156                             this.width = this.width - dx1 + dx2;\
0157                             this.height = this.height - dy1 + dy2;},\
0158                         adjusted: function(dx1, dy1, dx2, dy2) {\
0159                             return new QRectF(this.x + dx1, this.y + dy1,\
0160                                               this.width - dx1 + dx2,\
0161                                               this.height - dy1 + dy2)},\
0162                         translate: function(dx, dy) {this.x += dx; this.y += dy;},\
0163                         setCoords: function(x1, y1, x2, y2) {\
0164                             this.x = x1; this.y = y1;\
0165                             this.width = x2 - x1;\
0166                             this.height = y2 - y1;},\
0167                         setRect: function(x1, y1, w1, h1) {\
0168                             this.x = x1; this.y = y1;\
0169                             this.width = w1; this.height = h1;},\
0170                         contains: function(x1, y1) { return x1 >= this.x && x1 <= this.x + this.width && y1 >= this.y && y1 <= this.y + this.height},\
0171                         moveBottom: function(bottom1) {this.y = bottom1 - this.height;},\
0172                         moveLeft: function(left1) {this.x = left1;},\
0173                         moveRight: function(right1) {this.x = right1 - this.width;},\
0174                         moveTop: function(top1) {this.y = top1;},\
0175                         moveTo: function(x1, y1) {this.x = x1; this.y = y1;}\
0176               }};\
0177               function ConfigFile(config, group){return __newConfigFile(config, group)};\
0178               function Panel(plugin){return __newPanel(plugin)};");
0179     Q_ASSERT(!res.isError());
0180 
0181     m_scriptSelf.setProperty(QStringLiteral("createActivity"), globalScriptEngineObject.property("createActivity"));
0182     m_scriptSelf.setProperty(QStringLiteral("setCurrentActivity"), globalScriptEngineObject.property("setCurrentActivity"));
0183     m_scriptSelf.setProperty(QStringLiteral("currentActivity"), globalScriptEngineObject.property("currentActivity"));
0184     m_scriptSelf.setProperty(QStringLiteral("activities"), globalScriptEngineObject.property("activities"));
0185     m_scriptSelf.setProperty(QStringLiteral("activityName"), globalScriptEngineObject.property("activityName"));
0186     m_scriptSelf.setProperty(QStringLiteral("setActivityName"), globalScriptEngineObject.property("setActivityName"));
0187     m_scriptSelf.setProperty(QStringLiteral("loadSerializedLayout"), globalScriptEngineObject.property("loadSerializedLayout"));
0188     m_scriptSelf.setProperty(QStringLiteral("desktopsForActivity"), globalScriptEngineObject.property("desktopsForActivity"));
0189     m_scriptSelf.setProperty(QStringLiteral("desktops"), globalScriptEngineObject.property("desktops"));
0190     m_scriptSelf.setProperty(QStringLiteral("desktopById"), globalScriptEngineObject.property("desktopById"));
0191     m_scriptSelf.setProperty(QStringLiteral("desktopForScreen"), globalScriptEngineObject.property("desktopForScreen"));
0192     m_scriptSelf.setProperty(QStringLiteral("screenForConnector"), globalScriptEngineObject.property("screenForConnector"));
0193     m_scriptSelf.setProperty(QStringLiteral("panelById"), globalScriptEngineObject.property("panelById"));
0194     m_scriptSelf.setProperty(QStringLiteral("panels"), globalScriptEngineObject.property("panels"));
0195     m_scriptSelf.setProperty(QStringLiteral("fileExists"), globalScriptEngineObject.property("fileExists"));
0196     m_scriptSelf.setProperty(QStringLiteral("loadTemplate"), globalScriptEngineObject.property("loadTemplate"));
0197     m_scriptSelf.setProperty(QStringLiteral("applicationExists"), globalScriptEngineObject.property("applicationExists"));
0198     m_scriptSelf.setProperty(QStringLiteral("defaultApplication"), globalScriptEngineObject.property("defaultApplication"));
0199     m_scriptSelf.setProperty(QStringLiteral("userDataPath"), globalScriptEngineObject.property("userDataPath"));
0200     m_scriptSelf.setProperty(QStringLiteral("applicationPath"), globalScriptEngineObject.property("applicationPath"));
0201     m_scriptSelf.setProperty(QStringLiteral("knownWallpaperPlugins"), globalScriptEngineObject.property("knownWallpaperPlugins"));
0202     m_scriptSelf.setProperty(QStringLiteral("gridUnit"), globalScriptEngineObject.property("gridUnit"));
0203     m_scriptSelf.setProperty(QStringLiteral("setImmutability"), globalScriptEngineObject.property("setImmutability"));
0204     m_scriptSelf.setProperty(QStringLiteral("immutability"), globalScriptEngineObject.property("immutability"));
0205 
0206     // i18n
0207     m_scriptSelf.setProperty(QStringLiteral("i18n"), localizedContext.property("i18n"));
0208     m_scriptSelf.setProperty(QStringLiteral("i18nc"), localizedContext.property("i18nc"));
0209     m_scriptSelf.setProperty(QStringLiteral("i18np"), localizedContext.property("i18np"));
0210     m_scriptSelf.setProperty(QStringLiteral("i18ncp"), localizedContext.property("i18ncp"));
0211     m_scriptSelf.setProperty(QStringLiteral("i18nd"), localizedContext.property("i18nd"));
0212     m_scriptSelf.setProperty(QStringLiteral("i18ndc"), localizedContext.property("i18ndc"));
0213     m_scriptSelf.setProperty(QStringLiteral("i18ndp"), localizedContext.property("i18ndp"));
0214     m_scriptSelf.setProperty(QStringLiteral("i18ndcp"), localizedContext.property("i18ndcp"));
0215 
0216     m_scriptSelf.setProperty(QStringLiteral("xi18n"), localizedContext.property("xi18n"));
0217     m_scriptSelf.setProperty(QStringLiteral("xi18nc"), localizedContext.property("xi18nc"));
0218     m_scriptSelf.setProperty(QStringLiteral("xi18np"), localizedContext.property("xi18np"));
0219     m_scriptSelf.setProperty(QStringLiteral("xi18ncp"), localizedContext.property("xi18ncp"));
0220     m_scriptSelf.setProperty(QStringLiteral("xi18nd"), localizedContext.property("xi18nd"));
0221     m_scriptSelf.setProperty(QStringLiteral("xi18ndc"), localizedContext.property("xi18ndc"));
0222     m_scriptSelf.setProperty(QStringLiteral("xi18ndp"), localizedContext.property("xi18ndp"));
0223     m_scriptSelf.setProperty(QStringLiteral("xi18ndcp"), localizedContext.property("xi18ndcp"));
0224 }
0225 
0226 bool ScriptEngine::isPanel(const Plasma::Containment *c)
0227 {
0228     if (!c) {
0229         return false;
0230     }
0231 
0232     return c->containmentType() == Plasma::Types::PanelContainment || c->containmentType() == Plasma::Types::CustomPanelContainment;
0233 }
0234 
0235 Plasma::Corona *ScriptEngine::corona() const
0236 {
0237     return m_corona;
0238 }
0239 
0240 bool ScriptEngine::evaluateScript(const QString &script, const QString &path)
0241 {
0242     m_errorString = QString();
0243 
0244     QJSValue result = evaluate(script, path);
0245     if (result.isError()) {
0246         QString error = i18n("Error: %1 at line %2\n\nBacktrace:\n%3",
0247                              result.toString(),
0248                              result.property("lineNumber").toInt(),
0249                              result.property("stack").toVariant().value<QStringList>().join(QLatin1String("\n  ")));
0250         Q_EMIT printError(error);
0251         Q_EMIT exception(result);
0252         m_errorString = error;
0253         return false;
0254     }
0255 
0256     return true;
0257 }
0258 
0259 void ScriptEngine::exception(const QJSValue &value)
0260 {
0261     Q_EMIT printError(value.toVariant().toString());
0262 }
0263 
0264 QStringList ScriptEngine::pendingUpdateScripts(Plasma::Corona *corona)
0265 {
0266     if (!corona->kPackage().isValid()) {
0267         qCWarning(PLASMASHELL) << "Warning: corona package invalid";
0268         return QStringList();
0269     }
0270 
0271     const QString appName = corona->kPackage().metadata().pluginId();
0272     QStringList scripts;
0273 
0274     const QStringList dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation,
0275                                                        "plasma/shells/" + appName + QStringLiteral("/contents/updates"),
0276                                                        QStandardPaths::LocateDirectory);
0277     for (const QString &dir : dirs) {
0278         QDirIterator it(dir, QStringList() << QStringLiteral("*.js"));
0279         while (it.hasNext()) {
0280             scripts.append(it.next());
0281         }
0282     }
0283     QStringList scriptPaths;
0284 
0285     if (scripts.isEmpty()) {
0286         return scriptPaths;
0287     }
0288 
0289     KConfigGroup cg(KSharedConfig::openConfig(), "Updates");
0290     QStringList performed = cg.readEntry("performed", QStringList());
0291     const QString localXdgDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
0292 
0293     foreach (const QString &script, scripts) {
0294         if (performed.contains(script)) {
0295             continue;
0296         }
0297 
0298         if (script.startsWith(localXdgDir)) {
0299             continue;
0300         }
0301 
0302         scriptPaths.append(script);
0303         performed.append(script);
0304     }
0305 
0306     cg.writeEntry("performed", performed);
0307     KSharedConfig::openConfig()->sync();
0308     return scriptPaths;
0309 }
0310 
0311 QStringList ScriptEngine::availableActivities() const
0312 {
0313     ShellCorona *sc = qobject_cast<ShellCorona *>(m_corona);
0314     StandaloneAppCorona *ac = qobject_cast<StandaloneAppCorona *>(m_corona);
0315     if (sc) {
0316         return sc->availableActivities();
0317     } else if (ac) {
0318         return ac->availableActivities();
0319     }
0320 
0321     return QStringList();
0322 }
0323 
0324 QList<Containment *> ScriptEngine::desktopsForActivity(const QString &id)
0325 {
0326     QList<Containment *> result;
0327 
0328     // confirm this activity actually exists
0329     bool found = false;
0330     for (const QString &act : availableActivities()) {
0331         if (act == id) {
0332             found = true;
0333             break;
0334         }
0335     }
0336 
0337     if (!found) {
0338         return result;
0339     }
0340 
0341     foreach (Plasma::Containment *c, m_corona->containments()) {
0342         if (c->activity() == id && !isPanel(c)) {
0343             result << new Containment(c, this);
0344         }
0345     }
0346 
0347     if (result.count() == 0) {
0348         // we have no desktops for this activity, so lets make them now
0349         // this can happen when the activity already exists but has never been activated
0350         // with the current shell package and layout.js is run to set up the shell for the
0351         // first time
0352         ShellCorona *sc = qobject_cast<ShellCorona *>(m_corona);
0353         StandaloneAppCorona *ac = qobject_cast<StandaloneAppCorona *>(m_corona);
0354         if (sc) {
0355             foreach (int i, sc->screenIds()) {
0356                 result << new Containment(sc->createContainmentForActivity(id, i), this);
0357             }
0358         } else if (ac) {
0359             const int numScreens = m_corona->numScreens();
0360             for (int i = 0; i < numScreens; ++i) {
0361                 result << new Containment(ac->createContainmentForActivity(id, i), this);
0362             }
0363         }
0364     }
0365 
0366     return result;
0367 }
0368 
0369 Plasma::Containment *ScriptEngine::createContainment(const QString &type, const QString &plugin)
0370 {
0371     bool exists = false;
0372     const QList<KPluginMetaData> list = Plasma::PluginLoader::listContainmentsMetaDataOfType(type);
0373     foreach (const KPluginMetaData &pluginInfo, list) {
0374         if (pluginInfo.pluginId() == plugin) {
0375             exists = true;
0376             break;
0377         }
0378     }
0379 
0380     if (!exists) {
0381         return nullptr;
0382     }
0383 
0384     Plasma::Containment *c = nullptr;
0385     if (type == QLatin1String("Panel")) {
0386         ShellCorona *sc = qobject_cast<ShellCorona *>(m_corona);
0387         StandaloneAppCorona *ac = qobject_cast<StandaloneAppCorona *>(m_corona);
0388         if (sc) {
0389             c = sc->addPanel(plugin);
0390         } else if (ac) {
0391             c = ac->addPanel(plugin);
0392         }
0393     } else {
0394         c = m_corona->createContainment(plugin);
0395     }
0396 
0397     if (c) {
0398         if (type == QLatin1String("Panel")) {
0399             // we have to force lastScreen of the newly created containment,
0400             // or it won't have a screen yet at that point, breaking JS code
0401             // that relies on it
0402             // NOTE: if we'll allow setting a panel screen from JS, it will have to use the following lines as well
0403             KConfigGroup cg = c->config();
0404             cg.writeEntry(QStringLiteral("lastScreen"), 0);
0405             c->restore(cg);
0406         }
0407         c->updateConstraints(Plasma::Types::AllConstraints | Plasma::Types::StartupCompletedConstraint);
0408         c->flushPendingConstraintsEvents();
0409     }
0410 
0411     return c;
0412 }
0413 
0414 Containment *ScriptEngine::createContainmentWrapper(const QString &type, const QString &plugin)
0415 {
0416     Plasma::Containment *c = createContainment(type, plugin);
0417     return isPanel(c) ? new Panel(c, this) : new Containment(c, this);
0418 }
0419 
0420 } // namespace WorkspaceScripting