return; } PROFILE_RANGE(script, __FUNCTION__); QSharedPointer<ScriptEngines> scriptEngines(_scriptEngines); if (isStopping() || !scriptEngines || scriptEngines->isStopped()) { qCDebug(scriptengine) << "loadEntityScript.start " << entityID.toString() << " but isStopping==" << isStopping() << " || engines->isStopped==" << scriptEngines->isStopped(); return; } if (!hasEntityScriptDetails(entityID)) { // make sure EntityScriptDetails has an entry for this UUID right away // (which allows bailing from the loading/provisioning process early if the Entity gets deleted mid-flight) updateEntityScriptStatus(entityID, EntityScriptStatus::PENDING, "...pending..."); } #ifdef DEBUG_ENTITY_STATES { EntityScriptDetails details; bool hasEntityScript = getEntityScriptDetails(entityID, details); qCDebug(scriptengine) << "loadEntityScript.LOADING: " << entityID.toString() << "(previous: " << (hasEntityScript ? details.status : EntityScriptStatus::PENDING) << ")"; }#endif EntityScriptDetails newDetails; newDetails.scriptText = entityScript; newDetails.status = EntityScriptStatus::LOADING; newDetails.definingSandboxURL = currentSandboxURL; setEntityScriptDetails(entityID, newDetails); auto scriptCache = DependencyManager::get<ScriptCache>(); // note: see EntityTreeRenderer.cpp for shared pointer lifecycle management std::weak_ptr<ScriptManager> weakRef(shared_from_this()); scriptCache->getScriptContents(entityScript, [this, weakRef, entityScript, entityID](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) { std::shared_ptr<ScriptManager> strongRef(weakRef); if (!strongRef) { qCWarning(scriptengine) << "loadEntityScript.contentAvailable -- ScriptManager was deleted during getScriptContents!!"; return; } if (isStopping()) {#ifdef DEBUG_ENTITY_STATES qCDebug(scriptengine) << "loadEntityScript.contentAvailable -- stopping";#endif return; } executeOnScriptThread([=]{#ifdef DEBUG_ENTITY_STATES qCDebug(scriptengine) << "loadEntityScript.contentAvailable" << status << entityID.toString();#endif if (!isStopping() && hasEntityScriptDetails(entityID)) { _contentAvailableQueue[entityID] = { entityID, url, contents, isURL, success, status }; } else {#ifdef DEBUG_ENTITY_STATES qCDebug(scriptengine) << "loadEntityScript.contentAvailable -- aborting";#endif } }); }, forceRedownload);} // The JSDoc is for the callEntityScriptMethod() call in this method.// since all of these operations can be asynch we will always do the actual work in the response handler// for the downloadvoid ScriptManager::entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success , const QString& status) { if (QThread::currentThread() != thread()) {#ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptManager::entityScriptContentAvailable() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " "entityID:" << entityID << "scriptOrURL:" << scriptOrURL << "contents:" << contents << "isURL:" << isURL << "success:" << success;#endif QMetaObject::invokeMethod(this, "entityScriptContentAvailable", Q_ARG(const EntityItemID&, entityID), Q_ARG(const QString&, scriptOrURL), Q_ARG(const QString&, contents), Q_ARG(bool, isURL), Q_ARG(bool, success), Q_ARG(const QString&, status)); return; } #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "ScriptManager::entityScriptContentAvailable() thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]";#endif auto scriptCache = DependencyManager::get<ScriptCache>(); bool isFileUrl = isURL && scriptOrURL.startsWith("file://"); auto fileName = isURL ? scriptOrURL : "about:EmbeddedEntityScript"; QString entityScript; { QWriteLocker locker { &_entityScriptsLock }; entityScript = _entityScripts[entityID].scriptText; } EntityScriptDetails newDetails; newDetails.scriptText = scriptOrURL; // If an error happens below, we want to update newDetails with the new status info // and also abort any pending Entity loads that are waiting on the exact same script URL. auto setError = [&](const QString &errorInfo, const EntityScriptStatus& status) { newDetails.errorInfo = errorInfo; newDetails.status = status; setEntityScriptDetails(entityID, newDetails); }; // NETWORK / FILESYSTEM ERRORS if (!success) { setError("Failed to load script (" + status + ")", EntityScriptStatus::ERROR_LOADING_SCRIPT); return; } // SYNTAX ERRORS //auto syntaxError = _engine->lintScript(contents, fileName); auto program = _engine->newProgram( contents, fileName ); auto syntaxCheck = program->checkSyntax(); if (syntaxCheck->state() != ScriptSyntaxCheckResult::Valid) { auto message = syntaxCheck->errorMessage(); //syntaxError.property("formatted").toString(); //if (message.isEmpty()) { // message = syntaxError.toString(); //} setError(QString("Bad syntax (%1)").arg(message), EntityScriptStatus::ERROR_RUNNING_SCRIPT); //syntaxError.setProperty("detail", entityID.toString()); //V8TODO //emit unhandledException(syntaxError); return; } if (!program) { setError("Bad program (isNull)", EntityScriptStatus::ERROR_RUNNING_SCRIPT); std::shared_ptr<ScriptException> ex = std::make_shared<ScriptEngineException>("Program is Null", "Bad program in entityScriptContentAvailable"); emit unhandledException(ex); return; // done processing script } if (isURL) { setParentURL(scriptOrURL); } // SANITY/PERFORMANCE CHECK USING SANDBOX // V8TODO: can be skipped for now should we implement it before merging V8 branch? /*const int SANDBOX_TIMEOUT = 0.25 * MSECS_PER_SECOND; ScriptEnginePointer sandbox = newScriptEngine(); sandbox->setProcessEventsInterval(SANDBOX_TIMEOUT); ScriptValue testConstructor, exception; if (atoi(getenv("UNSAFE_ENTITY_SCRIPTS") ? getenv("UNSAFE_ENTITY_SCRIPTS") : "0")) { QTimer timeout; timeout.setSingleShot(true); timeout.start(SANDBOX_TIMEOUT); connect(&timeout, &QTimer::timeout, [=, &sandbox]{ qCDebug(scriptengine) << "ScriptManager::entityScriptContentAvailable timeout"; // Guard against infinite loops and non-performant code sandbox->raiseException( sandbox->makeError(sandbox->newValue(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT))) ); }); testConstructor = sandbox->evaluate(program); if (sandbox->hasUncaughtException()) { exception = sandbox->cloneUncaughtException(QString("(preflight %1)").arg(entityID.toString())); sandbox->clearExceptions(); } else if (testConstructor.isError()) { exception = testConstructor; } } else { // ENTITY SCRIPT WHITELIST STARTS HERE auto nodeList = DependencyManager::get<NodeList>(); bool passList = false; // assume unsafe QString whitelistPrefix = "[WHITELIST ENTITY SCRIPTS]"; QList<QString> safeURLPrefixes = { "file:///", "atp:", "cache:" }; safeURLPrefixes += qEnvironmentVariable("EXTRA_WHITELIST").trimmed().split(QRegExp("\\s*,\\s*"), Qt::SkipEmptyParts); // Entity Script Whitelist toggle check. Setting::Handle<bool> whitelistEnabled {"private/whitelistEnabled", false }; if (!whitelistEnabled.get()) { passList = true; } // Pull SAFEURLS from the Interface.JSON settings. QVariant raw = Setting::Handle<QVariant>("private/settingsSafeURLS").get(); QStringList settingsSafeURLS = raw.toString().trimmed().split(QRegExp("\\s*[,\r\n]+\\s*"), Qt::SkipEmptyParts); safeURLPrefixes += settingsSafeURLS; // END Pull SAFEURLS from the Interface.JSON settings. // Get current domain whitelist bypass, in case an entire domain is whitelisted. QString currentDomain = DependencyManager::get<AddressManager>()->getDomainURL().host(); QString domainSafeIP = nodeList->getDomainHandler().getHostname(); QString domainSafeURL = URL_SCHEME_OVERTE + "://" + currentDomain; for (const auto& str : safeURLPrefixes) { if (domainSafeURL.startsWith(str) || domainSafeIP.startsWith(str)) { qCDebug(scriptengine) << whitelistPrefix << "Whitelist Bypassed, entire domain is whitelisted. Current Domain Host: " << nodeList->getDomainHandler().getHostname() << "Current Domain: " << currentDomain; passList = true; } } // END bypass whitelist based on current domain. // Start processing scripts through the whitelist. if (ScriptManager::getContext() == "entity_server") { // If running on the server, do not engage whitelist. passList = true; } else if (!passList) { // If waved through, do not engage whitelist. for (const auto& str : safeURLPrefixes) { qCDebug(scriptengine) << whitelistPrefix << "Script URL: " << scriptOrURL << "TESTING AGAINST" << str << "RESULTS IN" << scriptOrURL.startsWith(str); if (!str.isEmpty() && scriptOrURL.startsWith(str)) { passList = true; qCDebug(scriptengine) << whitelistPrefix << "Script approved."; break; // Bail early since we found a match. } } } // END processing of scripts through the whitelist. if (!passList) { // If the entity failed to pass for any reason, it's blocked and an error is thrown. qCDebug(scriptengine) << whitelistPrefix << "(disabled entity script)" << entityID.toString() << scriptOrURL; exception = _engine->makeError(_engine->newValue("UNSAFE_ENTITY_SCRIPTS == 0")); } else { QTimer timeout; timeout.setSingleShot(true); timeout.start(SANDBOX_TIMEOUT); connect(&timeout, &QTimer::timeout, [=, &sandbox] { qCDebug(scriptengine) << "ScriptManager::entityScriptContentAvailable timeout"; // Guard against infinite loops and non-performant code sandbox->raiseException( sandbox->makeError(sandbox->newValue(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT)))); }); testConstructor = sandbox->evaluate(program); if (sandbox->hasUncaughtException()) { exception = sandbox->cloneUncaughtException(QString("(preflight %1)").arg(entityID.toString())); sandbox->clearExceptions(); } else if (testConstructor.isError()) { exception = testConstructor; } } // ENTITY SCRIPT WHITELIST ENDS HERE, uncomment below for original full disabling. // qCDebug(scriptengine) << "(disabled entity script)" << entityID.toString() << scriptOrURL; // exception = makeError("UNSAFE_ENTITY_SCRIPTS == 0"); } if (exception.isError()) { // create a local copy using makeError to decouple from the sandbox engine exception = _engine->makeError(exception); setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT); emit unhandledException(exception); return; } // CONSTRUCTOR VIABILITY if (!testConstructor.isFunction()) { QString testConstructorType = QString(testConstructor.toVariant().typeName()); if (testConstructorType == "") { testConstructorType = "empty"; } QString testConstructorValue = testConstructor.toString(); if (testConstructorValue.size() > MAX_DEBUG_VALUE_LENGTH) { testConstructorValue = testConstructorValue.mid(0, MAX_DEBUG_VALUE_LENGTH) + "..."; } auto message = QString("failed to load entity script -- expected a function, got %1, %2") .arg(testConstructorType).arg(testConstructorValue); auto err = _engine->makeError(_engine->newValue(message)); err.setProperty("fileName", scriptOrURL); err.setProperty("detail", "(constructor " + entityID.toString() + ")"); setError("Could not find constructor (" + testConstructorType + ")", EntityScriptStatus::ERROR_RUNNING_SCRIPT); emit unhandledException(err); return; // done processing script }*/ // ENTITY SCRIPT WHITELIST STARTS HERE auto nodeList = DependencyManager::get<NodeList>(); bool passList = false; // assume unsafe QString whitelistPrefix = "[WHITELIST ENTITY SCRIPTS]"; QList<QString> safeURLPrefixes = { "file:///", "atp:", "cache:" }; safeURLPrefixes += qEnvironmentVariable("EXTRA_WHITELIST").trimmed().split(QRegExp("\\s*,\\s*"), Qt::SkipEmptyParts); // Entity Script Whitelist toggle check. Setting::Handle<bool> whitelistEnabled {"private/whitelistEnabled", false }; if (!whitelistEnabled.get()) { passList = true; } // Pull SAFEURLS from the Interface.JSON settings. QVariant raw = Setting::Handle<QVariant>("private/settingsSafeURLS").get(); QStringList settingsSafeURLS = raw.toString().trimmed().split(QRegExp("\\s*[,\r\n]+\\s*"), Qt::SkipEmptyParts); safeURLPrefixes += settingsSafeURLS; // END Pull SAFEURLS from the Interface.JSON settings. // Get current domain whitelist bypass, in case an entire domain is whitelisted. QString currentDomain = DependencyManager::get<AddressManager>()->getDomainURL().host(); QString domainSafeIP = nodeList->getDomainHandler().getHostname(); QString domainSafeURL = URL_SCHEME_OVERTE + "://" + currentDomain; for (const auto& str : safeURLPrefixes) { if (domainSafeURL.startsWith(str) || domainSafeIP.startsWith(str)) { qCDebug(scriptengine) << whitelistPrefix << "Whitelist Bypassed, entire domain is whitelisted. Current Domain Host: " << nodeList->getDomainHandler().getHostname() << "Current Domain: " << currentDomain; passList = true; } } // END bypass whitelist based on current domain. // Start processing scripts through the whitelist. if (ScriptManager::getContext() == "entity_server") { // If running on the server, do not engage whitelist. passList = true; } else if (!passList) { // If waved through, do not engage whitelist. for (const auto& str : safeURLPrefixes) { qCDebug(scriptengine) << whitelistPrefix << "Script URL: " << scriptOrURL << "TESTING AGAINST" << str << "RESULTS IN" << scriptOrURL.startsWith(str); if (!str.isEmpty() && scriptOrURL.startsWith(str)) { passList = true; qCDebug(scriptengine) << whitelistPrefix << "Script approved."; break; // Bail early since we found a match. } } } // END processing of scripts through the whitelist. ScriptValue exception; if (!passList) { // If the entity failed to pass for any reason, it's blocked and an error is thrown. qCDebug(scriptengine) << whitelistPrefix << "(disabled entity script)" << entityID.toString() << scriptOrURL; exception = _engine->makeError(_engine->newValue("UNSAFE_ENTITY_SCRIPTS == 0")); } if (exception.isError()) { setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT); auto scriptRuntimeException = std::make_shared<ScriptRuntimeException>(); ScriptValue message = exception.property("stack"); //This contains more details along with the error message scriptRuntimeException->errorMessage = message.toString(); scriptRuntimeException->thrownValue = exception; emit unhandledException(scriptRuntimeException); return; } // ENTITY SCRIPT WHITELIST ENDS HERE, uncomment below for original full disabling. // qCDebug(scriptengine) << "(disabled entity script)" << entityID.toString() << scriptOrURL; // exception = makeError("UNSAFE_ENTITY_SCRIPTS == 0"); // (this feeds into refreshFileScript) int64_t lastModified = 0; if (isFileUrl) { QString file = QUrl(scriptOrURL).toLocalFile(); lastModified = (quint64)QFileInfo(file).lastModified().toMSecsSinceEpoch(); } // THE ACTUAL EVALUATION AND CONSTRUCTION ScriptValue entityScriptConstructor, entityScriptObject; QUrl sandboxURL = currentSandboxURL.isEmpty() ? scriptOrURL : currentSandboxURL; auto initialization = [&]{ entityScriptConstructor = _engine->evaluate(contents, fileName); //V8TODO: check if entityScriptConstructor is a function or not //Throw V8 exception if it's not? entityScriptObject = entityScriptConstructor.construct(); if (_engine->hasUncaughtException()) { // V8TODO: Why were we copying the uncaught exception here? Does anything // actually make use of that? // entityScriptObject = _engine->cloneUncaughtException("(construct " + entityID.toString() + ")"); entityScriptObject = _engine->nullValue(); _engine->clearExceptions(); } }; doWithEnvironment(entityID, sandboxURL, initialization); if (entityScriptObject.isError()) { // auto exception = entityScriptObject; // setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT); // emit unhandledException(exception); // V8TODO: Is this needed? Wouldn't the ScriptManager have already emitted the exception? return; } // ... AND WE HAVE LIFTOFF newDetails.status = EntityScriptStatus::RUNNING; newDetails.scriptObject = entityScriptObject; newDetails.lastModified = lastModified; newDetails.definingSandboxURL = sandboxURL; setEntityScriptDetails(entityID, newDetails); if (isURL) { setParentURL(""); } // if we got this far, then call the preload method callEntityScriptMethod(entityID, "preload"); emit entityScriptPreloadFinished(entityID);} /*@jsdoc * Triggered when the script terminates for a user. * <p>Note: Can only be connected to via <code>this.unoad = function () { ... }</code> in the entity script.</p> * <p class="availableIn"><strong>Supported Script Types:</strong> Client Entity Scripts • Server Entity Scripts</p> * @function Entities.unload * @param {Uuid} entityID - The ID of the entity that the script is running in. * @returns {Signal} */// The JSDoc is for the callEntityScriptMethod() call in this method.void ScriptManager::unloadEntityScript(const EntityItemID& entityID, bool shouldRemoveFromMap) { if (QThread::currentThread() != thread()) {#ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptManager::unloadEntityScript() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " "entityID:" << entityID;#endif QMetaObject::invokeMethod(this, "unloadEntityScript", Q_ARG(const EntityItemID&, entityID), Q_ARG(bool, shouldRemoveFromMap)); return; }#ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "ScriptManager::unloadEntityScript() called on correct thread [" << thread() << "] " "entityID:" << entityID;#endif EntityScriptDetails oldDetails; if (getEntityScriptDetails(entityID, oldDetails)) { auto scriptText = oldDetails.scriptText; if (isEntityScriptRunning(entityID)) { callEntityScriptMethod(entityID, "unload"); }#ifdef DEBUG_ENTITY_STATES else { qCDebug(scriptengine) << "unload called while !running" << entityID << oldDetails.status; }#endif if (shouldRemoveFromMap) { // this was a deleted entity, we've been asked to remove it from the map { QWriteLocker locker { &_entityScriptsLock }; _entityScripts.remove(entityID); } emit entityScriptDetailsUpdated(); } else if (oldDetails.status != EntityScriptStatus::UNLOADED) { EntityScriptDetails newDetails; newDetails.status = EntityScriptStatus::UNLOADED; newDetails.lastModified = QDateTime::currentMSecsSinceEpoch(); // keep scriptText populated for the current need to "debouce" duplicate calls to unloadEntityScript newDetails.scriptText = scriptText; setEntityScriptDetails(entityID, newDetails); } stopAllTimersForEntityScript(entityID); }} QList<EntityItemID> ScriptManager::getListOfEntityScriptIDs() { QReadLocker locker{ &_entityScriptsLock }; return _entityScripts.keys();} void ScriptManager::unloadAllEntityScripts(bool blockingCall) { if (QThread::currentThread() != thread()) {#ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptManager::unloadAllEntityScripts() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]";#endif QMetaObject::invokeMethod(this, "unloadAllEntityScripts", blockingCall ? Qt::BlockingQueuedConnection : Qt::QueuedConnection); return; }#ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "ScriptManager::unloadAllEntityScripts() called on correct thread [" << thread() << "]";#endif QList<EntityItemID> keys; { QReadLocker locker{ &_entityScriptsLock }; keys = _entityScripts.keys(); } foreach(const EntityItemID& entityID, keys) { unloadEntityScript(entityID); } { QWriteLocker locker{ &_entityScriptsLock }; _entityScripts.clear(); } emit entityScriptDetailsUpdated();Lines 2001–2500 of 2763. Previous lines Load more
return; } PROFILE_RANGE(script, __FUNCTION__); QSharedPointer<ScriptEngines> scriptEngines(_scriptEngines); if (isStopping() || !scriptEngines || scriptEngines->isStopped()) { qCDebug(scriptengine) << "loadEntityScript.start " << entityID.toString() << " but isStopping==" << isStopping() << " || engines->isStopped==" << scriptEngines->isStopped(); return; } if (!hasEntityScriptDetails(entityID)) { // make sure EntityScriptDetails has an entry for this UUID right away // (which allows bailing from the loading/provisioning process early if the Entity gets deleted mid-flight) updateEntityScriptStatus(entityID, EntityScriptStatus::PENDING, "...pending..."); } #ifdef DEBUG_ENTITY_STATES { EntityScriptDetails details; bool hasEntityScript = getEntityScriptDetails(entityID, details); qCDebug(scriptengine) << "loadEntityScript.LOADING: " << entityID.toString() << "(previous: " << (hasEntityScript ? details.status : EntityScriptStatus::PENDING) << ")"; } #endif EntityScriptDetails newDetails; newDetails.scriptText = entityScript; newDetails.status = EntityScriptStatus::LOADING; newDetails.definingSandboxURL = currentSandboxURL; setEntityScriptDetails(entityID, newDetails); auto scriptCache = DependencyManager::get<ScriptCache>(); // note: see EntityTreeRenderer.cpp for shared pointer lifecycle management std::weak_ptr<ScriptManager> weakRef(shared_from_this()); scriptCache->getScriptContents(entityScript, [this, weakRef, entityScript, entityID](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) { std::shared_ptr<ScriptManager> strongRef(weakRef); if (!strongRef) { qCWarning(scriptengine) << "loadEntityScript.contentAvailable -- ScriptManager was deleted during getScriptContents!!"; return; } if (isStopping()) { #ifdef DEBUG_ENTITY_STATES qCDebug(scriptengine) << "loadEntityScript.contentAvailable -- stopping"; #endif return; } executeOnScriptThread([=]{ #ifdef DEBUG_ENTITY_STATES qCDebug(scriptengine) << "loadEntityScript.contentAvailable" << status << entityID.toString(); #endif if (!isStopping() && hasEntityScriptDetails(entityID)) { _contentAvailableQueue[entityID] = { entityID, url, contents, isURL, success, status }; } else { #ifdef DEBUG_ENTITY_STATES qCDebug(scriptengine) << "loadEntityScript.contentAvailable -- aborting"; #endif } }); }, forceRedownload); } // The JSDoc is for the callEntityScriptMethod() call in this method. // since all of these operations can be asynch we will always do the actual work in the response handler // for the download void ScriptManager::entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success , const QString& status) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptManager::entityScriptContentAvailable() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " "entityID:" << entityID << "scriptOrURL:" << scriptOrURL << "contents:" << contents << "isURL:" << isURL << "success:" << success; #endif QMetaObject::invokeMethod(this, "entityScriptContentAvailable", Q_ARG(const EntityItemID&, entityID), Q_ARG(const QString&, scriptOrURL), Q_ARG(const QString&, contents), Q_ARG(bool, isURL), Q_ARG(bool, success), Q_ARG(const QString&, status)); return; } #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "ScriptManager::entityScriptContentAvailable() thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]"; #endif auto scriptCache = DependencyManager::get<ScriptCache>(); bool isFileUrl = isURL && scriptOrURL.startsWith("file://"); auto fileName = isURL ? scriptOrURL : "about:EmbeddedEntityScript"; QString entityScript; { QWriteLocker locker { &_entityScriptsLock }; entityScript = _entityScripts[entityID].scriptText; } EntityScriptDetails newDetails; newDetails.scriptText = scriptOrURL; // If an error happens below, we want to update newDetails with the new status info // and also abort any pending Entity loads that are waiting on the exact same script URL. auto setError = [&](const QString &errorInfo, const EntityScriptStatus& status) { newDetails.errorInfo = errorInfo; newDetails.status = status; setEntityScriptDetails(entityID, newDetails); }; // NETWORK / FILESYSTEM ERRORS if (!success) { setError("Failed to load script (" + status + ")", EntityScriptStatus::ERROR_LOADING_SCRIPT); return; } // SYNTAX ERRORS //auto syntaxError = _engine->lintScript(contents, fileName); auto program = _engine->newProgram( contents, fileName ); auto syntaxCheck = program->checkSyntax(); if (syntaxCheck->state() != ScriptSyntaxCheckResult::Valid) { auto message = syntaxCheck->errorMessage(); //syntaxError.property("formatted").toString(); //if (message.isEmpty()) { // message = syntaxError.toString(); //} setError(QString("Bad syntax (%1)").arg(message), EntityScriptStatus::ERROR_RUNNING_SCRIPT); //syntaxError.setProperty("detail", entityID.toString()); //V8TODO //emit unhandledException(syntaxError); return; } if (!program) { setError("Bad program (isNull)", EntityScriptStatus::ERROR_RUNNING_SCRIPT); std::shared_ptr<ScriptException> ex = std::make_shared<ScriptEngineException>("Program is Null", "Bad program in entityScriptContentAvailable"); emit unhandledException(ex); return; // done processing script } if (isURL) { setParentURL(scriptOrURL); } // SANITY/PERFORMANCE CHECK USING SANDBOX // V8TODO: can be skipped for now should we implement it before merging V8 branch? /*const int SANDBOX_TIMEOUT = 0.25 * MSECS_PER_SECOND; ScriptEnginePointer sandbox = newScriptEngine(); sandbox->setProcessEventsInterval(SANDBOX_TIMEOUT); ScriptValue testConstructor, exception; if (atoi(getenv("UNSAFE_ENTITY_SCRIPTS") ? getenv("UNSAFE_ENTITY_SCRIPTS") : "0")) { QTimer timeout; timeout.setSingleShot(true); timeout.start(SANDBOX_TIMEOUT); connect(&timeout, &QTimer::timeout, [=, &sandbox]{ qCDebug(scriptengine) << "ScriptManager::entityScriptContentAvailable timeout"; // Guard against infinite loops and non-performant code sandbox->raiseException( sandbox->makeError(sandbox->newValue(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT))) ); }); testConstructor = sandbox->evaluate(program); if (sandbox->hasUncaughtException()) { exception = sandbox->cloneUncaughtException(QString("(preflight %1)").arg(entityID.toString())); sandbox->clearExceptions(); } else if (testConstructor.isError()) { exception = testConstructor; } } else { // ENTITY SCRIPT WHITELIST STARTS HERE auto nodeList = DependencyManager::get<NodeList>(); bool passList = false; // assume unsafe QString whitelistPrefix = "[WHITELIST ENTITY SCRIPTS]"; QList<QString> safeURLPrefixes = { "file:///", "atp:", "cache:" }; safeURLPrefixes += qEnvironmentVariable("EXTRA_WHITELIST").trimmed().split(QRegExp("\\s*,\\s*"), Qt::SkipEmptyParts); // Entity Script Whitelist toggle check. Setting::Handle<bool> whitelistEnabled {"private/whitelistEnabled", false }; if (!whitelistEnabled.get()) { passList = true; } // Pull SAFEURLS from the Interface.JSON settings. QVariant raw = Setting::Handle<QVariant>("private/settingsSafeURLS").get(); QStringList settingsSafeURLS = raw.toString().trimmed().split(QRegExp("\\s*[,\r\n]+\\s*"), Qt::SkipEmptyParts); safeURLPrefixes += settingsSafeURLS; // END Pull SAFEURLS from the Interface.JSON settings. // Get current domain whitelist bypass, in case an entire domain is whitelisted. QString currentDomain = DependencyManager::get<AddressManager>()->getDomainURL().host(); QString domainSafeIP = nodeList->getDomainHandler().getHostname(); QString domainSafeURL = URL_SCHEME_OVERTE + "://" + currentDomain; for (const auto& str : safeURLPrefixes) { if (domainSafeURL.startsWith(str) || domainSafeIP.startsWith(str)) { qCDebug(scriptengine) << whitelistPrefix << "Whitelist Bypassed, entire domain is whitelisted. Current Domain Host: " << nodeList->getDomainHandler().getHostname() << "Current Domain: " << currentDomain; passList = true; } } // END bypass whitelist based on current domain. // Start processing scripts through the whitelist. if (ScriptManager::getContext() == "entity_server") { // If running on the server, do not engage whitelist. passList = true; } else if (!passList) { // If waved through, do not engage whitelist. for (const auto& str : safeURLPrefixes) { qCDebug(scriptengine) << whitelistPrefix << "Script URL: " << scriptOrURL << "TESTING AGAINST" << str << "RESULTS IN" << scriptOrURL.startsWith(str); if (!str.isEmpty() && scriptOrURL.startsWith(str)) { passList = true; qCDebug(scriptengine) << whitelistPrefix << "Script approved."; break; // Bail early since we found a match. } } } // END processing of scripts through the whitelist. if (!passList) { // If the entity failed to pass for any reason, it's blocked and an error is thrown. qCDebug(scriptengine) << whitelistPrefix << "(disabled entity script)" << entityID.toString() << scriptOrURL; exception = _engine->makeError(_engine->newValue("UNSAFE_ENTITY_SCRIPTS == 0")); } else { QTimer timeout; timeout.setSingleShot(true); timeout.start(SANDBOX_TIMEOUT); connect(&timeout, &QTimer::timeout, [=, &sandbox] { qCDebug(scriptengine) << "ScriptManager::entityScriptContentAvailable timeout"; // Guard against infinite loops and non-performant code sandbox->raiseException( sandbox->makeError(sandbox->newValue(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT)))); }); testConstructor = sandbox->evaluate(program); if (sandbox->hasUncaughtException()) { exception = sandbox->cloneUncaughtException(QString("(preflight %1)").arg(entityID.toString())); sandbox->clearExceptions(); } else if (testConstructor.isError()) { exception = testConstructor; } } // ENTITY SCRIPT WHITELIST ENDS HERE, uncomment below for original full disabling. // qCDebug(scriptengine) << "(disabled entity script)" << entityID.toString() << scriptOrURL; // exception = makeError("UNSAFE_ENTITY_SCRIPTS == 0"); } if (exception.isError()) { // create a local copy using makeError to decouple from the sandbox engine exception = _engine->makeError(exception); setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT); emit unhandledException(exception); return; } // CONSTRUCTOR VIABILITY if (!testConstructor.isFunction()) { QString testConstructorType = QString(testConstructor.toVariant().typeName()); if (testConstructorType == "") { testConstructorType = "empty"; } QString testConstructorValue = testConstructor.toString(); if (testConstructorValue.size() > MAX_DEBUG_VALUE_LENGTH) { testConstructorValue = testConstructorValue.mid(0, MAX_DEBUG_VALUE_LENGTH) + "..."; } auto message = QString("failed to load entity script -- expected a function, got %1, %2") .arg(testConstructorType).arg(testConstructorValue); auto err = _engine->makeError(_engine->newValue(message)); err.setProperty("fileName", scriptOrURL); err.setProperty("detail", "(constructor " + entityID.toString() + ")"); setError("Could not find constructor (" + testConstructorType + ")", EntityScriptStatus::ERROR_RUNNING_SCRIPT); emit unhandledException(err); return; // done processing script }*/ // ENTITY SCRIPT WHITELIST STARTS HERE auto nodeList = DependencyManager::get<NodeList>(); bool passList = false; // assume unsafe QString whitelistPrefix = "[WHITELIST ENTITY SCRIPTS]"; QList<QString> safeURLPrefixes = { "file:///", "atp:", "cache:" }; safeURLPrefixes += qEnvironmentVariable("EXTRA_WHITELIST").trimmed().split(QRegExp("\\s*,\\s*"), Qt::SkipEmptyParts); // Entity Script Whitelist toggle check. Setting::Handle<bool> whitelistEnabled {"private/whitelistEnabled", false }; if (!whitelistEnabled.get()) { passList = true; } // Pull SAFEURLS from the Interface.JSON settings. QVariant raw = Setting::Handle<QVariant>("private/settingsSafeURLS").get(); QStringList settingsSafeURLS = raw.toString().trimmed().split(QRegExp("\\s*[,\r\n]+\\s*"), Qt::SkipEmptyParts); safeURLPrefixes += settingsSafeURLS; // END Pull SAFEURLS from the Interface.JSON settings. // Get current domain whitelist bypass, in case an entire domain is whitelisted. QString currentDomain = DependencyManager::get<AddressManager>()->getDomainURL().host(); QString domainSafeIP = nodeList->getDomainHandler().getHostname(); QString domainSafeURL = URL_SCHEME_OVERTE + "://" + currentDomain; for (const auto& str : safeURLPrefixes) { if (domainSafeURL.startsWith(str) || domainSafeIP.startsWith(str)) { qCDebug(scriptengine) << whitelistPrefix << "Whitelist Bypassed, entire domain is whitelisted. Current Domain Host: " << nodeList->getDomainHandler().getHostname() << "Current Domain: " << currentDomain; passList = true; } } // END bypass whitelist based on current domain. // Start processing scripts through the whitelist. if (ScriptManager::getContext() == "entity_server") { // If running on the server, do not engage whitelist. passList = true; } else if (!passList) { // If waved through, do not engage whitelist. for (const auto& str : safeURLPrefixes) { qCDebug(scriptengine) << whitelistPrefix << "Script URL: " << scriptOrURL << "TESTING AGAINST" << str << "RESULTS IN" << scriptOrURL.startsWith(str); if (!str.isEmpty() && scriptOrURL.startsWith(str)) { passList = true; qCDebug(scriptengine) << whitelistPrefix << "Script approved."; break; // Bail early since we found a match. } } } // END processing of scripts through the whitelist. ScriptValue exception; if (!passList) { // If the entity failed to pass for any reason, it's blocked and an error is thrown. qCDebug(scriptengine) << whitelistPrefix << "(disabled entity script)" << entityID.toString() << scriptOrURL; exception = _engine->makeError(_engine->newValue("UNSAFE_ENTITY_SCRIPTS == 0")); } if (exception.isError()) { setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT); auto scriptRuntimeException = std::make_shared<ScriptRuntimeException>(); ScriptValue message = exception.property("stack"); //This contains more details along with the error message scriptRuntimeException->errorMessage = message.toString(); scriptRuntimeException->thrownValue = exception; emit unhandledException(scriptRuntimeException); return; } // ENTITY SCRIPT WHITELIST ENDS HERE, uncomment below for original full disabling. // qCDebug(scriptengine) << "(disabled entity script)" << entityID.toString() << scriptOrURL; // exception = makeError("UNSAFE_ENTITY_SCRIPTS == 0"); // (this feeds into refreshFileScript) int64_t lastModified = 0; if (isFileUrl) { QString file = QUrl(scriptOrURL).toLocalFile(); lastModified = (quint64)QFileInfo(file).lastModified().toMSecsSinceEpoch(); } // THE ACTUAL EVALUATION AND CONSTRUCTION ScriptValue entityScriptConstructor, entityScriptObject; QUrl sandboxURL = currentSandboxURL.isEmpty() ? scriptOrURL : currentSandboxURL; auto initialization = [&]{ entityScriptConstructor = _engine->evaluate(contents, fileName); //V8TODO: check if entityScriptConstructor is a function or not //Throw V8 exception if it's not? entityScriptObject = entityScriptConstructor.construct(); if (_engine->hasUncaughtException()) { // V8TODO: Why were we copying the uncaught exception here? Does anything // actually make use of that? // entityScriptObject = _engine->cloneUncaughtException("(construct " + entityID.toString() + ")"); entityScriptObject = _engine->nullValue(); _engine->clearExceptions(); } }; doWithEnvironment(entityID, sandboxURL, initialization); if (entityScriptObject.isError()) { // auto exception = entityScriptObject; // setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT); // emit unhandledException(exception); // V8TODO: Is this needed? Wouldn't the ScriptManager have already emitted the exception? return; } // ... AND WE HAVE LIFTOFF newDetails.status = EntityScriptStatus::RUNNING; newDetails.scriptObject = entityScriptObject; newDetails.lastModified = lastModified; newDetails.definingSandboxURL = sandboxURL; setEntityScriptDetails(entityID, newDetails); if (isURL) { setParentURL(""); } // if we got this far, then call the preload method callEntityScriptMethod(entityID, "preload"); emit entityScriptPreloadFinished(entityID); } /*@jsdoc * Triggered when the script terminates for a user. * <p>Note: Can only be connected to via <code>this.unoad = function () { ... }</code> in the entity script.</p> * <p class="availableIn"><strong>Supported Script Types:</strong> Client Entity Scripts • Server Entity Scripts</p> * @function Entities.unload * @param {Uuid} entityID - The ID of the entity that the script is running in. * @returns {Signal} */ // The JSDoc is for the callEntityScriptMethod() call in this method. void ScriptManager::unloadEntityScript(const EntityItemID& entityID, bool shouldRemoveFromMap) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptManager::unloadEntityScript() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " "entityID:" << entityID; #endif QMetaObject::invokeMethod(this, "unloadEntityScript", Q_ARG(const EntityItemID&, entityID), Q_ARG(bool, shouldRemoveFromMap)); return; } #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "ScriptManager::unloadEntityScript() called on correct thread [" << thread() << "] " "entityID:" << entityID; #endif EntityScriptDetails oldDetails; if (getEntityScriptDetails(entityID, oldDetails)) { auto scriptText = oldDetails.scriptText; if (isEntityScriptRunning(entityID)) { callEntityScriptMethod(entityID, "unload"); } #ifdef DEBUG_ENTITY_STATES else { qCDebug(scriptengine) << "unload called while !running" << entityID << oldDetails.status; } #endif if (shouldRemoveFromMap) { // this was a deleted entity, we've been asked to remove it from the map { QWriteLocker locker { &_entityScriptsLock }; _entityScripts.remove(entityID); } emit entityScriptDetailsUpdated(); } else if (oldDetails.status != EntityScriptStatus::UNLOADED) { EntityScriptDetails newDetails; newDetails.status = EntityScriptStatus::UNLOADED; newDetails.lastModified = QDateTime::currentMSecsSinceEpoch(); // keep scriptText populated for the current need to "debouce" duplicate calls to unloadEntityScript newDetails.scriptText = scriptText; setEntityScriptDetails(entityID, newDetails); } stopAllTimersForEntityScript(entityID); } } QList<EntityItemID> ScriptManager::getListOfEntityScriptIDs() { QReadLocker locker{ &_entityScriptsLock }; return _entityScripts.keys(); } void ScriptManager::unloadAllEntityScripts(bool blockingCall) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptManager::unloadAllEntityScripts() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]"; #endif QMetaObject::invokeMethod(this, "unloadAllEntityScripts", blockingCall ? Qt::BlockingQueuedConnection : Qt::QueuedConnection); return; } #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "ScriptManager::unloadAllEntityScripts() called on correct thread [" << thread() << "]"; #endif QList<EntityItemID> keys; { QReadLocker locker{ &_entityScriptsLock }; keys = _entityScripts.keys(); } foreach(const EntityItemID& entityID, keys) { unloadEntityScript(entityID); } { QWriteLocker locker{ &_entityScriptsLock }; _entityScripts.clear(); } emit entityScriptDetailsUpdated();