File indexing completed on 2024-12-29 04:49:58

0001 /*
0002    SPDX-FileCopyrightText: 2017-2021 Volker Krause <vkrause@kde.org>
0003 
0004    SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "extractorscriptengine_p.h"
0008 #include "extractordocumentnode.h"
0009 #include "extractordocumentprocessor.h"
0010 #include "extractorengine.h"
0011 #include "extractorresult.h"
0012 #include "scriptextractor.h"
0013 #include "logging.h"
0014 
0015 #include "jsapi/barcode.h"
0016 #include "jsapi/bytearray.h"
0017 #include "jsapi/extractorengine.h"
0018 #include "jsapi/jsonld.h"
0019 
0020 #include <QFile>
0021 #include <QJSEngine>
0022 #include <QJSValueIterator>
0023 #include <QScopeGuard>
0024 #include <QThread>
0025 #include <QTimer>
0026 
0027 using namespace KItinerary;
0028 
0029 namespace KItinerary {
0030 class ExtractorScriptEnginePrivate {
0031 public:
0032     ~ExtractorScriptEnginePrivate();
0033     bool loadScript(const QString &fileName);
0034 
0035     JsApi::Barcode *m_barcodeApi = nullptr;
0036     JsApi::JsonLd *m_jsonLdApi = nullptr;
0037     JsApi::ExtractorEngine *m_engineApi = nullptr;
0038     QJSEngine m_engine;
0039 
0040     QThread m_watchdogThread;
0041     QTimer *m_watchdogTimer = nullptr;
0042 };
0043 }
0044 
0045 ExtractorScriptEnginePrivate::~ExtractorScriptEnginePrivate()
0046 {
0047     m_watchdogTimer->deleteLater();
0048     m_watchdogThread.quit();
0049     m_watchdogThread.wait();
0050 }
0051 
0052 ExtractorScriptEngine::ExtractorScriptEngine() = default;
0053 ExtractorScriptEngine::~ExtractorScriptEngine() = default;
0054 
0055 void ExtractorScriptEngine::ensureInitialized()
0056 {
0057     if (d) {
0058         return;
0059     }
0060 
0061     d = std::make_unique<ExtractorScriptEnginePrivate>();
0062     d->m_engine.installExtensions(QJSEngine::ConsoleExtension);
0063     d->m_jsonLdApi = new JsApi::JsonLd(&d->m_engine);
0064     d->m_engine.globalObject().setProperty(QStringLiteral("JsonLd"), d->m_engine.newQObject(d->m_jsonLdApi));
0065     d->m_barcodeApi = new JsApi::Barcode;
0066     d->m_engine.globalObject().setProperty(QStringLiteral("Barcode"), d->m_engine.newQObject(d->m_barcodeApi));
0067     d->m_engine.globalObject().setProperty(QStringLiteral("ByteArray"), d->m_engine.newQObject(new JsApi::ByteArray));
0068     d->m_engineApi = new JsApi::ExtractorEngine(&d->m_engine);
0069     d->m_engine.globalObject().setProperty(QStringLiteral("ExtractorEngine"), d->m_engine.newQObject(d->m_engineApi));
0070 
0071     d->m_watchdogThread.start();
0072     d->m_watchdogTimer = new QTimer;
0073     d->m_watchdogTimer->setInterval(std::chrono::seconds(1));
0074     d->m_watchdogTimer->setSingleShot(true);
0075     d->m_watchdogTimer->moveToThread(&d->m_watchdogThread);
0076     QObject::connect(d->m_watchdogTimer, &QTimer::timeout, &d->m_engine, [this]() { d->m_engine.setInterrupted(true); }, Qt::DirectConnection);
0077 }
0078 
0079 void ExtractorScriptEngine::setExtractorEngine(ExtractorEngine *engine)
0080 {
0081     ensureInitialized();
0082     d->m_engineApi->setEngine(engine);
0083     d->m_barcodeApi->setDecoder(engine->barcodeDecoder());
0084 }
0085 
0086 // produce the same output as the JS engine error result fileName property would have
0087 static QString fileNameToUrl(const QString &fileName)
0088 {
0089     if (fileName.startsWith(QLatin1Char(':'))) {
0090       return QLatin1StringView("qrc:/") + QStringView(fileName).mid(1);
0091     }
0092     return QUrl::fromLocalFile(fileName).toString();
0093 }
0094 
0095 static void printScriptError(const QJSValue &result, const QString &fileNameFallback)
0096 {
0097     const auto fileName = result.property(QStringLiteral("fileName"));
0098     // don't change the formatting without adjusting KItinerary Workbench too!
0099     qCWarning(Log).noquote().nospace() << "JS ERROR: [" << (fileName.isString() ? fileName.toString() : fileNameToUrl(fileNameFallback))
0100         << "]:" << result.property(QStringLiteral("lineNumber")).toInt() << ": " << result.toString();
0101 }
0102 
0103 bool ExtractorScriptEnginePrivate::loadScript(const QString &fileName)
0104 {
0105     // TODO we could skip this is if the right script is already loaded
0106     // we cannot do this unconditionally however without breaking KItinerary Workbench's live editing
0107     if (fileName.isEmpty()) {
0108         return false;
0109     }
0110 
0111     QFile f(fileName);
0112     if (!f.open(QFile::ReadOnly)) {
0113         qCWarning(Log) << "Failed to open extractor script" << f.fileName() << f.errorString();
0114         return false;
0115     }
0116 
0117     auto result = m_engine.evaluate(QString::fromUtf8(f.readAll()), f.fileName());
0118     if (result.isError()) {
0119         printScriptError(result, fileName);
0120         return false;
0121     }
0122 
0123     return true;
0124 }
0125 
0126 ExtractorResult ExtractorScriptEngine::execute(const ScriptExtractor *extractor, const ExtractorDocumentNode &node, const ExtractorDocumentNode &triggerNode) const
0127 {
0128     const_cast<ExtractorScriptEngine*>(this)->ensureInitialized();
0129 
0130     // watchdog setup
0131     QMetaObject::invokeMethod(d->m_watchdogTimer, qOverload<>(&QTimer::start));
0132     const auto scopeCleanup = qScopeGuard([this]() {
0133         QMetaObject::invokeMethod(d->m_watchdogTimer, qOverload<>(&QTimer::stop));
0134     });
0135     d->m_engine.setInterrupted(false);
0136 
0137     if (!d->loadScript(extractor->scriptFileName())) {
0138         return {};
0139     }
0140 
0141     auto mainFunc = d->m_engine.globalObject().property(extractor->scriptFunction());
0142     if (!mainFunc.isCallable()) {
0143         qCWarning(Log) << "Script entry point not found!" << extractor->scriptFunction();
0144         return {};
0145     }
0146 
0147     qCDebug(Log) << "Running script extractor" << extractor->scriptFileName() << extractor->scriptFunction();
0148     node.setScriptEngine(&d->m_engine);
0149     triggerNode.setScriptEngine(&d->m_engine);
0150     const auto engineReset = qScopeGuard([&node, &triggerNode, this]{
0151         d->m_engineApi->clear();
0152         node.setScriptEngine(nullptr);
0153         triggerNode.setScriptEngine(nullptr);
0154     });
0155 
0156     d->m_jsonLdApi->setContextDate(node.contextDateTime());
0157     d->m_engineApi->setCurrentNode(node);
0158 
0159     const auto nodeArg = d->m_engine.toScriptValue(node);
0160     const auto dataArg = nodeArg.property(QLatin1StringView("content"));
0161     const auto triggerArg = d->m_engine.toScriptValue(triggerNode);
0162     QJSValueList args{ dataArg, nodeArg, triggerArg };
0163 
0164     const auto result = mainFunc.call(args);
0165     if (result.isError()) {
0166         printScriptError(result, extractor->scriptFileName());
0167         return {};
0168     }
0169 
0170     QJsonArray out;
0171     if (result.isArray()) {
0172         QJSValueIterator it(result);
0173         while (it.hasNext()) {
0174             it.next();
0175             if (it.value().isObject()) {
0176                 out.push_back(QJsonValue::fromVariant(it.value().toVariant()));
0177             }
0178         }
0179     } else if (result.isObject()) {
0180         out.push_back(QJsonValue::fromVariant(result.toVariant()));
0181     } else {
0182         qCWarning(Log) << "Invalid result type from script";
0183     }
0184 
0185     return out;
0186 }