blob: 63f6b0a65265297ed0067d203bc19e69b1eb1153 [file] [log] [blame]
/*
* 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 org.netbeans.api.scripting;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import java.util.function.Predicate;
import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
import org.netbeans.spi.scripting.EngineProvider;
import org.openide.util.Lookup;
/** NetBeans aware access to {@link ScriptEngineManager} manager.
* Rather than using JDK's {@link ScriptEngineManager} manager directly,
* instantiate it via {@link #createManager()} method. This method is aware
* of NetBeans specific runtime configurations. It uses the right classloader
* as well as specific discovery mechanisms to locate additional
* implementations of {@link ScriptEngineFactory}. To execute a JavaScript
* code use:
* <p>
* {@snippet file="org/netbeans/api/scripting/ScriptingTutorialTest.java" region="testFourtyTwo"}
* <p>
* Consult <a href="@org-netbeans-libs-graalsdk@/org/netbeans/libs/graalsdk/package-summary.html">scripting tutorial</a>
* to learn more about advanced polyglot scripting topics.
*
* @since 1.0
*/
public final class Scripting {
private boolean allowAllAccess;
private Scripting() {
}
/** Create new {@link ScriptEngineManager} configured for the NetBeans
* environment. The manager serves as an <em>isolated</em> environment -
* engines created from the same manager are supposed to share the
* same internals and be able to communicate with each other.
*
* @return new instance of the engine manager
*/
public static ScriptEngineManager createManager() {
return newBuilder().build();
}
/**
* A builder to configure and create new instance of {@link ScriptEngineManager}.
*
* @return a builder object with {@link #build()} method
* @since 1.2
*/
public static Scripting newBuilder() {
return new Scripting();
}
/** Allows the scripts to access JVM classes. By default the scripts
* run in as restricted environment as possible. See
* <a href="@org-netbeans-libs-graalsdk@/org/netbeans/libs/graalsdk/package-summary.html">scripting tutorial</a>
* for details. That is the prefered mode of execution. However,
* if your script is known and trusted, you may allow it to access
* classes and features in the JVM. For example it is common in Nashorn scripts
* to use:
*
* {@snippet file="org/netbeans/api/scripting/JavaScriptEnginesTest.java" region="allowLoadAClassInJS"}
*
* Such classloading is prevented by default. To allow it, specify {@code true}
* in here.
* <p>
* {@link ScriptEngineManager} created with all access on, has a boolean property
* in its {@link ScriptEngineManager#getBindings()}:
* {@snippet file="org/netbeans/api/scripting/ScriptingTest.java" region="testBuilderAllowAccess"}
*
* @param allAccess allow access to JVM internals from the script
* @return instance of {@code this} builder
* @since 1.2
*/
public Scripting allowAllAccess(boolean allAccess) {
this.allowAllAccess = allAccess;
return this;
}
/** Create new {@link ScriptEngineManager} configured for the NetBeans
* environment. The manager serves as an <em>isolated</em> environment -
* engines created from the same manager are supposed to share the
* same internals and be able to communicate with each other.
*
* @return new instance of the engine manager
* @since 1.2
*/
public ScriptEngineManager build() {
ClassLoader l = Lookup.getDefault().lookup(ClassLoader.class);
if (l == null) {
l = Thread.currentThread().getContextClassLoader();
}
if (l == null) {
l = Scripting.class.getClassLoader();
}
return new EngineManager(allowAllAccess, l);
}
private static final class EngineManager extends ScriptEngineManager {
private final List<ScriptEngineFactory> extra;
private final boolean allowAllAccess;
EngineManager(boolean allowAllAccess, ClassLoader loader) {
super(loader);
this.allowAllAccess = allowAllAccess;
if (allowAllAccess) {
getBindings().put("allowAllAccess", true); // NOI18N
}
this.extra = populateExtras(this);
for (ScriptEngineFactory f : extra) {
registerEngineName(f.getEngineName(), f);
for (String ext : f.getExtensions()) {
registerEngineExtension(ext, f);
}
for (String mime : f.getMimeTypes()) {
registerEngineMimeType(mime, f);
}
}
}
private static List<ScriptEngineFactory> populateExtras(EngineManager m) {
List<ScriptEngineFactory> extra = new ArrayList<>();
for (EngineProvider p : Lookup.getDefault().lookupAll(EngineProvider.class)) {
extra.addAll(p.factories(m));
}
return Collections.unmodifiableList(extra);
}
@Override
public List<ScriptEngineFactory> getEngineFactories() {
List<ScriptEngineFactory> all = new ArrayList<>();
all.addAll(super.getEngineFactories());
all.addAll(extra);
ListIterator<ScriptEngineFactory> it = all.listIterator();
while (it.hasNext()) {
ScriptEngineFactory f = it.next();
if (f.getNames().contains("Graal.js") || isNashornFactory(f)) { // NOI18N
it.set(new GraalJSWrapperFactory(f));
}
}
// reverse the list: as later engines override the earlier ones in MIME mappings, so
// they should be enumerated first, to give them higher precedence
Collections.reverse(all);
return all;
}
@Override
public ScriptEngine getEngineByExtension(String extension) {
return postConfigure(super.getEngineByExtension(extension));
}
@Override
public ScriptEngine getEngineByMimeType(String mimeType) {
return postConfigure(super.getEngineByMimeType(mimeType));
}
@Override
public ScriptEngine getEngineByName(String shortName) {
return postConfigure(super.getEngineByName(shortName));
}
private ScriptEngine postConfigure(ScriptEngine eng) {
if (eng == null) {
return null;
}
if (eng.getFactory().getNames().contains("Graal.js")) { // NOI18N
final Bindings b = eng.getBindings(ScriptContext.ENGINE_SCOPE);
if (allowAllAccess) {
b.put("polyglot.js.nashorn-compat", true); // NOI18N
}
b.put("polyglot.js.allowHostAccess", true); // NOI18N
b.put("polyglot.js.allowHostClassLookup", (Predicate<String>) (s) -> { // NOI18N
return allowHostClassLookup(eng, s);
});
}
if (isNashornFactory(eng.getFactory())) {
return secureEngineEngine(eng);
}
return eng;
}
private static final Class<?> nashornScriptEngineFactory;
static {
Class<?> klass;
try {
klass = Class.forName("jdk.nashorn.api.scripting.NashornScriptEngineFactory"); // NOI18N
} catch (ClassNotFoundException ex) {
klass = String.class;
}
nashornScriptEngineFactory = klass;
}
private boolean isNashornFactory(ScriptEngineFactory f) {
return nashornScriptEngineFactory.isInstance(f);
}
private ScriptEngine secureEngineEngine(ScriptEngine prototypeEngine) {
final ScriptEngine[] engine = { prototypeEngine };
try {
ScriptEngineFactory f = engine[0].getFactory();
final Class<? extends ScriptEngineFactory> factoryClass = f.getClass();
final ClassLoader factoryClassLoader = factoryClass.getClassLoader();
Class<?> filterClass = Class.forName("jdk.nashorn.api.scripting.ClassFilter", true, factoryClassLoader); // NOI18N
Method createMethod = factoryClass.getMethod("getScriptEngine", filterClass); // NOI18N
Object filter = java.lang.reflect.Proxy.newProxyInstance(factoryClassLoader, new Class[]{filterClass}, (Object proxy, Method method, Object[] args) -> {
return allowHostClassLookup(engine[0], (String) args[0]);
});
engine[0] = (ScriptEngine) createMethod.invoke(f, filter);
return engine[0];
} catch (NoSuchMethodException | ClassNotFoundException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
return engine[0];
}
}
private boolean allowHostClassLookup(final ScriptEngine engine, String className) {
return allowAllAccess;
}
private final class GraalJSWrapperFactory implements ScriptEngineFactory {
private final ScriptEngineFactory original;
GraalJSWrapperFactory(ScriptEngineFactory original) {
this.original = original;
}
@Override
public String getEngineName() {
return original.getEngineName();
}
@Override
public String getEngineVersion() {
return original.getEngineVersion();
}
@Override
public List<String> getExtensions() {
return original.getExtensions();
}
@Override
public List<String> getMimeTypes() {
return original.getMimeTypes();
}
@Override
public List<String> getNames() {
return original.getNames();
}
@Override
public String getLanguageName() {
return original.getLanguageName();
}
@Override
public String getLanguageVersion() {
return original.getLanguageVersion();
}
@Override
public Object getParameter(String key) {
return original.getParameter(key);
}
@Override
public String getMethodCallSyntax(String obj, String m, String... args) {
return original.getMethodCallSyntax(obj, m, args);
}
@Override
public String getOutputStatement(String toDisplay) {
return original.getOutputStatement(toDisplay);
}
@Override
public String getProgram(String... statements) {
return original.getProgram(statements);
}
@Override
public ScriptEngine getScriptEngine() {
return postConfigure(original.getScriptEngine());
}
}
}
}