530 lines
16 KiB
C++
530 lines
16 KiB
C++
/*
|
|
* Copyright 2007-2008 Richard J. Moore <rich@kde.org>
|
|
* Copyright 2009 Aaron J. Seigo <aseigo@kde.org>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU Library General Public License version 2 as
|
|
* published by the Free Software Foundation
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details
|
|
*
|
|
* You should have received a copy of the GNU Library General Public
|
|
* License along with this program; if not, write to the
|
|
* Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
*/
|
|
|
|
#include "scriptenv.h"
|
|
|
|
#include <iostream>
|
|
|
|
#include <QFile>
|
|
#include <QMetaEnum>
|
|
|
|
#include <KDebug>
|
|
#include <KDesktopFile>
|
|
#include <KIO/Job>
|
|
#include <KLocale>
|
|
#include <KMimeType>
|
|
#include <KPluginInfo>
|
|
#include <KService>
|
|
#include <KServiceTypeTrader>
|
|
#include <KShell>
|
|
#include <KStandardDirs>
|
|
#include <KRun>
|
|
|
|
#include <Plasma/Package>
|
|
|
|
#ifdef USEGUI
|
|
#include "simplebindings/filedialogproxy.h"
|
|
#endif
|
|
|
|
#include "javascriptaddonpackagestructure.h"
|
|
|
|
Q_DECLARE_METATYPE(ScriptEnv*)
|
|
|
|
ScriptEnv::ScriptEnv(QObject *parent, QScriptEngine *engine)
|
|
: QObject(parent),
|
|
m_allowedUrls(NoUrls),
|
|
m_engine(engine)
|
|
{
|
|
connect(m_engine, SIGNAL(signalHandlerException(QScriptValue)), this, SLOT(signalException()));
|
|
|
|
setupGlobalObject();
|
|
}
|
|
|
|
ScriptEnv::~ScriptEnv()
|
|
{
|
|
}
|
|
|
|
void ScriptEnv::setupGlobalObject()
|
|
{
|
|
QScriptValue global = m_engine->globalObject();
|
|
|
|
// Add an accessor so we can find the scriptenv given only the engine. The
|
|
// property is hidden from scripts.
|
|
global.setProperty("__plasma_scriptenv", m_engine->newQObject(this),
|
|
QScriptValue::ReadOnly|QScriptValue::Undeletable|QScriptValue::SkipInEnumeration);
|
|
|
|
// Add utility functions
|
|
#ifndef DECLARATIVE
|
|
global.setProperty("print", m_engine->newFunction(ScriptEnv::print));
|
|
#endif
|
|
global.setProperty("debug", m_engine->newFunction(ScriptEnv::debug));
|
|
}
|
|
|
|
void ScriptEnv::addMainObjectProperties(QScriptValue &value)
|
|
{
|
|
value.setProperty("listAddons", m_engine->newFunction(ScriptEnv::listAddons));
|
|
value.setProperty("loadAddon", m_engine->newFunction(ScriptEnv::loadAddon));
|
|
value.setProperty("addEventListener", m_engine->newFunction(ScriptEnv::addEventListener));
|
|
value.setProperty("removeEventListener", m_engine->newFunction(ScriptEnv::removeEventListener));
|
|
value.setProperty("hasExtension", m_engine->newFunction(ScriptEnv::hasExtension));
|
|
}
|
|
|
|
QScriptEngine *ScriptEnv::engine() const
|
|
{
|
|
return m_engine;
|
|
}
|
|
|
|
ScriptEnv *ScriptEnv::findScriptEnv(QScriptEngine *engine)
|
|
{
|
|
QScriptValue global = engine->globalObject();
|
|
return qscriptvalue_cast<ScriptEnv*>(global.property("__plasma_scriptenv"));
|
|
}
|
|
|
|
void ScriptEnv::signalException()
|
|
{
|
|
checkForErrors(false);
|
|
}
|
|
|
|
void ScriptEnv::registerEnums(QScriptValue &scriptValue, const QMetaObject &meta)
|
|
{
|
|
//manually create enum values. ugh
|
|
QScriptEngine *engine = scriptValue.engine();
|
|
for (int i = 0; i < meta.enumeratorCount(); ++i) {
|
|
QMetaEnum e = meta.enumerator(i);
|
|
//kDebug() << e.name();
|
|
for (int i=0; i < e.keyCount(); ++i) {
|
|
//kDebug() << e.key(i) << e.value(i);
|
|
scriptValue.setProperty(e.key(i), QScriptValue(engine, e.value(i)));
|
|
}
|
|
}
|
|
}
|
|
|
|
bool ScriptEnv::include(const QString &path)
|
|
{
|
|
QFile file(path);
|
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
|
kWarning() << i18n("Unable to load script file: %1", path);
|
|
return false;
|
|
}
|
|
|
|
QString script = file.readAll();
|
|
//kDebug() << "Script says" << script;
|
|
|
|
// change the context to the parent context so that the include is actually
|
|
// executed in the same context as the caller; seems to be what javascript
|
|
// coders expect :)
|
|
QScriptContext *ctx = m_engine->currentContext();
|
|
if (ctx && ctx->parentContext()) {
|
|
ctx->setActivationObject(ctx->parentContext()->activationObject());
|
|
ctx->setThisObject(ctx->parentContext()->thisObject());
|
|
}
|
|
|
|
m_engine->evaluate(script, path);
|
|
|
|
return !checkForErrors(true);
|
|
}
|
|
|
|
bool ScriptEnv::checkForErrors(bool fatal)
|
|
{
|
|
if (m_engine->hasUncaughtException()) {
|
|
emit reportError(this, fatal);
|
|
if (!fatal) {
|
|
m_engine->clearExceptions();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool ScriptEnv::importBuiltinExtension(const QString &extension, QScriptValue &obj)
|
|
{
|
|
kDebug() << extension;
|
|
if ("filedialog" == extension) {
|
|
#ifdef USEGUI
|
|
FileDialogProxy::registerWithRuntime(m_engine);
|
|
return true;
|
|
#endif
|
|
} else if ("launchapp" == extension) {
|
|
m_allowedUrls |= AppLaunching;
|
|
obj.setProperty("runApplication", m_engine->newFunction(ScriptEnv::runApplication));
|
|
obj.setProperty("runCommand", m_engine->newFunction(ScriptEnv::runCommand));
|
|
registerOpenUrl(obj);
|
|
return true;
|
|
} else if ("http" == extension) {
|
|
m_allowedUrls |= HttpUrls;
|
|
registerGetUrl(obj);
|
|
registerOpenUrl(obj);
|
|
return true;
|
|
} else if ("networkio" == extension) {
|
|
m_allowedUrls |= HttpUrls | NetworkUrls;
|
|
registerGetUrl(obj);
|
|
return true;
|
|
} else if ("localio" == extension) {
|
|
m_allowedUrls |= LocalUrls;
|
|
registerGetUrl(obj);
|
|
obj.setProperty("userDataPath", m_engine->newFunction(ScriptEnv::userDataPath));
|
|
obj.setProperty("runCommand", m_engine->newFunction(ScriptEnv::runCommand));
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool ScriptEnv::importExtensions(const KPluginInfo &info, QScriptValue &obj, Authorization &auth)
|
|
{
|
|
QStringList requiredExtensions = info.service()->property("X-Plasma-RequiredExtensions", QVariant::StringList).toStringList();
|
|
if (!requiredExtensions.isEmpty()) {
|
|
kDebug() << "required extensions are" << requiredExtensions;
|
|
}
|
|
|
|
foreach (const QString &ext, requiredExtensions) {
|
|
QString extension = ext.toLower();
|
|
if (m_extensions.contains(extension)) {
|
|
continue;
|
|
}
|
|
|
|
if (!auth.authorizeRequiredExtension(extension)) {
|
|
return false;
|
|
}
|
|
|
|
if (!importBuiltinExtension(extension, obj)) {
|
|
if (auth.authorizeExternalExtensions()) {
|
|
m_engine->importExtension(extension);
|
|
}
|
|
}
|
|
|
|
if (checkForErrors(true)) {
|
|
return false;
|
|
} else {
|
|
m_extensions << extension;
|
|
}
|
|
}
|
|
|
|
QStringList optionalExtensions = info.service()->property("X-Plasma-OptionalExtensions", QVariant::StringList).toStringList();
|
|
if (!optionalExtensions.isEmpty()) {
|
|
kDebug() << "optional extensions are" << optionalExtensions;
|
|
}
|
|
|
|
foreach (const QString &ext, optionalExtensions) {
|
|
QString extension = ext.toLower();
|
|
|
|
if (m_extensions.contains(extension)) {
|
|
continue;
|
|
}
|
|
|
|
if (!auth.authorizeOptionalExtension(extension)) {
|
|
continue;
|
|
}
|
|
|
|
if (!importBuiltinExtension(extension, obj)) {
|
|
if (auth.authorizeExternalExtensions()) {
|
|
m_engine->importExtension(extension);
|
|
}
|
|
}
|
|
|
|
if (!checkForErrors(false)) {
|
|
m_extensions << extension;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
QSet<QString> ScriptEnv::loadedExtensions() const
|
|
{
|
|
return m_extensions;
|
|
}
|
|
|
|
QScriptValue ScriptEnv::debug(QScriptContext *context, QScriptEngine *engine)
|
|
{
|
|
if (context->argumentCount() != 1) {
|
|
return throwNonFatalError(i18n("debug takes one argument"), context, engine);
|
|
}
|
|
|
|
kDebug() << context->argument(0).toString();
|
|
return engine->undefinedValue();
|
|
}
|
|
|
|
QScriptValue ScriptEnv::throwNonFatalError(const QString &msg, QScriptContext *context, QScriptEngine *engine)
|
|
{
|
|
QScriptValue rv = context->throwError(msg);
|
|
ScriptEnv *env = ScriptEnv::findScriptEnv(engine);
|
|
if (env) {
|
|
env->checkForErrors(false);
|
|
}
|
|
return rv;
|
|
}
|
|
|
|
QScriptValue ScriptEnv::print(QScriptContext *context, QScriptEngine *engine)
|
|
{
|
|
if (context->argumentCount() != 1) {
|
|
return throwNonFatalError(i18n("print() takes one argument"), context, engine);
|
|
}
|
|
|
|
std::cout << context->argument(0).toString().toLocal8Bit().constData() << std::endl;
|
|
return engine->undefinedValue();
|
|
}
|
|
|
|
QScriptValue ScriptEnv::listAddons(QScriptContext *context, QScriptEngine *engine)
|
|
{
|
|
if (context->argumentCount() < 1) {
|
|
return throwNonFatalError(i18n("listAddons takes one argument: addon type"), context, engine);
|
|
}
|
|
|
|
const QString type = context->argument(0).toString();
|
|
|
|
if (type.isEmpty()) {
|
|
return engine->undefinedValue();
|
|
}
|
|
|
|
const QString constraint = QString("[X-KDE-PluginInfo-Category] == '%1'").arg(type);
|
|
KService::List offers = KServiceTypeTrader::self()->query("Plasma/JavascriptAddon", constraint);
|
|
|
|
int i = 0;
|
|
QScriptValue addons = engine->newArray(offers.count());
|
|
foreach (KService::Ptr offer, offers) {
|
|
KPluginInfo info(offer);
|
|
QScriptValue v = engine->newObject();
|
|
v.setProperty("id", info.pluginName(), QScriptValue::ReadOnly);
|
|
v.setProperty("name", info.name(), QScriptValue::ReadOnly);
|
|
addons.setProperty(i++, v);
|
|
}
|
|
|
|
return addons;
|
|
}
|
|
|
|
QScriptValue ScriptEnv::loadAddon(QScriptContext *context, QScriptEngine *engine)
|
|
{
|
|
if (context->argumentCount() < 2) {
|
|
return throwNonFatalError(i18n("loadAddon takes two arguments: addon type and addon name to load"), context, engine);
|
|
}
|
|
|
|
const QString type = context->argument(0).toString();
|
|
const QString plugin = context->argument(1).toString();
|
|
|
|
if (type.isEmpty() || plugin.isEmpty()) {
|
|
return throwNonFatalError(i18n("loadAddon takes two arguments: addon type and addon name to load"), context, engine);
|
|
}
|
|
|
|
const QString constraint = QString("[X-KDE-PluginInfo-Category] == '%1' and [X-KDE-PluginInfo-Name] == '%2'")
|
|
.arg(type, plugin);
|
|
KService::List offers = KServiceTypeTrader::self()->query("Plasma/JavascriptAddon", constraint);
|
|
|
|
if (offers.isEmpty()) {
|
|
return throwNonFatalError(i18n("Failed to find Addon %1 of type %2", plugin, type), context, engine);
|
|
}
|
|
|
|
Plasma::PackageStructure::Ptr structure(new JavascriptAddonPackageStructure);
|
|
const QString subPath = structure->defaultPackageRoot() + '/' + plugin + '/';
|
|
const QString path = KStandardDirs::locate("data", subPath);
|
|
Plasma::Package package(path, structure);
|
|
|
|
QFile file(package.filePath("mainscript"));
|
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
|
return throwNonFatalError(i18n("Failed to open script file for Addon %1: %2", plugin, package.filePath("mainscript")), context, engine);
|
|
}
|
|
|
|
QTextStream buffer(&file);
|
|
QString code(buffer.readAll());
|
|
|
|
QScriptContext *innerContext = engine->pushContext();
|
|
innerContext->activationObject().setProperty("registerAddon", engine->newFunction(ScriptEnv::registerAddon));
|
|
QScriptValue v = engine->newVariant(QVariant::fromValue(package));
|
|
innerContext->activationObject().setProperty("__plasma_package", v,
|
|
QScriptValue::ReadOnly |
|
|
QScriptValue::Undeletable |
|
|
QScriptValue::SkipInEnumeration);
|
|
//kDebug() << "context is" << innerContext;
|
|
engine->evaluate(code, file.fileName());
|
|
engine->popContext();
|
|
|
|
ScriptEnv *env = ScriptEnv::findScriptEnv(engine);
|
|
if (env && env->checkForErrors(false)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
QScriptValue ScriptEnv::registerAddon(QScriptContext *context, QScriptEngine *engine)
|
|
{
|
|
if (context->argumentCount() > 0) {
|
|
QScriptValue func = context->argument(0);
|
|
if (func.isFunction()) {
|
|
QScriptValue obj = func.construct();
|
|
obj.setProperty("__plasma_package",
|
|
context->parentContext()->activationObject().property("__plasma_package"),
|
|
QScriptValue::ReadOnly |
|
|
QScriptValue::Undeletable |
|
|
QScriptValue::SkipInEnumeration);
|
|
|
|
ScriptEnv *env = ScriptEnv::findScriptEnv(engine);
|
|
if (env) {
|
|
QScriptValueList args;
|
|
args << obj;
|
|
env->callEventListeners("addoncreated", args);
|
|
}
|
|
}
|
|
}
|
|
|
|
return engine->undefinedValue();
|
|
}
|
|
|
|
QString ScriptEnv::filePathFromScriptContext(const char *type, const QString &file) const
|
|
{
|
|
//kDebug() << type << file;
|
|
QScriptContext *c = m_engine->currentContext();
|
|
while (c) {
|
|
QScriptValue v = c->activationObject().property("__plasma_package");
|
|
//kDebug() << "variant in parent context?" << v.isVariant();
|
|
if (v.isVariant()) {
|
|
const QString path = v.toVariant().value<Plasma::Package>().filePath(type, file);
|
|
if (!path.isEmpty()) {
|
|
return path;
|
|
}
|
|
}
|
|
|
|
c = c->parentContext();
|
|
}
|
|
|
|
//kDebug() << "fail";
|
|
return QString();
|
|
}
|
|
|
|
QScriptValue ScriptEnv::addEventListener(QScriptContext *context, QScriptEngine *engine)
|
|
{
|
|
if (context->argumentCount() < 2) {
|
|
return false;
|
|
}
|
|
|
|
ScriptEnv *env = ScriptEnv::findScriptEnv(engine);
|
|
if (!env) {
|
|
return false;
|
|
}
|
|
|
|
return env->addEventListener(context->argument(0).toString(), context->argument(1));
|
|
}
|
|
|
|
QScriptValue ScriptEnv::removeEventListener(QScriptContext *context, QScriptEngine *engine)
|
|
{
|
|
if (context->argumentCount() < 2) {
|
|
return false;
|
|
}
|
|
|
|
ScriptEnv *env = ScriptEnv::findScriptEnv(engine);
|
|
if (!env) {
|
|
return false;
|
|
}
|
|
|
|
return env->removeEventListener(context->argument(0).toString(), context->argument(1));
|
|
}
|
|
|
|
QScriptValue ScriptEnv::hasExtension(QScriptContext *context, QScriptEngine *engine)
|
|
{
|
|
if (context->argumentCount() < 1) {
|
|
return false;
|
|
}
|
|
|
|
ScriptEnv *env = ScriptEnv::findScriptEnv(engine);
|
|
if (!env) {
|
|
return false;
|
|
}
|
|
|
|
return env->m_extensions.contains(context->argument(0).toString().toLower());
|
|
}
|
|
|
|
QScriptValue ScriptEnv::callFunction(QScriptValue &func, const QScriptValueList &args, const QScriptValue &activator)
|
|
{
|
|
if (!func.isFunction()) {
|
|
return m_engine->undefinedValue();
|
|
}
|
|
|
|
QScriptContext *ctx = m_engine->pushContext();
|
|
ctx->setActivationObject(activator);
|
|
QScriptValue rv = func.call(activator, args);
|
|
m_engine->popContext();
|
|
|
|
if (m_engine->hasUncaughtException()) {
|
|
emit reportError(this, false);
|
|
m_engine->clearExceptions();
|
|
return m_engine->undefinedValue();
|
|
}
|
|
|
|
return rv;
|
|
}
|
|
|
|
bool ScriptEnv::hasEventListeners(const QString &event) const
|
|
{
|
|
return m_eventListeners.contains(event);
|
|
}
|
|
|
|
bool ScriptEnv::callEventListeners(const QString &event, const QScriptValueList &args)
|
|
{
|
|
if (!m_eventListeners.contains(event.toLower())) {
|
|
return false;
|
|
}
|
|
|
|
QScriptValueList funcs = m_eventListeners.value(event.toLower());
|
|
QMutableListIterator<QScriptValue> it(funcs);
|
|
while (it.hasNext()) {
|
|
callFunction(it.next(), args);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ScriptEnv::addEventListener(const QString &event, const QScriptValue &func)
|
|
{
|
|
if (func.isFunction() && !event.isEmpty()) {
|
|
m_eventListeners[event.toLower()].append(func);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool ScriptEnv::removeEventListener(const QString &event, const QScriptValue &func)
|
|
{
|
|
bool found = false;
|
|
|
|
if (func.isFunction()) {
|
|
QScriptValueList funcs = m_eventListeners.value(event);
|
|
QMutableListIterator<QScriptValue> it(funcs);
|
|
while (it.hasNext()) {
|
|
if (it.next().equals(func)) {
|
|
it.remove();
|
|
found = true;
|
|
}
|
|
}
|
|
|
|
if (funcs.isEmpty()) {
|
|
m_eventListeners.remove(event.toLower());
|
|
} else {
|
|
m_eventListeners.insert(event.toLower(), funcs);
|
|
}
|
|
}
|
|
|
|
return found;
|
|
}
|
|
|
|
#ifndef USEGUI
|
|
#include "scriptenv.moc"
|
|
#endif
|