History

* changes per CR feedback * revert JSON content-type workaround * add specific error check / advice for unanchored module ids * update unit tests

humbletim committed 9 years ago

Commit 52a57151 parent db73c805 files · +65 −128Browse files

Showing the changes to libraries/script-engine/src/ScriptEngine.cpp (1 of 5 files). Show all

Unified · Split

libraries/script-engine/src/ScriptEngine.cpp+32−100
@@ -73,15 +73,11 @@ · 72 unmodified lines
73 73
74#include "MIDIEvent.h"74#include "MIDIEvent.h"
75 75
76const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_MODULE_COMPAT {
77 "com.highfidelity.experimental.enableExtendedModuleCompatbility"
78};
79
80const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS {76const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS {
81 "com.highfidelity.experimental.enableExtendedJSExceptions"77 "com.highfidelity.experimental.enableExtendedJSExceptions"
82};78};
83 79
84static const int MAX_MODULE_ID_LENTGH { 4096 };80static const int MAX_MODULE_ID_LENGTH { 4096 };
85static const int MAX_DEBUG_VALUE_LENGTH { 80 };81static const int MAX_DEBUG_VALUE_LENGTH { 80 };
86 82
87static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS =83static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS =
@@ -96,7 +92,7 @@ int functionSignatureMetaID = qRegisterMetaType<QScriptEngine::FunctionSignature · 8 unmodified lines
96 92
97Q_LOGGING_CATEGORY(scriptengineScript, "hifi.scriptengine.script")93Q_LOGGING_CATEGORY(scriptengineScript, "hifi.scriptengine.script")
98 94
99static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine){95static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine) {
100 QString message = "";96 QString message = "";
101 for (int i = 0; i < context->argumentCount(); i++) {97 for (int i = 0; i < context->argumentCount(); i++) {
102 if (i > 0) {98 if (i > 0) {
@@ -345,11 +341,7 @@ void ScriptEngine::runInThread() { · 242 unmodified lines
345 // The thread interface cannot live on itself, and we want to move this into the thread, so341 // The thread interface cannot live on itself, and we want to move this into the thread, so
346 // the thread cannot have this as a parent.342 // the thread cannot have this as a parent.
347 QThread* workerThread = new QThread();343 QThread* workerThread = new QThread();
348#ifdef Q_OS_LINUX344 workerThread->setObjectName(QString("js:") + getFilename().replace("about:",""));
349 workerThread->setObjectName(QString("js:") + getFilename());
350#else
351 workerThread->setObjectName(QString("Script Thread:") + getFilename());
352#endif
353 moveToThread(workerThread);345 moveToThread(workerThread);
354 346
355 // NOTE: If you connect any essential signals for proper shutdown or cleanup of347 // NOTE: If you connect any essential signals for proper shutdown or cleanup of
@@ -550,7 +542,7 @@ static QScriptValue createScriptableResourcePrototype(QScriptEngine* engine) { · 194 unmodified lines
550 542
551void ScriptEngine::resetModuleCache(bool deleteScriptCache) {543void ScriptEngine::resetModuleCache(bool deleteScriptCache) {
552 if (QThread::currentThread() != thread()) {544 if (QThread::currentThread() != thread()) {
553 executeOnScriptThread([=](){ resetModuleCache(deleteScriptCache); });545 executeOnScriptThread([=]() { resetModuleCache(deleteScriptCache); });
554 return;546 return;
555 }547 }
556 auto jsRequire = globalObject().property("Script").property("require");548 auto jsRequire = globalObject().property("Script").property("require");
@@ -1379,14 +1371,14 @@ QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& re · 822 unmodified lines
1379 auto throwResolveError = [&](const QScriptValue& error) -> QString {1371 auto throwResolveError = [&](const QScriptValue& error) -> QString {
1380 raiseException(error);1372 raiseException(error);
1381 maybeEmitUncaughtException("require.resolve");1373 maybeEmitUncaughtException("require.resolve");
1382 return nullptr;1374 return QString();
1383 };1375 };
1384 1376
1385 // de-fuzz the input a little by restricting to rational sizes1377 // de-fuzz the input a little by restricting to rational sizes
1386 auto idLength = url.toString().length();1378 auto idLength = url.toString().length();
1387 if (idLength < 1 || idLength > MAX_MODULE_ID_LENTGH) {1379 if (idLength < 1 || idLength > MAX_MODULE_ID_LENGTH) {
1388 auto details = QString("rejecting invalid module id size (%1 chars [1,%2])")1380 auto details = QString("rejecting invalid module id size (%1 chars [1,%2])")
1389 .arg(idLength).arg(MAX_MODULE_ID_LENTGH);1381 .arg(idLength).arg(MAX_MODULE_ID_LENGTH);
1390 return throwResolveError(makeError(message.arg(details), "RangeError"));1382 return throwResolveError(makeError(message.arg(details), "RangeError"));
1391 }1383 }
1392 1384
@@ -1402,11 +1394,21 @@ QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& re · 9 unmodified lines
1402 url = resolvePath(moduleId);1394 url = resolvePath(moduleId);
1403 } else {1395 } else {
1404 // check if the moduleId refers to a "system" module1396 // check if the moduleId refers to a "system" module
1405 QString defaultsPath = defaultScriptsLoc.path();1397 QString systemPath = defaultScriptsLoc.path();
1406 QString systemModulePath = QString("%1/modules/%2.js").arg(defaultsPath).arg(moduleId);1398 QString systemModulePath = QString("%1/modules/%2.js").arg(systemPath).arg(moduleId);
1407 url = defaultScriptsLoc;1399 url = defaultScriptsLoc;
1408 url.setPath(systemModulePath);1400 url.setPath(systemModulePath);
1409 if (!QFileInfo(url.toLocalFile()).isFile()) {1401 if (!QFileInfo(url.toLocalFile()).isFile()) {
1402 if (!moduleId.contains("./")) {
1403 // the user might be trying to refer to a relative file without anchoring it
1404 // let's do them a favor and test for that case -- offering specific advice if detected
1405 auto unanchoredUrl = resolvePath("./" + moduleId);
1406 if (QFileInfo(unanchoredUrl.toLocalFile()).isFile()) {
1407 auto msg = QString("relative module ids must be anchored; use './%1' instead")
1408 .arg(moduleId);
1409 return throwResolveError(makeError(message.arg(msg)));
1410 }
1411 }
1410 return throwResolveError(makeError(message.arg("system module not found")));1412 return throwResolveError(makeError(message.arg("system module not found")));
1411 }1413 }
1412 }1414 }
@@ -1469,7 +1471,7 @@ bool ScriptEngine::registerModuleWithParent(const QScriptValue& module, const QS · 56 unmodified lines
1469 if (children.isArray()) {1471 if (children.isArray()) {
1470 auto key = module.property("id");1472 auto key = module.property("id");
1471 auto length = children.property("length").toInt32();1473 auto length = children.property("length").toInt32();
1472 for (int i=0; i < length; i++) {1474 for (int i = 0; i < length; i++) {
1473 if (children.property(i).property("id").strictlyEquals(key)) {1475 if (children.property(i).property("id").strictlyEquals(key)) {
1474 qCDebug(scriptengine_module) << key.toString() << " updating parent.children[" << i << "] = module";1476 qCDebug(scriptengine_module) << key.toString() << " updating parent.children[" << i << "] = module";
1475 children.setProperty(i, module);1477 children.setProperty(i, module);
@@ -1516,10 +1518,10 @@ QScriptValue ScriptEngine::newModule(const QString& modulePath, const QScriptVal · 40 unmodified lines
1516}1518}
1517 1519
1518// synchronously fetch a module's source code using BatchLoader1520// synchronously fetch a module's source code using BatchLoader
1519QScriptValue ScriptEngine::fetchModuleSource(const QString& modulePath, const bool forceDownload) {1521QVariantMap ScriptEngine::fetchModuleSource(const QString& modulePath, const bool forceDownload) {
1520 using UrlMap = QMap<QUrl, QString>;1522 using UrlMap = QMap<QUrl, QString>;
1521 auto scriptCache = DependencyManager::get<ScriptCache>();1523 auto scriptCache = DependencyManager::get<ScriptCache>();
1522 QScriptValue req = newObject();1524 QVariantMap req;
1523 qCDebug(scriptengine_module) << "require.fetchModuleSource: " << QUrl(modulePath).fileName() << QThread::currentThread();1525 qCDebug(scriptengine_module) << "require.fetchModuleSource: " << QUrl(modulePath).fileName() << QThread::currentThread();
1524 1526
1525 auto onload = [=, &req](const UrlMap& data, const UrlMap& _status) {1527 auto onload = [=, &req](const UrlMap& data, const UrlMap& _status) {
@@ -1528,13 +1530,13 @@ QScriptValue ScriptEngine::fetchModuleSource(const QString& modulePath, const bo · 2 unmodified lines
1528 auto contents = data[url];1530 auto contents = data[url];
1529 qCDebug(scriptengine_module) << "require.fetchModuleSource.onload: " << QUrl(url).fileName() << status << QThread::currentThread();1531 qCDebug(scriptengine_module) << "require.fetchModuleSource.onload: " << QUrl(url).fileName() << status << QThread::currentThread();
1530 if (isStopping()) {1532 if (isStopping()) {
1531 req.setProperty("status", "Stopped");1533 req["status"] = "Stopped";
1532 req.setProperty("success", false);1534 req["success"] = false;
1533 } else {1535 } else {
1534 req.setProperty("url", url);1536 req["url"] = url;
1535 req.setProperty("status", status);1537 req["status"] = status;
1536 req.setProperty("success", ScriptCache::isSuccessStatus(status));1538 req["success"] = ScriptCache::isSuccessStatus(status);
1537 req.setProperty("contents", contents, READONLY_HIDDEN_PROP_FLAGS);1539 req["contents"] = contents;
1538 }1540 }
1539 };1541 };
1540 1542
@@ -1661,17 +1663,17 @@ QScriptValue ScriptEngine::require(const QString& moduleId) { · 120 unmodified lines
1661 // download the module source1663 // download the module source
1662 auto req = fetchModuleSource(modulePath, invalidateCache);1664 auto req = fetchModuleSource(modulePath, invalidateCache);
1663 1665
1664 if (!req.property("success").toBool()) {1666 if (!req.contains("success") || !req["success"].toBool()) {
1665 auto error = QString("error retrieving script (%1)").arg(req.property("status").toString());1667 auto error = QString("error retrieving script (%1)").arg(req["status"].toString());
1666 return throwModuleError(modulePath, error);1668 return throwModuleError(modulePath, error);
1667 }1669 }
1668 1670
1669#if DEBUG_JS_MODULES1671#if DEBUG_JS_MODULES
1670 qCDebug(scriptengine_module) << "require.loaded: " <<1672 qCDebug(scriptengine_module) << "require.loaded: " <<
1671 QUrl(req.property("url").toString()).fileName() << req.property("status").toString();1673 QUrl(req["url"].toString()).fileName() << req["status"].toString();
1672#endif1674#endif
1673 1675
1674 auto sourceCode = req.property("contents").toString();1676 auto sourceCode = req["contents"].toString();
1675 1677
1676 if (QUrl(modulePath).fileName().endsWith(".json", Qt::CaseInsensitive)) {1678 if (QUrl(modulePath).fileName().endsWith(".json", Qt::CaseInsensitive)) {
1677 module.setProperty("content-type", "application/json");1679 module.setProperty("content-type", "application/json");
@@ -1679,28 +1681,6 @@ QScriptValue ScriptEngine::require(const QString& moduleId) { · 1 unmodified line
1679 module.setProperty("content-type", "application/javascript");1681 module.setProperty("content-type", "application/javascript");
1680 }1682 }
1681 1683
1682 {
1683 // It seems that many JSON sources don't actually put .json in the URL...
1684 // so for now as a workaround users wanting to indicate JSON parsing mode can
1685 // do so by calling with a custom this context, eg:
1686 //
1687 // var ITEMS_URL = 'https://highfidelity.io/api/v1/marketplace/items';
1688 // var thisObject = { 'content-type': 'application/json' };
1689 // var items = Script.require.call(thisObject, ITEMS_URL + '?category=everything&sort=recent');
1690
1691 auto thisObject = currentContext()->thisObject();
1692 bool calledWithCustomThis = thisObject.isObject() &&
1693 !thisObject.strictlyEquals(globalObject()) &&
1694 !thisObject.toQObject();
1695
1696 if (calledWithCustomThis) {
1697#ifdef DEBUG_JS
1698 _debugDump("this", thisObject);
1699#endif
1700 applyUserOptions(module, thisObject);
1701 }
1702 }
1703
1704 // evaluate the module1684 // evaluate the module
1705 auto result = instantiateModule(module, sourceCode);1685 auto result = instantiateModule(module, sourceCode);
1706 1686
@@ -1721,54 +1701,6 @@ QScriptValue ScriptEngine::require(const QString& moduleId) { · 14 unmodified lines
1721 return module.property("exports");1701 return module.property("exports");
1722}1702}
1723 1703
1724// User-configurable override options
1725void ScriptEngine::applyUserOptions(QScriptValue& module, QScriptValue& options) {
1726 if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
1727 return;
1728 }
1729 if (!options.isValid()) {
1730 return;
1731 }
1732 // options['content-type'] === 'application/json'
1733 // -- allows JSON modules to be used from URLs not ending in .json
1734 if (options.property("content-type").isString()) {
1735 module.setProperty("content-type", options.property("content-type"));
1736 qCDebug(scriptengine_module) << "module['content-type'] =" << module.property("content-type").toString();
1737 }
1738
1739 if (ScriptEngine::_enableExtendedModuleCompatbility.get()) {
1740 auto closure = module.property("__closure__");
1741
1742 auto maybeSetToExports = [&](const QString& key) {
1743 if (options.property(key).toString() == "exports") {
1744 closure.setProperty(key, module.property("exports"));
1745 qCDebug(scriptengine_module) << "module.closure[" << key << "] = exports";
1746 }
1747 };
1748
1749 // options[{key}] = 'exports'
1750 // several "agnostic" modules in the wild are just one step away from being compatible --
1751 // they just didn't know not to look specifically for this, self or global for attaching
1752 // things onto.
1753 maybeSetToExports("global");
1754 maybeSetToExports("self");
1755 maybeSetToExports("this");
1756
1757 // when options is an Object it will get used as the value of "this" during module evaluation
1758 // (which is what one might expect when calling require.call(thisObject, ...))
1759 if (options.isObject()) {
1760 closure.setProperty("this", options);
1761 }
1762
1763 // when options.global is an Object it'll get used as the global object (during evaluation only)
1764 if (options.property("global").isObject()) {
1765 closure.setProperty("global", options.property("global"));
1766 qCDebug(scriptengine_module) << "module.closure['global'] = options.global";
1767 }
1768 }
1769 maybeEmitUncaughtException(__FUNCTION__);
1770}
1771
1772// If a callback is specified, the included files will be loaded asynchronously and the callback will be called1704// If a callback is specified, the included files will be loaded asynchronously and the callback will be called
1773// when all of the files have finished loading.1705// when all of the files have finished loading.
1774// If no callback is specified, the included files will be loaded synchronously and will block execution until1706// If no callback is specified, the included files will be loaded synchronously and will block execution until