diff --git a/api/build.gradle b/api/build.gradle index ec41567f313..015584a4179 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -855,25 +855,12 @@ dependencies { BuildUtils.addExternalDependency( project, new ExternalDependency( - 'com.sun.script.js:jsr223-js-engine:Aug2008', - "JSR-223 ScriptEngine for Rhino", - "Scripting Project", - "https://scripting.dev.java.net/", - ExternalDependency.BSD_LICENSE_NAME, - ExternalDependency.BSD_LICENSE_URL, - "JSR-223 ScriptEngine APIs for Rhino" - ) - ) - - BuildUtils.addExternalDependency( - project, - new ExternalDependency( - 'org.mozilla:rhino:1.7R3', + "org.mozilla:rhino:${rhinoVersion}", "Mozilla Rhino", "Mozilla Rhino", - "http://www.mozilla.org/rhino/", - "MPL 1.1", - "http://www.mozilla.org/MPL", + "https://github.com/mozilla/rhino", + "MPL 2.0", + "https://www.mozilla.org/en-US/MPL/2.0/", "Embedded JavaScript engine" ) ) diff --git a/api/src/org/labkey/api/data/triggers/ScriptTrigger.java b/api/src/org/labkey/api/data/triggers/ScriptTrigger.java index 9c8083930b0..d4ea7a363f1 100644 --- a/api/src/org/labkey/api/data/triggers/ScriptTrigger.java +++ b/api/src/org/labkey/api/data/triggers/ScriptTrigger.java @@ -347,15 +347,11 @@ public static ServerContextModuleScript create(Script serverContext) public static Script getServerContext(Container c, User u) { String jsCode = PageFlowUtil.class.getName() + ".jsInitObject(" + ContainerUser.class.getName() + ".create(" + ContainerManager.class.getName() + ".getForId(" + PageFlowUtil.jsString(c.getId()) + "), " + UserManager.class.getName() + ".getUser(" + u.getUserId() + ")), null, null, false).toMap()"; - Context ctx = Context.enter(); - try + + try (Context ctx = Context.enter()) { return ctx.compileString("module.exports = " + jsCode, SERVER_CONTEXT_SCRIPT_NAME + ".js", 1, null); } - finally - { - Context.exit(); - } } interface ScriptFn diff --git a/core/src/org/labkey/core/script/ExternalScriptable.java b/core/src/org/labkey/core/script/ExternalScriptable.java index faa802d7dda..18ae92e2f5d 100644 --- a/core/src/org/labkey/core/script/ExternalScriptable.java +++ b/core/src/org/labkey/core/script/ExternalScriptable.java @@ -30,6 +30,8 @@ import org.mozilla.javascript.ScriptRuntime; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; +import org.mozilla.javascript.Symbol; +import org.mozilla.javascript.SymbolScriptable; import org.mozilla.javascript.Wrapper; import javax.script.Bindings; @@ -47,7 +49,7 @@ * @since 1.6 */ // kevink: changes marked -final class ExternalScriptable implements Scriptable +final class ExternalScriptable implements Scriptable, SymbolScriptable { /* Underlying ScriptContext that we use to store * named variables of this scope. @@ -387,11 +389,9 @@ public Object getDefaultValue(Class typeHint) { Object v = ScriptableObject.getProperty(this, methodName); if (!(v instanceof Function fun)) continue; - Context cx = RhinoScriptEngine.enterContext(); - try { + try (Context cx = Context.enter()) + { v = fun.call(cx, fun.getParentScope(), this, args); - } finally { - cx.exit(); } if (v != null) { if (!(v instanceof Scriptable)) { @@ -417,6 +417,28 @@ public Object getDefaultValue(Class typeHint) { "Cannot find default value for object " + arg); } + @Override + public Object get(Symbol key, Scriptable start) + { + return NOT_FOUND; + } + + @Override + public boolean has(Symbol key, Scriptable start) + { + return false; + } + + @Override + public void put(Symbol key, Scriptable start, Object value) + { + } + + @Override + public void delete(Symbol key) + { + } + /** * Implements the instanceof operator. * diff --git a/core/src/org/labkey/core/script/RhinoCompiledScript.java b/core/src/org/labkey/core/script/RhinoCompiledScript.java index 8bde0339e58..5d46d4dc3f0 100644 --- a/core/src/org/labkey/core/script/RhinoCompiledScript.java +++ b/core/src/org/labkey/core/script/RhinoCompiledScript.java @@ -25,10 +25,8 @@ package org.labkey.core.script; -import com.sun.phobos.script.util.ExtendedScriptException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.labkey.api.util.ExceptionUtil; import org.mozilla.javascript.Context; import org.mozilla.javascript.JavaScriptException; import org.mozilla.javascript.RhinoException; @@ -40,14 +38,7 @@ import javax.script.ScriptEngine; import javax.script.ScriptException; -/** - * Represents compiled JavaScript code. - * - * @author Mike Grogan - * @version 1.0 - * @since 1.6 - */ -// kevink: Essentially the same as the original, with changes marked with kevink +/** Represents compiled JavaScript code. */ final class RhinoCompiledScript extends CompiledScript { private final Logger _log = LogManager.getLogger(RhinoCompiledScript.class); @@ -65,32 +56,17 @@ public Object eval(ScriptContext context) throws ScriptException { Object result; - Context cx = RhinoScriptEngine.enterContext(); - try { - + try (Context cx = Context.enter()) + { Scriptable scope = engine.getRuntimeScope(context); - Object ret = script.exec(cx, scope); + Object ret = script.exec(cx, scope, scope); result = engine.unwrapReturnValue(ret); } catch (JavaScriptException jse) { _log.debug(jse); - int line = (line = jse.lineNumber()) == 0 ? -1 : line; - Object value = jse.getValue(); - String str = (value != null && value.getClass().getName().equals("org.mozilla.javascript.NativeError") ? - value.toString() : - jse.toString()); - // kevink: suppress mothership logging. - ScriptException ex = new ExtendedScriptException(jse, str, jse.sourceName(), line); - ExceptionUtil.decorateException(ex, ExceptionUtil.ExceptionInfo.SkipMothershipLogging, "true", true); - throw ex; + throw RhinoScriptEngine.toScriptException(jse); } catch (RhinoException re) { _log.debug(re); - int line = (line = re.lineNumber()) == 0 ? -1 : line; - // kevink: suppress mothership logging. - ScriptException ex = new ExtendedScriptException(re, re.toString(), re.sourceName(), line); - ExceptionUtil.decorateException(ex, ExceptionUtil.ExceptionInfo.SkipMothershipLogging, "true", true); - throw ex; - } finally { - Context.exit(); + throw RhinoScriptEngine.toScriptException(re); } return result; diff --git a/core/src/org/labkey/core/script/RhinoScriptEngine.java b/core/src/org/labkey/core/script/RhinoScriptEngine.java index 63b66749430..26766c47b8e 100644 --- a/core/src/org/labkey/core/script/RhinoScriptEngine.java +++ b/core/src/org/labkey/core/script/RhinoScriptEngine.java @@ -1,32 +1,24 @@ - /* - * Copyright (C) 2006 Sun Microsystems, Inc. All rights reserved. - * Use is subject to license terms. + * Copyright (c) 2024 LabKey Corporation * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: Redistributions of source code - * must retain the above copyright notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list of - * conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. Neither the name of the Sun Microsystems nor the names of - * is contributors may be used to endorse or promote products derived from this software - * without specific prior written permission. - - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS - * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER - * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE - * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Portions of this file are derived from the Phobos scripting engine + * (Copyright (C) 2006 Sun Microsystems, Inc., BSD license) and the + * Helma scripting engine (Copyright 2006 Hannes Wallnoefer, Apache 2.0). */ package org.labkey.core.script; -import com.sun.phobos.script.javascript.RhinoScriptEngineFactory; -import com.sun.phobos.script.util.ExtendedScriptException; -import com.sun.phobos.script.util.InterfaceImplementor; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.labkey.api.reports.LabKeyScriptEngine; @@ -35,12 +27,10 @@ import org.mozilla.javascript.Function; import org.mozilla.javascript.ImporterTopLevel; import org.mozilla.javascript.JavaScriptException; -import org.mozilla.javascript.LazilyLoadedCtor; import org.mozilla.javascript.RhinoException; import org.mozilla.javascript.Script; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; -import org.mozilla.javascript.Synchronizer; import org.mozilla.javascript.Undefined; import org.mozilla.javascript.Wrapper; @@ -50,537 +40,271 @@ import javax.script.CompiledScript; import javax.script.Invocable; import javax.script.ScriptContext; -import javax.script.ScriptEngine; import javax.script.ScriptEngineFactory; import javax.script.ScriptException; import javax.script.SimpleBindings; -import javax.script.SimpleScriptContext; -import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; import java.io.Reader; import java.io.StringReader; -import java.lang.reflect.Method; +import java.lang.reflect.Proxy; import java.util.HashMap; import java.util.Map; /** - * Implementation of ScriptEngine using the Mozilla Rhino - * interpreter. - * - * @author Mike Grogan - * @author A. Sundararajan - * @version 1.0 - * @since 1.6 - * - * Modified for phobos to remove some of the restrictions. - * Modified to allow subclassing and preprocessing of script source code. - * Modified to avoid using the RhinoTopLevel class, since that introduces - * a circularity that prevents objects from being garbage collected. - * - * @author Roberto Chinnici - * + * JSR-223 ScriptEngine implementation backed by Mozilla Rhino. + * Protected methods serve as extension points for {@link RhinoEngine}. */ -// kevink: Essentially the same as the original, with changes marked with kevink public class RhinoScriptEngine extends AbstractScriptEngine implements LabKeyScriptEngine, Invocable, Compilable { private final Logger _log = LogManager.getLogger(RhinoScriptEngine.class); - public static final boolean DEBUG = false; - private static final String TOPLEVEL_SCRIPT_NAME = "META-INF/toplevel.js"; - - /* Scope where standard JavaScript objects and our - * extensions to it are stored. Note that these are not - * user defined engine level global variables. These are - * variables have to be there on all compliant ECMAScript - * scopes. We put these standard objects in this top level. - */ private final ScriptableObject topLevel; - - /* map used to store indexed properties in engine scope - * refer to comment on 'indexedProps' in ExternalScriptable.java. - */ - private final Map indexedProps; - + private final Map indexedProps; private ScriptEngineFactory factory; - private final InterfaceImplementor implementor; - - /* - // in Phobos we want to support all javascript features - static { - ContextFactory.initGlobal(new ContextFactory() { - protected Context makeContext() { - Context cx = super.makeContext(); - cx.setClassShutter(RhinoClassShutter.getInstance()); - cx.setWrapFactory(RhinoWrapFactory.getInstance()); - return cx; - } - - public boolean hasFeature(Context cx, int feature) { - // we do not support E4X (ECMAScript for XML)! - if (feature == Context.FEATURE_E4X) { - return false; - } else { - return super.hasFeature(cx, feature); - } - } - }); - } - - static { - if (USE_INTERPRETER) { - ContextFactory.initGlobal(new ContextFactory() { - protected Context makeContext() { - Context cx = super.makeContext(); - cx.setOptimizationLevel(-1); - return cx; - } - }); - } - } - */ - - /** - * Creates a new instance of RhinoScriptEngine - */ - public RhinoScriptEngine() { + protected RhinoScriptEngine() + { topLevel = createTopLevel(); - - indexedProps = new HashMap(); - - //construct object used to implement getInterface - implementor = new InterfaceImplementor(this) { - @Override - protected Object convertResult(Method method, Object res) - { - Class desiredType = method.getReturnType(); - if (desiredType == Void.TYPE) { - return null; - } else { - // kevink: use our converter - return ScriptUtils.jsToJava(res, desiredType); - } - } - }; + indexedProps = new HashMap<>(); } - // kevink: expose topLevel as protected protected ScriptableObject createTopLevel() { - Context cx = enterContext(); - ScriptableObject top; - try { - /* - * RRC - modified this code to register JSAdapter and some functions - * directly, without using a separate RhinoTopLevel class - */ - top = new ImporterTopLevel(cx, false); - new LazilyLoadedCtor(top, "JSAdapter", - "com.sun.phobos.script.javascript.JSAdapter", - false); - // add top level functions - String names[] = { "bindings", "scope", "sync" }; - top.defineFunctionProperties(names, RhinoScriptEngine.class, ScriptableObject.DONTENUM); - - processAllTopLevelScripts(cx); - } finally { - cx.exit(); + try (Context cx = Context.enter()) + { + return new ImporterTopLevel(cx, false); } - return top; } - // kevink: expose topLevel as protected protected ScriptableObject getTopLevel() { return topLevel; } @Override - public Object eval(Reader reader, ScriptContext ctxt) - throws ScriptException { - Object ret; - - Context cx = enterContext(); - try { + public Object eval(Reader reader, ScriptContext ctxt) throws ScriptException + { + try (Context cx = Context.enter()) + { Scriptable scope = getRuntimeScope(ctxt); scope.put("context", scope, ctxt); - - // NOTE (RRC) - why does it look straight into the engine instead of asking - // the given ScriptContext object? - // Modified to use the context - // String filename = (String) get(ScriptEngine.FILENAME); - String filename = null; - if (ctxt != null && ctxt.getBindings(ScriptContext.ENGINE_SCOPE) != null) { - filename = (String) ctxt.getBindings(ScriptContext.ENGINE_SCOPE).get(ScriptEngine.FILENAME); - } - if (filename == null) { - filename = (String) get(ScriptEngine.FILENAME); - } - - filename = filename == null ? "" : filename; - ret = cx.evaluateReader(scope, preProcessScriptSource(reader), filename , 1, null); - } catch (JavaScriptException jse) { + Object ret = cx.evaluateReader(scope, preProcessScriptSource(reader), getFilename(ctxt), 1, null); + return unwrapReturnValue(ret); + } + catch (JavaScriptException jse) + { _log.debug(jse); - int line = (line = jse.lineNumber()) == 0 ? -1 : line; - Object value = jse.getValue(); - String str = (value != null && value.getClass().getName().equals("org.mozilla.javascript.NativeError") ? - value.toString() : - jse.toString()); - // kevink: suppress mothership logging. - ScriptException ex = new ExtendedScriptException(jse, str, jse.sourceName(), line); - ExceptionUtil.decorateException(ex, ExceptionUtil.ExceptionInfo.SkipMothershipLogging, "true", true); - throw ex; - } catch (RhinoException re) { + throw toScriptException(jse); + } + catch (RhinoException re) + { _log.debug(re); - int line = (line = re.lineNumber()) == 0 ? -1 : line; - // kevink: suppress mothership logging. - ScriptException ex = new ExtendedScriptException(re, re.toString(), re.sourceName(), line); - ExceptionUtil.decorateException(ex, ExceptionUtil.ExceptionInfo.SkipMothershipLogging, "true", true); - throw ex; - } catch (IOException ee) { + throw toScriptException(re); + } + catch (IOException ee) + { throw new ScriptException(ee); - } finally { - cx.exit(); } - - return unwrapReturnValue(ret); } @Override - public Object eval(String script, ScriptContext ctxt) throws ScriptException { - if (script == null) { + public Object eval(String script, ScriptContext ctxt) throws ScriptException + { + if (script == null) throw new NullPointerException("null script"); - } - return eval(preProcessScriptSource(new StringReader(script)) , ctxt); + return eval(new StringReader(script), ctxt); } @Override - public ScriptEngineFactory getFactory() { - if (factory != null) { - return factory; - } else { - return new RhinoScriptEngineFactory(); - } + public ScriptEngineFactory getFactory() + { + return factory; } @Override - public Bindings createBindings() { + public Bindings createBindings() + { return new SimpleBindings(); } - //Invocable methods @Override - public Object invokeFunction(String name, Object... args) - throws ScriptException, NoSuchMethodException { + public Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException + { return invokeMethod(null, name, args); } @Override - public Object invokeMethod(Object thiz, String name, Object... args) - throws ScriptException, NoSuchMethodException { - - Context cx = enterContext(); - try { - if (name == null) { + public Object invokeMethod(Object thiz, String name, Object... args) throws ScriptException, NoSuchMethodException + { + try (Context cx = Context.enter()) + { + if (name == null) throw new NullPointerException("method name is null"); - } - if (thiz != null && !(thiz instanceof Scriptable)) { + if (thiz != null && !(thiz instanceof Scriptable)) thiz = cx.toObject(thiz, topLevel); - } - Scriptable localScope = (thiz != null)? (Scriptable) thiz : - getRuntimeScope(context); + Scriptable localScope = (thiz != null) ? (Scriptable) thiz : getRuntimeScope(context); Object obj = ScriptableObject.getProperty(localScope, name); - if (! (obj instanceof Function func)) { + if (!(obj instanceof Function func)) throw new NoSuchMethodException("no such method: " + name); - } Scriptable scope = func.getParentScope(); - if (scope == null) { + if (scope == null) scope = getRuntimeScope(context); - } - Object result = func.call(cx, scope, localScope, - wrapArguments(args)); + Object result = func.call(cx, scope, localScope, wrapArguments(args)); return unwrapReturnValue(result); - } catch (JavaScriptException jse) { + } + catch (JavaScriptException jse) + { _log.debug(jse); - int line = (line = jse.lineNumber()) == 0 ? -1 : line; - Object value = jse.getValue(); - String str = (value != null && value.getClass().getName().equals("org.mozilla.javascript.NativeError") ? - value.toString() : - jse.toString()); - // kevink: suppress mothership logging. - ScriptException ex = new ExtendedScriptException(jse, str, jse.sourceName(), line); - ExceptionUtil.decorateException(ex, ExceptionUtil.ExceptionInfo.SkipMothershipLogging, "true", true); - throw ex; - } catch (RhinoException re) { + throw toScriptException(jse); + } + catch (RhinoException re) + { _log.debug(re); - int line = (line = re.lineNumber()) == 0 ? -1 : line; - // kevink: Throw our exception class to suppress mothership logging. - ScriptException ex = new ExtendedScriptException(re, re.toString(), re.sourceName(), line); - ExceptionUtil.decorateException(ex, ExceptionUtil.ExceptionInfo.SkipMothershipLogging, "true", true); - throw ex; - } finally { - cx.exit(); + throw toScriptException(re); } } + @SuppressWarnings("unchecked") @Override - public T getInterface(Class clasz) { - try { - return implementor.getInterface(null, clasz); - } catch (ScriptException e) { + public T getInterface(Class clasz) + { + if (clasz == null || !clasz.isInterface()) return null; - } + return (T) Proxy.newProxyInstance( + Thread.currentThread().getContextClassLoader(), + new Class[]{clasz}, + (proxy, method, args) -> { + try + { + Object result = invokeFunction(method.getName(), args != null ? args : new Object[0]); + Class rt = method.getReturnType(); + return rt == Void.TYPE ? null : ScriptUtils.jsToJava(result, rt); + } + catch (NoSuchMethodException e) + { + return null; + } + }); } + @SuppressWarnings("unchecked") @Override - public T getInterface(Object thiz, Class clasz) { - if (thiz == null) { + public T getInterface(Object thiz, Class clasz) + { + if (thiz == null) throw new IllegalArgumentException("script object can not be null"); - } - - try { - return implementor.getInterface(thiz, clasz); - } catch (ScriptException e) { + if (clasz == null || !clasz.isInterface()) return null; - } - } - - private static final String printSource = - "var console = { }; \n" + - "console.log = function (str) { \n" + - " if (typeof(str) == 'undefined') { \n" + - " str = 'undefined'; \n" + - " } else if (str == null) { \n" + - " str = 'null'; \n" + - " } \n" + - " context.getWriter().print(String(str)); \n" + - "}"; - - // kevink: expose as protected - protected Scriptable getRuntimeScope(ScriptContext ctxt) { - if (ctxt == null) { - throw new NullPointerException("null script context"); - } - - // we create a scope for the given ScriptContext - Scriptable newScope = new ExternalScriptable(ctxt, indexedProps); - - // Set the prototype of newScope to be 'topLevel' so that - // JavaScript standard objects are visible from the scope. - newScope.setPrototype(topLevel); - - // define "context" variable in the new scope - newScope.put("context", newScope, ctxt); - -// kevink: we now use 'var console = require("console")' instead -// // define "print" function in the new scope -// Context cx = enterContext(); -// try { -// cx.evaluateString(newScope, printSource, "print", 1, null); -// } finally { -// cx.exit(); -// } - return newScope; + return (T) Proxy.newProxyInstance( + Thread.currentThread().getContextClassLoader(), + new Class[]{clasz}, + (proxy, method, args) -> { + try + { + Object result = invokeMethod(thiz, method.getName(), args != null ? args : new Object[0]); + Class rt = method.getReturnType(); + return rt == Void.TYPE ? null : ScriptUtils.jsToJava(result, rt); + } + catch (NoSuchMethodException e) + { + return null; + } + }); } - - //Compilable methods @Override - public CompiledScript compile(String script) throws ScriptException { - return compile(preProcessScriptSource(new StringReader(script))); + public CompiledScript compile(String script) throws ScriptException + { + return compile(new StringReader(script)); } @Override - public CompiledScript compile(java.io.Reader script) throws ScriptException { - CompiledScript ret; - Context cx = enterContext(); - - try { - String filename = (String) get(ScriptEngine.FILENAME); - if (filename == null) { + public CompiledScript compile(Reader script) throws ScriptException + { + try (Context cx = Context.enter()) + { + String filename = (String) get(javax.script.ScriptEngine.FILENAME); + if (filename == null) filename = ""; - } - Script scr = cx.compileReader(preProcessScriptSource(script), filename, 1, null); - ret = new RhinoCompiledScript(this, scr); - } catch (Exception e) { + return new RhinoCompiledScript(this, scr); + } + catch (Exception e) + { _log.debug(e); throw new ScriptException(e); - } finally { - cx.exit(); } - return ret; } + protected Scriptable getRuntimeScope(ScriptContext ctxt) + { + if (ctxt == null) + throw new NullPointerException("null script context"); + Scriptable newScope = new ExternalScriptable(ctxt, indexedProps); + newScope.setPrototype(topLevel); + newScope.put("context", newScope, ctxt); + return newScope; + } - //package-private helpers - - static Context enterContext() { - // call this always so that initializer of this class runs - // and initializes custom wrap factory and class shutter. - return Context.enter(); + protected Reader preProcessScriptSource(Reader reader) + { + return reader; } - // kevink: expose as protected - protected void setEngineFactory(ScriptEngineFactory fac) { + protected void setEngineFactory(ScriptEngineFactory fac) + { factory = fac; } - Object[] wrapArguments(Object[] args) { - if (args == null) { + Object[] wrapArguments(Object[] args) + { + if (args == null) return Context.emptyArgs; - } Object[] res = new Object[args.length]; - for (int i = 0; i < res.length; i++) { + for (int i = 0; i < res.length; i++) res[i] = Context.javaToJS(args[i], topLevel); - } return res; } - Object unwrapReturnValue(Object result) { - if (result instanceof Wrapper) { - result = ( (Wrapper) result).unwrap(); - } - + Object unwrapReturnValue(Object result) + { + if (result instanceof Wrapper w) + result = w.unwrap(); return result instanceof Undefined ? null : result; } - protected Reader preProcessScriptSource(Reader reader) + private String getFilename(ScriptContext ctxt) { - return reader; - } - - protected void processAllTopLevelScripts(Context cx) { - processTopLevelScript(TOPLEVEL_SCRIPT_NAME, cx); - } - - protected void processTopLevelScript(String scriptName, Context cx) { - InputStream toplevelScript = this.getClass().getClassLoader().getResourceAsStream(scriptName); - if (toplevelScript != null) { - Reader reader = new InputStreamReader(toplevelScript); - try { - cx.evaluateReader(topLevel, reader, scriptName, 1, null); - } - catch (Exception e) { - _log.debug(e); - } - finally { - try { - toplevelScript.close(); - } - catch (IOException e) { - } - } - } - } - - /** - * The bindings function takes a JavaScript scope object - * of type ExternalScriptable and returns the underlying Bindings - * instance. - * - * var page = scope(pageBindings); - * with (page) { - * // code that uses page scope - * } - * var b = bindings(page); - * // operate on bindings here. - */ - public static Object bindings(Context cx, Scriptable thisObj, Object[] args, - Function funObj) { - if (args.length == 1) { - Object arg = args[0]; - if (arg instanceof Wrapper) { - arg = ((Wrapper)arg).unwrap(); - } - if (arg instanceof ExternalScriptable) { - ScriptContext ctx = ((ExternalScriptable)arg).getContext(); - Bindings bind = ctx.getBindings(ScriptContext.ENGINE_SCOPE); - return Context.javaToJS(bind, - ScriptableObject.getTopLevelScope(thisObj)); - } - } - return cx.getUndefinedValue(); + String filename = null; + if (ctxt != null && ctxt.getBindings(ScriptContext.ENGINE_SCOPE) != null) + filename = (String) ctxt.getBindings(ScriptContext.ENGINE_SCOPE).get(javax.script.ScriptEngine.FILENAME); + if (filename == null) + filename = (String) get(javax.script.ScriptEngine.FILENAME); + return filename != null ? filename : ""; } - /** - * The scope function creates a new JavaScript scope object - * with given Bindings object as backing store. This can be used - * to create a script scope based on arbitrary Bindings instance. - * For example, in webapp scenario, a 'page' level Bindings instance - * may be wrapped as a scope and code can be run in JavaScript 'with' - * statement: - * - * var page = scope(pageBindings); - * with (page) { - * // code that uses page scope - * } - */ - public static Object scope(Context cx, Scriptable thisObj, Object[] args, - Function funObj) { - if (args.length == 1) { - Object arg = args[0]; - if (arg instanceof Wrapper) { - arg = ((Wrapper)arg).unwrap(); - } - if (arg instanceof Bindings) { - ScriptContext ctx = new SimpleScriptContext(); - ctx.setBindings((Bindings)arg, ScriptContext.ENGINE_SCOPE); - Scriptable res = new ExternalScriptable(ctx); - res.setPrototype(ScriptableObject.getObjectPrototype(thisObj)); - res.setParentScope(ScriptableObject.getTopLevelScope(thisObj)); - return res; - } - } - return cx.getUndefinedValue(); - } - - /** - * The sync function creates a synchronized function (in the sense - * of a Java synchronized method) from an existing function. The - * new function synchronizes on the this object of - * its invocation. - * js> var o = { f : sync(function(x) { - * print("entry"); - * Packages.java.lang.Thread.sleep(x*1000); - * print("exit"); - * })}; - * js> thread(function() {o.f(5);}); - * entry - * js> thread(function() {o.f(5);}); - * js> - * exit - * entry - * exit - */ - public static Object sync(Context cx, Scriptable thisObj, Object[] args, - Function funObj) { - if (args.length == 1 && args[0] instanceof Function) { - return new Synchronizer((Function)args[0]); - } else { - throw Context.reportRuntimeError("wrong argument(s) for sync"); - } + static ScriptException toScriptException(JavaScriptException jse) + { + int line = jse.lineNumber() == 0 ? -1 : jse.lineNumber(); + Object value = jse.getValue(); + String str = (value != null && "org.mozilla.javascript.NativeError".equals(value.getClass().getName())) + ? value.toString() : jse.toString(); + ScriptException ex = new ScriptException(str, jse.sourceName(), line); + ex.initCause(jse); + ExceptionUtil.decorateException(ex, ExceptionUtil.ExceptionInfo.SkipMothershipLogging, "true", true); + return ex; } - public static void main(String[] args) throws Exception { - if (args.length == 0) { - System.out.println("No file specified"); - return; - } - - InputStreamReader r = new InputStreamReader(new FileInputStream(args[0])); - ScriptEngine engine = new RhinoScriptEngine(); - - SimpleScriptContext context = new SimpleScriptContext(); - engine.put(ScriptEngine.FILENAME, args[0]); - engine.eval(r, context); - // added this statement to save some typing to most script authors - context.getWriter().flush(); + static ScriptException toScriptException(RhinoException re) + { + int line = re.lineNumber() == 0 ? -1 : re.lineNumber(); + ScriptException ex = new ScriptException(re.toString(), re.sourceName(), line); + ex.initCause(re); + ExceptionUtil.decorateException(ex, ExceptionUtil.ExceptionInfo.SkipMothershipLogging, "true", true); + return ex; } @Override diff --git a/core/src/org/labkey/core/script/RhinoService.java b/core/src/org/labkey/core/script/RhinoService.java index c317b9438fe..d6a07b56a9e 100644 --- a/core/src/org/labkey/core/script/RhinoService.java +++ b/core/src/org/labkey/core/script/RhinoService.java @@ -15,12 +15,11 @@ */ package org.labkey.core.script; -import com.sun.phobos.script.javascript.RhinoScriptEngineFactory; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.Nullable; -import org.json.JSONObject; import org.json.JSONArray; +import org.json.JSONObject; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; @@ -54,9 +53,9 @@ import org.mozilla.javascript.Function; import org.mozilla.javascript.ImporterTopLevel; import org.mozilla.javascript.JavaScriptException; -import org.mozilla.javascript.LazilyLoadedCtor; import org.mozilla.javascript.NativeArray; import org.mozilla.javascript.NativeJavaObject; +import org.mozilla.javascript.NativeObject; import org.mozilla.javascript.RhinoException; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; @@ -68,6 +67,7 @@ import org.mozilla.javascript.commonjs.module.provider.ModuleSourceProvider; import org.mozilla.javascript.commonjs.module.provider.ModuleSourceProviderBase; import org.mozilla.javascript.commonjs.module.provider.SoftCachingModuleScriptProvider; +import org.mozilla.javascript.lc.type.TypeInfo; import org.springframework.validation.BindException; import org.springframework.validation.Errors; @@ -220,14 +220,14 @@ public void testModuleResourceCache() .mapToInt(Map::size) .sum(); - LOG.info(scriptCount + " scripts in all modules"); + LOG.info("{} scripts in all modules", scriptCount); // Load all the top-level script timestamps to ensure no exceptions and get a count int timestampCount = LabKeyModuleSourceProvider.TOP_LEVEL_SCRIPT_CACHE.streamAllResourceMaps() .mapToInt(Map::size) .sum(); - LOG.info(timestampCount + " top-level script timestamps in all modules"); + LOG.info("{} top-level script timestamps in all modules", timestampCount); assertEquals("Mismatch in counts for JavaScript scripts vs. script timestamps", scriptCount, timestampCount); @@ -244,8 +244,13 @@ public void testModuleResourceCache() } } -class RhinoFactory extends RhinoScriptEngineFactory implements ScriptService +class RhinoFactory implements ScriptService { + private static final List NAMES = List.of("rhino", "Rhino", "javascript", "JavaScript"); + private static final List EXTENSIONS = List.of("js"); + private static final List MIME_TYPES = List.of( + "application/javascript", "application/ecmascript", "text/javascript", "text/ecmascript"); + @Override public RhinoEngine getScriptEngine() { @@ -257,6 +262,73 @@ public ScriptReference compile(Module module, Path path) { return ScriptReferenceImpl.get(module, path); } + + @Override + public String getEngineName() { return "rhino"; } + + @Override + public String getEngineVersion() + { + try (Context cx = Context.enter()) + { + return cx.getImplementationVersion(); + } + } + + @Override + public List getExtensions() { return EXTENSIONS; } + + @Override + public List getMimeTypes() { return MIME_TYPES; } + + @Override + public List getNames() { return NAMES; } + + @Override + public String getLanguageName() { return "javascript"; } + + @Override + public String getLanguageVersion() { return String.valueOf(Context.VERSION_ES6); } + + @Override + public Object getParameter(String key) + { + return switch (key) + { + case ScriptEngine.ENGINE, ScriptEngine.NAME -> getEngineName(); + case ScriptEngine.ENGINE_VERSION -> getEngineVersion(); + case ScriptEngine.LANGUAGE -> getLanguageName(); + case ScriptEngine.LANGUAGE_VERSION -> getLanguageVersion(); + default -> null; + }; + } + + @Override + public String getMethodCallSyntax(String obj, String m, String... args) + { + StringBuilder sb = new StringBuilder(obj).append('.').append(m).append('('); + for (int i = 0; i < args.length; i++) + { + if (i > 0) sb.append(','); + sb.append(args[i]); + } + return sb.append(");").toString(); + } + + @Override + public String getOutputStatement(String toDisplay) + { + return "print('" + toDisplay + "');"; + } + + @Override + public String getProgram(String... statements) + { + StringBuilder sb = new StringBuilder(); + for (String stmt : statements) + sb.append(stmt).append(";\n"); + return sb.toString(); + } } class ScriptReferenceImpl implements ScriptReference @@ -283,24 +355,19 @@ public Map load(Stream resources, Modu private @Nullable CompiledScript compile(Resource r, Module module) { RhinoEngine engine = RhinoService.RHINO_FACTORY.getScriptEngine(); - Context ctx = Context.enter(); - LOG.debug("Compiling script '" + r.toString() + "'"); + LOG.debug("Compiling script '{}'", r.toString()); - try (Reader reader = Readers.getReader(r.getInputStream())) + try (Context _ = Context.enter(); Reader reader = Readers.getReader(r.getInputStream())) { engine.put(ScriptEngine.FILENAME, r.getPath().toString()); return engine.compile(reader); } catch (Throwable t) { - LOG.error("Failed to compile script '" + r + "': " + t.getMessage()); + LOG.error("Failed to compile script '{}': {}", r, t.getMessage()); return null; } - finally - { - Context.exit(); - } } }; @@ -372,18 +439,13 @@ public T eval(Class resultType) throws ScriptException @Override public T eval(Class resultType, Map map) throws ScriptException { - Context ctx = Context.enter(); - try + try (Context _ = Context.enter()) { Object result = eval(map); if (result == null) return null; return (T)ScriptUtils.jsToJava(result, resultType); } - finally - { - Context.exit(); - } } @Override @@ -395,8 +457,7 @@ public Object eval() throws ScriptException @Override public Object eval(Map map) throws ScriptException { - Context ctx = Context.enter(); - try + try (Context _ = Context.enter()) { ScriptContext ctxt = getContext(); if (map != null) @@ -412,15 +473,11 @@ public Object eval(Map map) throws ScriptException } ctxt.getBindings(ScriptContext.ENGINE_SCOPE).put(ScriptEngine.FILENAME, _path.toString()); - LOG.debug("Evaluating script '" + _path + "'"); + LOG.debug("Evaluating script '{}'", _path); Object result = _script.eval(ctxt); _evaluated = true; return result; } - finally - { - Context.exit(); - } } @Override @@ -453,19 +510,14 @@ public T invokeFn(Class resultType, Object thiz, String name, Object... a if (!_evaluated) eval(); - Context ctx = Context.enter(); - try + try (Context _ = Context.enter()) { - LOG.debug("Invoking method '" + name + "' in script '" + _path.toString() + "'"); + LOG.debug("Invoking method '{}' in script '{}'", name, _path.toString()); Object result = _engine.invokeMethod(thiz, name, args); if (result == null) return null; return (T)ScriptUtils.jsToJava(result, resultType); } - finally - { - Context.exit(); - } } @Override @@ -481,10 +533,9 @@ public T invokeFn(Class resultType, String name, Object... args) throws S if (!_evaluated) eval(); - Context ctx = Context.enter(); - try + try (Context _ = Context.enter()) { - LOG.debug("Invoking method '" + name + "' in script '" + _path.toString() + "'"); + LOG.debug("Invoking method '{}' in script '{}'", name, _path.toString()); ScriptContext ctxt = getContext(); Scriptable scope = _engine.getRuntimeScope(ctxt); Object result = _engine.invokeMethod(scope, name, args); @@ -492,10 +543,6 @@ public T invokeFn(Class resultType, String name, Object... args) throws S return null; return (T)ScriptUtils.jsToJava(result, resultType); } - finally - { - Context.exit(); - } } @Override @@ -667,10 +714,10 @@ static void clearTopLevel() // Similar to the topLevel scope created in RhinoScriptEngine // except it is sealed to prevent modifications to built-in objects - // or adding any additional objects to the scope. In addition, the + // or adding any additional objects to the scope. In addition, the // topLevel is cached in a WeakReference so can be shared with other - // instances of RhinoService and across threads. The topLevel won't - // be gc'd until all of the ScriptResourceRef in the SCRIPT_CACHE are gone. + // instances of RhinoService and across threads. The topLevel won't + // be GCed until all the ScriptResourceRef in the SCRIPT_CACHE are gone. @Override protected ScriptableObject createTopLevel() { @@ -688,38 +735,18 @@ protected ScriptableObject createTopLevel() ModuleSourceProvider moduleSourceProvider = new LabKeyModuleSourceProvider(); _moduleScriptProvider = new SoftCachingModuleScriptProvider(moduleSourceProvider); - Context cx = Context.enter(); - cx.setLanguageVersion(Context.VERSION_1_8); - - try + try (Context cx = Context.enter()) { - /* - * RRC - modified this code to register JSAdapter and some functions - * directly, without using a separate RhinoTopLevel class - */ - topLevel = new ImporterTopLevel(cx, false /*true*/); - //topLevel = new TopLevel(cx, this, true); + cx.setLanguageVersion(Context.VERSION_ES6); + topLevel = new ImporterTopLevel(cx, false); MemTracker.getInstance().put(topLevel); - new LazilyLoadedCtor(topLevel, "JSAdapter", - "com.sun.phobos.script.javascript.JSAdapter", - false); - /* - // add top level functions - String names[] = { "bindings", "scope", "sync" }; - topLevel.defineFunctionProperties(names, RhinoScriptEngine.class, ScriptableObject.DONTENUM); - */ initHostObjects(topLevel); processAllTopLevelScripts(cx, topLevel); - //sealStandardObjects(cx, topLevel); topLevel.sealObject(); } - finally - { - Context.exit(); - } - + sharedTopLevel = new WeakReference<>(topLevel); } @@ -746,8 +773,8 @@ protected void processAllTopLevelScripts(Context cx, Scriptable scope) { try { - ModuleScript global = _moduleScriptProvider.getModuleScript(cx, "global", null, null); - global.getScript().exec(cx, scope); + ModuleScript global = _moduleScriptProvider.getModuleScript(cx, "global", null, null, scope); + global.getScript().exec(cx, scope, scope); } catch (Exception e) { @@ -792,8 +819,7 @@ protected Scriptable getRuntimeScope(ScriptContext ctxt) // from the shared SoftCachingModuleScriptProvider. // NOTE: we can't install this in the topLevel since the Require instance // holds on to all modules that have been require()'ed. - Context cx = enterContext(); - try + try (Context cx = Context.enter()) { // See: https://github.com/LabKey/platform/pull/3902 final Object scriptContext = ctxt.getAttribute(SERVER_CONTEXT_KEY, ScriptContext.ENGINE_SCOPE); @@ -810,13 +836,18 @@ protected Scriptable getRuntimeScope(ScriptContext ctxt) extraModules = Map.of(ScriptTrigger.SERVER_CONTEXT_SCRIPT_NAME, scriptContextScript); } - Require require = new Require(cx, getTopLevel(), new WrappingModuleScriptProvider(_moduleScriptProvider, extraModules), null, null, true); + // Rhino 1.9.1's ModuleScope constructor passes its prototype argument to + // TopLevel.cacheBuiltins(), which unconditionally calls putProperty() to register + // __GeneratorFunction (ES6 generator support). This fails with "Cannot modify a + // sealed object" if the prototype is the sealed topLevel. Using a thin non-sealed + // wrapper with topLevel as its prototype lets cacheBuiltins write safely while + // still making all built-ins visible to module scripts via the prototype chain. + NativeObject moduleNativeScope = new NativeObject(); + moduleNativeScope.setPrototype(getTopLevel()); + moduleNativeScope.setParentScope(null); + Require require = new Require(cx, moduleNativeScope, new WrappingModuleScriptProvider(_moduleScriptProvider, extraModules), null, null, true); require.install(scriptable); } - finally - { - Context.exit(); - } return scriptable; } @@ -900,9 +931,9 @@ public WrappingModuleScriptProvider(SoftCachingModuleScriptProvider provider, @N } @Override - public ModuleScript getModuleScript(Context cx, String moduleId, URI moduleUri, Scriptable paths) throws Exception + public ModuleScript getModuleScript(Context cx, String moduleId, URI moduleUri, URI baseUri, Scriptable paths) throws Exception { - return(_extraModules.containsKey(moduleId) ? _extraModules.get(moduleId) : _provider.getModuleScript(cx, moduleId, moduleUri, paths)); + return(_extraModules.containsKey(moduleId) ? _extraModules.get(moduleId) : _provider.getModuleScript(cx, moduleId, moduleUri, baseUri, paths)); } } } @@ -1048,7 +1079,7 @@ private static class SandboxContext extends Context private SandboxContext(SandboxContextFactory factory) { super(factory); - setLanguageVersion(Context.VERSION_1_8); + setLanguageVersion(Context.VERSION_ES6); // TODO: New version for now. Make configurable. startTime = HeartBeat.currentTimeMillis(); } } @@ -1063,7 +1094,7 @@ public SandboxWrapFactory() } @Override - public Object wrap(Context cx, Scriptable scope, Object obj, Class staticType) + public Object wrap(Context cx, Scriptable scope, Object obj, TypeInfo staticType) { // Unwrap JSONArrays to standard lists first if (obj instanceof JSONArray ja) @@ -1096,7 +1127,7 @@ else if (obj instanceof Object[] arr) } @Override - public Scriptable wrapAsJavaObject(Context cx, Scriptable scope, Object javaObject, Class staticType) + public Scriptable wrapAsJavaObject(Context cx, Scriptable scope, Object javaObject, TypeInfo staticType) { return new SandboxNativeJavaObject(scope, javaObject, staticType); } @@ -1104,7 +1135,7 @@ public Scriptable wrapAsJavaObject(Context cx, Scriptable scope, Object javaObje private static class SandboxNativeJavaObject extends NativeJavaObject { - public SandboxNativeJavaObject(Scriptable scope, Object javaObject, Class staticType) + public SandboxNativeJavaObject(Scriptable scope, Object javaObject, TypeInfo staticType) { super(scope, javaObject, staticType); } diff --git a/core/src/org/labkey/core/script/ScriptableErrors.java b/core/src/org/labkey/core/script/ScriptableErrors.java index 5b69278a490..89a4651ac0a 100644 --- a/core/src/org/labkey/core/script/ScriptableErrors.java +++ b/core/src/org/labkey/core/script/ScriptableErrors.java @@ -24,6 +24,7 @@ import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.Wrapper; +import org.mozilla.javascript.lc.type.TypeInfoFactory; import java.util.List; import java.util.Map; @@ -65,7 +66,6 @@ public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] ar private ScriptableErrors(Scriptable scope, Object obj) { - this.parent = scope; if (obj instanceof Wrapper) obj = ((Wrapper)obj).unwrap(); @@ -78,14 +78,15 @@ else if (obj instanceof Scriptable) else throw new EvaluatorException("Invalid argument to ScriptableErrors(): " + obj); - this.staticType = this.errors.getClass(); + this.parent = scope; + this.staticType = TypeInfoFactory.GLOBAL.create(this.errors.getClass()); initMembers(); initPrototype(scope); } public ScriptableErrors(Scriptable scope, ValidationException errors) { - super(scope, errors, errors.getClass()); + super(scope, errors, TypeInfoFactory.GLOBAL.create(errors.getClass())); this.errors = errors; initPrototype(scope); } diff --git a/core/src/org/labkey/core/script/ScriptableList.java b/core/src/org/labkey/core/script/ScriptableList.java index 7f7827c2eab..cda81261b99 100644 --- a/core/src/org/labkey/core/script/ScriptableList.java +++ b/core/src/org/labkey/core/script/ScriptableList.java @@ -25,6 +25,7 @@ import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.Undefined; import org.mozilla.javascript.Wrapper; +import org.mozilla.javascript.lc.type.TypeInfoFactory; import java.util.ArrayList; import java.util.Collection; @@ -67,7 +68,6 @@ public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] ar * @param obj the list, possibly wrapped */ private ScriptableList(Scriptable scope, Object obj) { - this.parent = scope; if (obj instanceof Wrapper) { obj = ((Wrapper) obj).unwrap(); } @@ -82,7 +82,8 @@ private ScriptableList(Scriptable scope, Object obj) { } else { throw new EvaluatorException("Invalid argument to ScriptableList(): " + obj); } - this.staticType = this.list.getClass(); + this.parent = scope; + this.staticType = TypeInfoFactory.GLOBAL.create(this.list.getClass()); initMembers(); initPrototype(scope); } @@ -94,7 +95,7 @@ private ScriptableList(Scriptable scope, Object obj) { * @param list the list instance */ public ScriptableList(Scriptable scope, List list) { - super(scope, list, list.getClass()); + super(scope, list, TypeInfoFactory.GLOBAL.create(list.getClass())); this.list = list; initPrototype(scope); } diff --git a/core/src/org/labkey/core/script/ScriptableMap.java b/core/src/org/labkey/core/script/ScriptableMap.java index b8c125a4e82..0d42e4b8985 100644 --- a/core/src/org/labkey/core/script/ScriptableMap.java +++ b/core/src/org/labkey/core/script/ScriptableMap.java @@ -25,6 +25,7 @@ import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.Undefined; import org.mozilla.javascript.Wrapper; +import org.mozilla.javascript.lc.type.TypeInfoFactory; import java.util.HashMap; import java.util.Map; @@ -64,7 +65,6 @@ public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] ar } private ScriptableMap(Scriptable scope, Object obj, boolean reflect) { - this.parent = scope; this.reflect = reflect; if (obj instanceof Wrapper) { obj = ((Wrapper) obj).unwrap(); @@ -87,14 +87,15 @@ private ScriptableMap(Scriptable scope, Object obj, boolean reflect) { throw new EvaluatorException("Invalid argument to ScriptableMap(): " + obj); } this.javaObject = this.map; - this.staticType = this.map.getClass(); + this.parent = scope; + this.staticType = TypeInfoFactory.GLOBAL.create(this.map.getClass()); initMembers(); initPrototype(scope); } public ScriptableMap(Scriptable scope, Map map) { - super(scope, map, map.getClass()); + super(scope, map, TypeInfoFactory.GLOBAL.create(map.getClass())); this.map = map; initPrototype(scope); }