This is an automated email from the ASF dual-hosted git repository. jtulach pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/incubator-netbeans-html4j.git
commit 4171d02183e27c19cbdbf92a14dec78b8211df0d Author: Jaroslav Tulach <[email protected]> AuthorDate: Sun Feb 17 20:17:22 2019 +0100 Scripts.newPresenter() is a builder to create presenter --- boot-script/pom.xml | 3 +- .../java/net/java/html/boot/script/Sanitizer.java | 80 ++++++++++++ .../net/java/html/boot/script/ScriptPresenter.java | 17 ++- .../java/net/java/html/boot/script/Scripts.java | 133 ++++++++++++++++---- .../html/boot/script/Jsr223JavaScriptTest.java | 24 +++- .../java/html/boot/script/KnockoutEnvJSTest.java | 6 +- .../net/java/html/boot/script/ScriptsTest.java | 139 +++++++++++++++++++++ pom.xml | 10 +- src/main/javadoc/overview.html | 3 + 9 files changed, 369 insertions(+), 46 deletions(-) diff --git a/boot-script/pom.xml b/boot-script/pom.xml index ab65024..5a16110 100644 --- a/boot-script/pom.xml +++ b/boot-script/pom.xml @@ -31,7 +31,7 @@ <version>2.0-SNAPSHOT</version> <packaging>bundle</packaging> <properties> - <netbeans.compile.on.save>NONE</netbeans.compile.on.save> + <netbeans.compile.on.save>none</netbeans.compile.on.save> <publicPackages>net.java.html.boot.script</publicPackages> </properties> <build> @@ -108,7 +108,6 @@ <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <scope>test</scope> - <version>3.1.0</version> </dependency> <dependency> <groupId>org.netbeans.html</groupId> diff --git a/boot-script/src/main/java/net/java/html/boot/script/Sanitizer.java b/boot-script/src/main/java/net/java/html/boot/script/Sanitizer.java new file mode 100644 index 0000000..2d3c57a --- /dev/null +++ b/boot-script/src/main/java/net/java/html/boot/script/Sanitizer.java @@ -0,0 +1,80 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package net.java.html.boot.script; + +import javax.script.Invocable; +import javax.script.ScriptEngine; +import javax.script.ScriptException; + +final class Sanitizer { + private Sanitizer() { + } + + private static final String[] ALLOWED_GLOBALS = ("" + + "Object,Function,Array,String,Date,Number,BigInt," + + "Boolean,RegExp,Math,JSON,NaN,Infinity,undefined," + + "isNaN,isFinite,parseFloat,parseInt,encodeURI," + + "encodeURIComponent,decodeURI,decodeURIComponent,eval," + + "escape,unescape," + + "Error,EvalError,RangeError,ReferenceError,SyntaxError," + + "TypeError,URIError,ArrayBuffer,Int8Array,Uint8Array," + + "Uint8ClampedArray,Int16Array,Uint16Array,Int32Array," + + "Uint32Array,Float32Array,Float64Array,BigInt64Array," + + "BigUint64Array,DataView,Map,Set,WeakMap," + + "WeakSet,Symbol,Reflect,Proxy,Promise,SharedArrayBuffer," + + "Atomics,console,performance," + + "arguments,load").split(","); + + + static void clean(ScriptEngine engine) throws ScriptException { + try { + Object cleaner = engine.eval("" + + "(function(allowed) {\n" + + " var names = Object.getOwnPropertyNames(this);\n" + + " MAIN: for (var i = 0; i < names.length; i++) {\n" + + " for (var j = 0; j < allowed.length; j++) {\n" + + " if (names[i] === allowed[j]) {\n" + + " continue MAIN;\n" + + " }\n" + + " }\n" + + " delete this[names[i]];\n" + + " }\n" + + "})" + ); + ((Invocable) engine).invokeMethod(cleaner, "call", null, ALLOWED_GLOBALS); + } catch (NoSuchMethodException ex) { + throw new ScriptException(ex); + } + } + + static void defineAlert(ScriptEngine engine) throws ScriptException { + try { + Object defineAlert = engine.eval("" + + "(function(out) {\n" + + " this.alert = function(msg) {\n" + + " out.println(msg);\n" + + " };" + + "});" + ); + ((Invocable) engine).invokeMethod(defineAlert, "call", null, System.out); + } catch (NoSuchMethodException ex) { + throw new ScriptException(ex); + } + } +} diff --git a/boot-script/src/main/java/net/java/html/boot/script/ScriptPresenter.java b/boot-script/src/main/java/net/java/html/boot/script/ScriptPresenter.java index 289e625..dee6415 100644 --- a/boot-script/src/main/java/net/java/html/boot/script/ScriptPresenter.java +++ b/boot-script/src/main/java/net/java/html/boot/script/ScriptPresenter.java @@ -20,7 +20,6 @@ package net.java.html.boot.script; import java.io.Closeable; import java.io.IOException; -import java.io.ObjectOutput; import java.io.Reader; import java.lang.ref.WeakReference; import java.lang.reflect.Array; @@ -72,19 +71,19 @@ Presenter, Fn.FromJavaScript, Fn.ToJavaScript, Executor { private final Set<Class<?>> jsReady; private final CallbackImpl callback; - ScriptPresenter(Executor exc) { - this(new ScriptEngineManager().getEngineByName("javascript"), exc); - } - - ScriptPresenter(ScriptEngine eng, Executor exc) { + ScriptPresenter(ScriptEngine eng, Executor exc, boolean sanitize) { + if (eng == null) { + eng = new ScriptEngineManager().getEngineByName("javascript"); + } this.eng = eng; this.exc = exc; try { - eng.eval("function alert(msg) { Packages.java.lang.System.out.println(msg); };"); - eng.eval("function confirm(msg) { Packages.java.lang.System.out.println(msg); return true; };"); - eng.eval("function prompt(msg, txt) { Packages.java.lang.System.out.println(msg + ':' + txt); return txt; };"); Object undef = new UndefinedCallback().undefined(eng); this.undefined = undef; + if (sanitize) { + Sanitizer.clean(eng); + } + Sanitizer.defineAlert(eng); } catch (ScriptException ex) { throw new IllegalStateException(ex); } diff --git a/boot-script/src/main/java/net/java/html/boot/script/Scripts.java b/boot-script/src/main/java/net/java/html/boot/script/Scripts.java index 4df7086..4e8633f 100644 --- a/boot-script/src/main/java/net/java/html/boot/script/Scripts.java +++ b/boot-script/src/main/java/net/java/html/boot/script/Scripts.java @@ -18,52 +18,47 @@ */ package net.java.html.boot.script; -import java.io.Closeable; import java.util.concurrent.Executor; import javax.script.ScriptEngine; -import net.java.html.boot.BrowserBuilder; import net.java.html.js.JavaScriptBody; -import org.netbeans.html.boot.spi.Fn; import org.netbeans.html.boot.spi.Fn.Presenter; -/** Implementations of {@link Presenter}s that delegate +/** Builder to create a {@link Presenter} that delegates * to Java {@link ScriptEngine scripting} API. Initialize your presenter * like this: - * - * <pre> - * - * {@link Runnable} <em>run</em> = ...; // your own init code - * {@link Presenter Fn.Presenter} <b>p</b> = Scripts.{@link Scripts#createPresenter()}; - * BrowserBuilder.{@link BrowserBuilder#newBrowser(java.lang.Object...) newBrowser(<b>p</b>)}. - * {@link BrowserBuilder#loadFinished(java.lang.Runnable) loadFinished(run)}. - * {@link BrowserBuilder#showAndWait()}; - * </pre> + * <p> + * {@codesnippet ScriptsTest#initViaBrowserBuilder} * * and your runnable can make extensive use of {@link JavaScriptBody} directly or * indirectly via APIs using {@link JavaScriptBody such annotation} themselves. * <p> * Alternatively one can manipulate the presenter manually, which is * especially useful when writing tests: - * <pre> - * {@code @Test} public void runInASimulatedBrowser() throws Exception { - * {@link Presenter Fn.Presenter} <b>p</b> = Scripts.{@link Scripts#createPresenter()}; - * try ({@link Closeable} c = {@link Fn#activate(org.netbeans.html.boot.spi.Fn.Presenter) Fn.activate}(<b>p</b>)) { - * // your code operating in context of <b>p</b> - * } - * } - * </pre> - * The previous code snippet requires Java 7 language syntax, as it relies - * on try-with-resources language syntactic sugar feature. The same block + * <p> + * {@codesnippet ScriptsTest#activatePresenterDirectly} + * <p> + * The previous code snippet relies + * on try-with-resources <em>Java7</em> syntax. The same block * of code can be used on older versions of Java, but it is slightly more * verbose. * * @author Jaroslav Tulach */ public final class Scripts { + + private Executor exc; + private ScriptEngine engine; + private boolean sanitize = true; + private Scripts() { } - /** Simple implementation of {@link Presenter} that delegates + /** {@linkplain #sanitize(boolean) Non-sanitized} version of the presenter. + * Rather use following code to obtain safer version of the engine: + * <p> + * {@codesnippet ScriptsTest#testNewPresenterNoExecutor} + * <p> + * Simple implementation of {@link Presenter} that delegates * to Java {@link ScriptEngine scripting} API. The presenter runs headless * without appropriate simulation of browser APIs. Its primary usefulness * is inside testing environments. The presenter implements {@link Executor} @@ -72,12 +67,19 @@ public final class Scripts { * * @return new instance of a presenter that is using its own * {@link ScriptEngine} for <code>text/javascript</code> mimetype + * @deprecated use {@link #newPresenter()} builder */ + @Deprecated public static Presenter createPresenter() { - return new ScriptPresenter(null); + return newPresenter().sanitize(false).build(); } - /** Implementation of {@link Presenter} that delegates + /** {@linkplain #sanitize(boolean) Non-sanitized} version of the presenter. + * Rather use following code to obtain safer version of the engine: + * <p> + * {@codesnippet Jsr223JavaScriptTest#createPresenter} + * <p> + * Implementation of {@link Presenter} that delegates * to Java {@link ScriptEngine scripting} API and can control execution * thread. The presenter runs headless * without appropriate simulation of browser APIs. Its primary usefulness @@ -88,8 +90,85 @@ public final class Scripts { * @param exc the executor to re-schedule all asynchronous requests to * @return new instance of a presenter that is using its own * {@link ScriptEngine} for <code>text/javascript</code> mimetype + * @deprecated use {@link #newPresenter()} builder */ + @Deprecated public static Presenter createPresenter(Executor exc) { - return new ScriptPresenter(exc); + return newPresenter().sanitize(false).executor(exc).build(); + } + + /** Creates new scripting {@link Presenter} builder. Simplest way + * to use is: + * <p> + * {@codesnippet ScriptsTest#testNewPresenterNoExecutor} + * <p> + * It is possible to specify own + * {@link #engine(javax.script.ScriptEngine) scripting engine} + * and {@link #executor(java.util.concurrent.Executor)} + * and control the thread that executes the scripts: + * <p> + * {@codesnippet Jsr223JavaScriptTest#createPresenter} + * <p> + * By default the created presenters are {@linkplain #sanitize(boolean) sanitized}. + * + * @return instance of the new builder + * @since 1.6.1 + */ + public static Scripts newPresenter() { + return new Scripts(); + } + + /** Associates new executor. + * The {@linkplain #build() to be created presenter} will implement {@link Executor} + * interface, and passes all runnables from its own + * {@link Executor#execute(java.lang.Runnable)} method + * to here in provided {@code exc} instance of executor. + * + * @param exc dedicated executor to use + * @return instance of the new builder + * @since 1.6.1 + */ + public Scripts executor(Executor exc) { + this.exc = exc; + return this; + } + + /** Associates a scripting engine. + * The engine is used to {@link #build() build} an + * implementation of {@link Presenter} that delegates + * to Java {@link ScriptEngine scripting} API. The presenter runs headless + * without appropriate simulation of browser APIs. + * + * @param engine dedicated script engine to use + * @return instance of the new builder + * @since 1.6.1 + */ + public Scripts engine(ScriptEngine engine) { + this.engine = engine; + return this; + } + + /** Turn sandboxing of the engine on or off. When sanitization is on + * a special care is taken to remove all global symbols not present + * in the EcmaScript specification. By default the sanitization is on + * to increase security. + * + * @param yesOrNo do the sanitization or not + * @return instance of the new builder + * @since 1.6.1 + */ + public Scripts sanitize(boolean yesOrNo) { + this.sanitize = yesOrNo; + return this; + } + + /** Builds new instance of the scripting presenter. Use + * arguments of this builder and creates new instance. + * + * @return creates new instance of the presenter + * @since 1.6.1 + */ + public Presenter build() { + return new ScriptPresenter(engine, exc, sanitize); } } diff --git a/boot-script/src/test/java/net/java/html/boot/script/Jsr223JavaScriptTest.java b/boot-script/src/test/java/net/java/html/boot/script/Jsr223JavaScriptTest.java index d405490..bfa5f21 100644 --- a/boot-script/src/test/java/net/java/html/boot/script/Jsr223JavaScriptTest.java +++ b/boot-script/src/test/java/net/java/html/boot/script/Jsr223JavaScriptTest.java @@ -22,6 +22,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.Executor; import java.util.concurrent.Executors; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; @@ -61,10 +62,11 @@ public class Jsr223JavaScriptTest { "" ); assertEquals(left.toString().toLowerCase().indexOf("java"), -1, "No Java symbols " + left); - final BrowserBuilder bb = BrowserBuilder.newBrowser(new ScriptPresenter(engine, SingleCase.JS)). - loadClass(Jsr223JavaScriptTest.class). + + Fn.Presenter presenter = createPresenter(engine); + final BrowserBuilder bb = BrowserBuilder.newBrowser(presenter). loadPage("empty.html"). - invoke("initialized"); + loadFinished(Jsr223JavaScriptTest::initialized); Executors.newSingleThreadExecutor().submit(new Runnable() { @Override @@ -73,7 +75,7 @@ public class Jsr223JavaScriptTest { } }); - List<Object> res = new ArrayList<Object>(); + List<Object> res = new ArrayList<>(); Class<? extends Annotation> test = loadClass().getClassLoader().loadClass(KOTest.class.getName()). asSubclass(Annotation.class); @@ -89,6 +91,16 @@ public class Jsr223JavaScriptTest { return res.toArray(); } + private static Fn.Presenter createPresenter(ScriptEngine engine) { + final Executor someExecutor = SingleCase.JS; + // BEGIN: Jsr223JavaScriptTest#createPresenter + return Scripts.newPresenter() + .engine(engine) + .executor(someExecutor) + .build(); + // END: Jsr223JavaScriptTest#createPresenter + } + static synchronized Class<?> loadClass() throws InterruptedException { while (browserClass == null) { Jsr223JavaScriptTest.class.wait(); @@ -96,13 +108,13 @@ public class Jsr223JavaScriptTest { return browserClass; } - public static synchronized void ready(Class<?> browserCls) throws Exception { + private static synchronized void ready(Class<?> browserCls) { browserClass = browserCls; browserPresenter = Fn.activePresenter(); Jsr223JavaScriptTest.class.notifyAll(); } - public static void initialized() throws Exception { + private static void initialized() { Assert.assertSame( Jsr223JavaScriptTest.class.getClassLoader(), ClassLoader.getSystemClassLoader(), diff --git a/boot-script/src/test/java/net/java/html/boot/script/KnockoutEnvJSTest.java b/boot-script/src/test/java/net/java/html/boot/script/KnockoutEnvJSTest.java index 4ad97bb..6316f10 100644 --- a/boot-script/src/test/java/net/java/html/boot/script/KnockoutEnvJSTest.java +++ b/boot-script/src/test/java/net/java/html/boot/script/KnockoutEnvJSTest.java @@ -84,7 +84,11 @@ public final class KnockoutEnvJSTest extends KnockoutTCK { baseUri = DynamicHTTP.initServer(); - final Fn.Presenter p = new ScriptPresenter(eng, KOCase.JS); + final Fn.Presenter p = Scripts.newPresenter() + .engine(eng) + .sanitize(false) + .executor(KOCase.JS) + .build(); try { Class.forName("java.lang.Module"); } catch (ClassNotFoundException oldJDK) { diff --git a/boot-script/src/test/java/net/java/html/boot/script/ScriptsTest.java b/boot-script/src/test/java/net/java/html/boot/script/ScriptsTest.java new file mode 100644 index 0000000..d7c8947 --- /dev/null +++ b/boot-script/src/test/java/net/java/html/boot/script/ScriptsTest.java @@ -0,0 +1,139 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package net.java.html.boot.script; + +import java.io.Closeable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.logging.Level; +import java.util.logging.Logger; +import net.java.html.boot.BrowserBuilder; +import net.java.html.js.JavaScriptBody; +import org.netbeans.html.boot.spi.Fn; +import static org.testng.Assert.*; +import org.testng.annotations.Test; + +public class ScriptsTest { + + public ScriptsTest() { + } + + @Test + public void testNewPresenterNoExecutor() throws Exception { + // BEGIN: ScriptsTest#testNewPresenterNoExecutor + Fn.Presenter presenter = Scripts.newPresenter().build(); + // END: ScriptsTest#testNewPresenterNoExecutor + assertNotNull(presenter); + Fn fn = presenter.defineFn("return a * b", "a", "b"); + Object fourtyTwo = fn.invoke(null, 6, 7); + assertTrue(fourtyTwo instanceof Number); + assertEquals(((Number)fourtyTwo).intValue(), 42); + } + + @Test + public void testActivatePresenterDirectly() throws Exception { + int fortyTwo = activatePresenterDirectly(); + assertEquals(fortyTwo, 42); + } + + // BEGIN: ScriptsTest#activatePresenterDirectly + @JavaScriptBody(args = { "a", "b" }, body = "return a * b;") + private static native int mul(int a, int b); + + private static int activatePresenterDirectly() throws Exception { + Fn.Presenter p = Scripts.newPresenter().build(); + try (Closeable c = Fn.activate(p)) { + int fortyTwo = mul(2, mul(7, 3)); + assert fortyTwo == 42; + return fortyTwo; + } + } + // END: ScriptsTest#activatePresenterDirectly + + @Test + public void initViaBrowserBuilder() throws Exception { + String[] executed = { null }; + // BEGIN: ScriptsTest#initViaBrowserBuilder + Runnable run = () -> { + executed[0] = "OK"; + }; + Fn.Presenter p = Scripts.newPresenter().build(); + BrowserBuilder.newBrowser(p) + .loadFinished(run) + .loadPage("empty.html") + .showAndWait(); + // END: ScriptsTest#initViaBrowserBuilder + assertEquals(executed[0], "OK", "Executed without issues"); + } + + @Test + public void isSanitizationOnByDefault() throws Exception { + assertSanitized(Scripts.newPresenter()); + } + + @Test + public void isSanitizationOnExplicitly() throws Exception { + assertSanitized(Scripts.newPresenter().sanitize(true)); + } + + @Test + public void noSanitization() throws Exception { + assertNotSanitized(Scripts.newPresenter().sanitize(false)); + } + + private void assertSanitized(Scripts newPresenter) throws Exception { + Fn.Presenter p = newPresenter.build(); + awaitPresenter(p); + try (Closeable c = Fn.activate(p)) { + Object Java = p.defineFn("return typeof Java;").invoke(null); + Object engine = p.defineFn("return typeof engine;").invoke(null); + Object Packages = p.defineFn("return typeof Packages;").invoke(null); + Object alert = p.defineFn("return typeof alert;").invoke(null); + assertEquals(Java, "undefined", "No Java symbol"); + assertEquals(engine, "undefined", "No engine symbol"); + assertEquals(Packages, "undefined", "No Packages symbol"); + assertEquals(alert, "function", "alert is defined symbol"); + } + } + + private void assertNotSanitized(Scripts builder) throws Exception { + Fn.Presenter p = builder.build(); + try (Closeable c = Fn.activate(p)) { + Object Java = p.defineFn("return typeof Java;").invoke(null); + Object engine = p.defineFn("return typeof engine;").invoke(null); + Object Packages = p.defineFn("return typeof Packages;").invoke(null); + Object alert = p.defineFn("return typeof alert;").invoke(null); + assertEquals(Java, "object", "Java symbol found"); + assertEquals(engine, "object", "Engine symbol found"); + assertEquals(Packages, "object", "Packages symbol found"); + assertEquals(alert, "function", "alert is defined symbol"); + } + } + + private static void awaitPresenter(Fn.Presenter p) { + Executor e = (Executor) p; + CountDownLatch cdl = new CountDownLatch(1); + e.execute(cdl::countDown); + try { + cdl.await(); + } catch (InterruptedException ex) { + throw new IllegalStateException(ex); + } + } +} diff --git a/pom.xml b/pom.xml index bbe30d8..4954d94 100644 --- a/pom.xml +++ b/pom.xml @@ -148,7 +148,13 @@ org.netbeans.html.boot.impl:org.netbeans.html.boot.fx:org.netbeans.html.context. <artifactId>codesnippet-doclet</artifactId> <version>0.23</version> </docletArtifact> - <additionalparam>-snippetpath json/src/test -snippetpath boot-fx/src/test ${javadoc.allowjs} -hiddingannotation java.lang.Deprecated</additionalparam> + <additionalparam> + -snippetpath boot-fx/src/test + -snippetpath boot-script/src/test + -snippetpath json/src/test + ${javadoc.allowjs} + -hiddingannotation java.lang.Deprecated + </additionalparam> </configuration> </plugin> <plugin> @@ -268,6 +274,8 @@ org.netbeans.html.boot.impl:org.netbeans.html.boot.fx:org.netbeans.html.context. <configuration> <source>1.6</source> <target>1.6</target> + <testSource>1.8</testSource> + <testTarget>1.8</testTarget> </configuration> </plugin> <plugin> diff --git a/src/main/javadoc/overview.html b/src/main/javadoc/overview.html index 13f6c70..3e4a06c 100644 --- a/src/main/javadoc/overview.html +++ b/src/main/javadoc/overview.html @@ -167,6 +167,9 @@ $ mvn -f client/pom.xml process-classes exec:exec <p> One model instance can be used in two views (<a target="_blank" href="https://github.com/apache/incubator-netbeans-html4j/pull/14">PR #14</a>). + Safe and {@link net.java.html.boot.script.Scripts sanitized builder} to + create {@link javax.script.ScriptEngine}-based execution environment + (<a target="_blank" href="https://github.com/apache/incubator-netbeans-html4j/pull/15">PR #15</a>). </p> <h3>New in version 1.6</h3> --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected] For further information about the NetBeans mailing lists, visit: https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists
