1 /*
   2  * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package jdk.nashorn.api.scripting;
  27 
  28 import static jdk.nashorn.internal.runtime.ECMAErrors.referenceError;
  29 import static jdk.nashorn.internal.runtime.ScriptRuntime.UNDEFINED;
  30 
  31 import java.io.IOException;
  32 import java.io.InputStream;
  33 import java.io.InputStreamReader;
  34 import java.io.Reader;
  35 import java.lang.reflect.Method;
  36 import java.lang.reflect.Modifier;
  37 import java.net.URL;
  38 import java.nio.charset.Charset;
  39 import java.security.AccessControlContext;
  40 import java.security.AccessController;
  41 import java.security.Permissions;
  42 import java.security.PrivilegedAction;
  43 import java.security.PrivilegedActionException;
  44 import java.security.PrivilegedExceptionAction;
  45 import java.security.ProtectionDomain;
  46 import java.text.MessageFormat;
  47 import java.util.Locale;
  48 import java.util.ResourceBundle;
  49 import javax.script.AbstractScriptEngine;
  50 import javax.script.Bindings;
  51 import javax.script.Compilable;
  52 import javax.script.CompiledScript;
  53 import javax.script.Invocable;
  54 import javax.script.ScriptContext;
  55 import javax.script.ScriptEngine;
  56 import javax.script.ScriptEngineFactory;
  57 import javax.script.ScriptException;
  58 import jdk.nashorn.internal.runtime.Context;
  59 import jdk.nashorn.internal.runtime.ErrorManager;
  60 import jdk.nashorn.internal.runtime.GlobalObject;
  61 import jdk.nashorn.internal.runtime.Property;
  62 import jdk.nashorn.internal.runtime.ScriptFunction;
  63 import jdk.nashorn.internal.runtime.ScriptObject;
  64 import jdk.nashorn.internal.runtime.ScriptRuntime;
  65 import jdk.nashorn.internal.runtime.Source;
  66 import jdk.nashorn.internal.runtime.linker.JavaAdapterFactory;
  67 import jdk.nashorn.internal.runtime.options.Options;
  68 
  69 /**
  70  * JSR-223 compliant script engine for Nashorn. Instances are not created directly, but rather returned through
  71  * {@link NashornScriptEngineFactory#getScriptEngine()}. Note that this engine implements the {@link Compilable} and
  72  * {@link Invocable} interfaces, allowing for efficient precompilation and repeated execution of scripts.
  73  * @see NashornScriptEngineFactory
  74  */
  75 
  76 public final class NashornScriptEngine extends AbstractScriptEngine implements Compilable, Invocable {
  77     private static AccessControlContext createPermAccCtxt(final String permName) {
  78         final Permissions perms = new Permissions();
  79         perms.add(new RuntimePermission(permName));
  80         return new AccessControlContext(new ProtectionDomain[] { new ProtectionDomain(null, perms) });
  81     }
  82 
  83     private static final AccessControlContext CREATE_CONTEXT_ACC_CTXT = createPermAccCtxt(Context.NASHORN_CREATE_CONTEXT);
  84     private static final AccessControlContext CREATE_GLOBAL_ACC_CTXT  = createPermAccCtxt(Context.NASHORN_CREATE_GLOBAL);
  85 
  86     private final ScriptEngineFactory factory;
  87     private final Context             nashornContext;
  88     private final ScriptObject        global;
  89     // initialized bit late to be made 'final'. Property object for "context"
  90     // property of global object
  91     private Property                  contextProperty;
  92 
  93     // default options passed to Nashorn Options object
  94     private static final String[] DEFAULT_OPTIONS = new String[] { "-scripting", "-doe" };
  95 
  96     private static final String MESSAGES_RESOURCE = "jdk.nashorn.api.scripting.resources.Messages";
  97 
  98     private static final ResourceBundle MESSAGES_BUNDLE;
  99     static {
 100         MESSAGES_BUNDLE = ResourceBundle.getBundle(MESSAGES_RESOURCE, Locale.getDefault());
 101     }
 102 
 103     private static String getMessage(final String msgId, final String... args) {
 104         try {
 105             return new MessageFormat(MESSAGES_BUNDLE.getString(msgId)).format(args);
 106         } catch (final java.util.MissingResourceException e) {
 107             throw new RuntimeException("no message resource found for message id: "+ msgId);
 108         }
 109     }
 110 
 111     NashornScriptEngine(final NashornScriptEngineFactory factory, final ClassLoader appLoader) {
 112         this(factory, DEFAULT_OPTIONS, appLoader);
 113     }
 114 
 115     NashornScriptEngine(final NashornScriptEngineFactory factory, final String[] args, final ClassLoader appLoader) {
 116         this.factory = factory;
 117         final Options options = new Options("nashorn");
 118         options.process(args);
 119 
 120         // throw ParseException on first error from script
 121         final ErrorManager errMgr = new Context.ThrowErrorManager();
 122         // create new Nashorn Context
 123         this.nashornContext = AccessController.doPrivileged(new PrivilegedAction<Context>() {
 124             @Override
 125             public Context run() {
 126                 try {
 127                     return new Context(options, errMgr, appLoader);
 128                 } catch (final RuntimeException e) {
 129                     if (Context.DEBUG) {
 130                         e.printStackTrace();
 131                     }
 132                     throw e;
 133                 }
 134             }
 135         }, CREATE_CONTEXT_ACC_CTXT);
 136 
 137         // create new global object
 138         this.global = createNashornGlobal();
 139         // set the default engine scope for the default context
 140         context.setBindings(new ScriptObjectMirror(global, global), ScriptContext.ENGINE_SCOPE);
 141 
 142         // evaluate engine initial script
 143         try {
 144             evalEngineScript();
 145         } catch (final ScriptException e) {
 146             if (Context.DEBUG) {
 147                 e.printStackTrace();
 148             }
 149             throw new RuntimeException(e);
 150         }
 151     }
 152 
 153     @Override
 154     public Object eval(final Reader reader, final ScriptContext ctxt) throws ScriptException {
 155         try {
 156             if (reader instanceof URLReader) {
 157                 final URL url = ((URLReader)reader).getURL();
 158                 final Charset cs = ((URLReader)reader).getCharset();
 159                 return evalImpl(compileImpl(new Source(url.toString(), url, cs), ctxt), ctxt);
 160             }
 161             return evalImpl(Source.readFully(reader), ctxt);
 162         } catch (final IOException e) {
 163             throw new ScriptException(e);
 164         }
 165     }
 166 
 167     @Override
 168     public Object eval(final String script, final ScriptContext ctxt) throws ScriptException {
 169         return evalImpl(script.toCharArray(), ctxt);
 170     }
 171 
 172     @Override
 173     public ScriptEngineFactory getFactory() {
 174         return factory;
 175     }
 176 
 177     @Override
 178     public Bindings createBindings() {
 179         final ScriptObject newGlobal = createNashornGlobal();
 180         return new ScriptObjectMirror(newGlobal, newGlobal);
 181     }
 182 
 183     // Compilable methods
 184 
 185     @Override
 186     public CompiledScript compile(final Reader reader) throws ScriptException {
 187         try {
 188             return asCompiledScript(compileImpl(Source.readFully(reader), context));
 189         } catch (final IOException e) {
 190             throw new ScriptException(e);
 191         }
 192     }
 193 
 194     @Override
 195     public CompiledScript compile(final String str) throws ScriptException {
 196         return asCompiledScript(compileImpl(str.toCharArray(), context));
 197     }
 198 
 199     // Invocable methods
 200 
 201     @Override
 202     public Object invokeFunction(final String name, final Object... args)
 203             throws ScriptException, NoSuchMethodException {
 204         return invokeImpl(null, name, args);
 205     }
 206 
 207     @Override
 208     public Object invokeMethod(final Object thiz, final String name, final Object... args)
 209             throws ScriptException, NoSuchMethodException {
 210         if (thiz == null) {
 211             throw new IllegalArgumentException(getMessage("thiz.cannot.be.null"));
 212         }
 213         return invokeImpl(thiz, name, args);
 214     }
 215 
 216     private <T> T getInterfaceInner(final Object thiz, final Class<T> clazz) {
 217         if (clazz == null || !clazz.isInterface()) {
 218             throw new IllegalArgumentException(getMessage("interface.class.expected"));
 219         }
 220 
 221         // perform security access check as early as possible
 222         final SecurityManager sm = System.getSecurityManager();
 223         if (sm != null) {
 224             if (! Modifier.isPublic(clazz.getModifiers())) {
 225                 throw new SecurityException(getMessage("implementing.non.public.interface", clazz.getName()));
 226             }
 227             Context.checkPackageAccess(clazz.getName());
 228         }
 229 
 230         ScriptObject realSelf = null;
 231         ScriptObject realGlobal = null;
 232         if(thiz == null) {
 233             // making interface out of global functions
 234             realSelf = realGlobal = getNashornGlobalFrom(context);
 235         } else if (thiz instanceof ScriptObjectMirror) {
 236             final ScriptObjectMirror mirror = (ScriptObjectMirror)thiz;
 237             realSelf = mirror.getScriptObject();
 238             realGlobal = mirror.getHomeGlobal();
 239             if (! realGlobal.isOfContext(nashornContext)) {
 240                 throw new IllegalArgumentException(getMessage("script.object.from.another.engine"));
 241             }
 242         } else if (thiz instanceof ScriptObject) {
 243             // called from script code.
 244             realSelf = (ScriptObject)thiz;
 245             realGlobal = Context.getGlobal();
 246             if (realGlobal == null) {
 247                 throw new IllegalArgumentException(getMessage("no.current.nashorn.global"));
 248             }
 249 
 250             if (! realGlobal.isOfContext(nashornContext)) {
 251                 throw new IllegalArgumentException(getMessage("script.object.from.another.engine"));
 252             }
 253         }
 254 
 255         if (realSelf == null) {
 256             throw new IllegalArgumentException(getMessage("interface.on.non.script.object"));
 257         }
 258 
 259         try {
 260             final ScriptObject oldGlobal = Context.getGlobal();
 261             final boolean globalChanged = (oldGlobal != realGlobal);
 262             try {
 263                 if (globalChanged) {
 264                     Context.setGlobal(realGlobal);
 265                 }
 266 
 267                 if (! isInterfaceImplemented(clazz, realSelf)) {
 268                     return null;
 269                 }
 270                 return clazz.cast(JavaAdapterFactory.getConstructor(realSelf.getClass(), clazz).invoke(realSelf));
 271             } finally {
 272                 if (globalChanged) {
 273                     Context.setGlobal(oldGlobal);
 274                 }
 275             }
 276         } catch(final RuntimeException|Error e) {
 277             throw e;
 278         } catch(final Throwable t) {
 279             throw new RuntimeException(t);
 280         }
 281     }
 282 
 283     @Override
 284     public <T> T getInterface(final Class<T> clazz) {
 285         return getInterfaceInner(null, clazz);
 286     }
 287 
 288     @Override
 289     public <T> T getInterface(final Object thiz, final Class<T> clazz) {
 290         if (thiz == null) {
 291             throw new IllegalArgumentException(getMessage("thiz.cannot.be.null"));
 292         }
 293         return getInterfaceInner(thiz, clazz);
 294     }
 295 
 296     // These are called from the "engine.js" script
 297 
 298     /**
 299      * This hook is used to search js global variables exposed from Java code.
 300      *
 301      * @param self 'this' passed from the script
 302      * @param ctxt current ScriptContext in which name is searched
 303      * @param name name of the variable searched
 304      * @return the value of the named variable
 305      */
 306     public Object __noSuchProperty__(final Object self, final ScriptContext ctxt, final String name) {
 307         final int scope = ctxt.getAttributesScope(name);
 308         final ScriptObject ctxtGlobal = getNashornGlobalFrom(ctxt);
 309         if (scope != -1) {
 310             return ScriptObjectMirror.unwrap(ctxt.getAttribute(name, scope), ctxtGlobal);
 311         }
 312 
 313         if (self == UNDEFINED) {
 314             // scope access and so throw ReferenceError
 315             throw referenceError(ctxtGlobal, "not.defined", name);
 316         }
 317 
 318         return UNDEFINED;
 319     }
 320 
 321     private ScriptObject getNashornGlobalFrom(final ScriptContext ctxt) {
 322         final Bindings bindings = ctxt.getBindings(ScriptContext.ENGINE_SCOPE);
 323         if (bindings instanceof ScriptObjectMirror) {
 324             final ScriptObjectMirror mirror = (ScriptObjectMirror)bindings;
 325             ScriptObject sobj = ((ScriptObjectMirror)bindings).getScriptObject();
 326             if (sobj instanceof GlobalObject && sobj.isOfContext(nashornContext)) {
 327                 return sobj;
 328             }
 329         }
 330 
 331         // didn't find global object from context given - return the engine-wide global
 332         return global;
 333     }
 334 
 335     private ScriptObject createNashornGlobal() {
 336         final ScriptObject newGlobal = AccessController.doPrivileged(new PrivilegedAction<ScriptObject>() {
 337             @Override
 338             public ScriptObject run() {
 339                 try {
 340                     return nashornContext.newGlobal();
 341                 } catch (final RuntimeException e) {
 342                     if (Context.DEBUG) {
 343                         e.printStackTrace();
 344                     }
 345                     throw e;
 346                 }
 347             }
 348         }, CREATE_GLOBAL_ACC_CTXT);
 349 
 350         nashornContext.initGlobal(newGlobal);
 351 
 352         final int NON_ENUMERABLE_CONSTANT = Property.NOT_ENUMERABLE | Property.NOT_CONFIGURABLE | Property.NOT_WRITABLE;
 353         // current ScriptContext exposed as "context"
 354         // "context" is non-writable from script - but script engine still
 355         // needs to set it and so save the context Property object
 356         contextProperty = newGlobal.addOwnProperty("context", NON_ENUMERABLE_CONSTANT, UNDEFINED);
 357         // current ScriptEngine instance exposed as "engine". We added @SuppressWarnings("LeakingThisInConstructor") as
 358         // NetBeans identifies this assignment as such a leak - this is a false positive as we're setting this property
 359         // in the Global of a Context we just created - both the Context and the Global were just created and can not be
 360         // seen from another thread outside of this constructor.
 361         newGlobal.addOwnProperty("engine", NON_ENUMERABLE_CONSTANT, this);
 362         // global script arguments with undefined value
 363         newGlobal.addOwnProperty("arguments", Property.NOT_ENUMERABLE, UNDEFINED);
 364         // file name default is null
 365         newGlobal.addOwnProperty(ScriptEngine.FILENAME, Property.NOT_ENUMERABLE, null);
 366         return newGlobal;
 367     }
 368 
 369     private void evalEngineScript() throws ScriptException {
 370         final String script = "resources/engine.js";
 371         final String name   = NashornException.ENGINE_SCRIPT_SOURCE_NAME;
 372         try {
 373             final InputStream is = AccessController.doPrivileged(
 374                     new PrivilegedExceptionAction<InputStream>() {
 375                         @Override
 376                         public InputStream run() throws Exception {
 377                             final URL url = NashornScriptEngine.class.getResource(script);
 378                             return url.openStream();
 379                         }
 380                     });
 381             put(ScriptEngine.FILENAME, name);
 382             try (final InputStreamReader isr = new InputStreamReader(is)) {
 383                 eval(isr);
 384             }
 385         } catch (final PrivilegedActionException | IOException e) {
 386             if (Context.DEBUG) {
 387                 e.printStackTrace();
 388             }
 389             throw new ScriptException(e);
 390         } finally {
 391             put(ScriptEngine.FILENAME, null);
 392         }
 393     }
 394 
 395     // scripts should see "context" and "engine" as variables
 396     private void setContextVariables(final ScriptContext ctxt) {
 397         final ScriptObject ctxtGlobal = getNashornGlobalFrom(ctxt);
 398         // set "context" global variable via contextProperty - because this
 399         // property is non-writable
 400         contextProperty.setObjectValue(ctxtGlobal, ctxtGlobal, ctxt, false);
 401         Object args = ScriptObjectMirror.unwrap(ctxt.getAttribute("arguments"), ctxtGlobal);
 402         if (args == null || args == UNDEFINED) {
 403             args = ScriptRuntime.EMPTY_ARRAY;
 404         }
 405         // if no arguments passed, expose it
 406         if (! (args instanceof ScriptObject)) {
 407             args = ((GlobalObject)ctxtGlobal).wrapAsObject(args);
 408             ctxtGlobal.set("arguments", args, false);
 409         }
 410     }
 411 
 412     private Object invokeImpl(final Object selfObject, final String name, final Object... args) throws ScriptException, NoSuchMethodException {
 413         name.getClass(); // null check
 414 
 415         ScriptObjectMirror selfMirror = null;
 416         if (selfObject instanceof ScriptObjectMirror) {
 417             selfMirror = (ScriptObjectMirror)selfObject;
 418             if (! selfMirror.getHomeGlobal().isOfContext(nashornContext)) {
 419                 throw new IllegalArgumentException(getMessage("script.object.from.another.engine"));
 420             }
 421         } else if (selfObject instanceof ScriptObject) {
 422             // invokeMethod called from script code - in which case we may get 'naked' ScriptObject
 423             // Wrap it with oldGlobal to make a ScriptObjectMirror for the same.
 424             final ScriptObject oldGlobal = Context.getGlobal();
 425             if (oldGlobal == null) {
 426                 throw new IllegalArgumentException(getMessage("no.current.nashorn.global"));
 427             }
 428 
 429             if (! oldGlobal.isOfContext(nashornContext)) {
 430                 throw new IllegalArgumentException(getMessage("script.object.from.another.engine"));
 431             }
 432 
 433             selfMirror = (ScriptObjectMirror)ScriptObjectMirror.wrap(selfObject, oldGlobal);
 434         } else if (selfObject == null) {
 435             // selfObject is null => global function call
 436             final ScriptObject ctxtGlobal = getNashornGlobalFrom(context);
 437             selfMirror = (ScriptObjectMirror)ScriptObjectMirror.wrap(ctxtGlobal, ctxtGlobal);
 438         }
 439 
 440         if (selfMirror != null) {
 441             try {
 442                 return ScriptObjectMirror.translateUndefined(selfMirror.call(name, args));
 443             } catch (final Exception e) {
 444                 final Throwable cause = e.getCause();
 445                 if (cause instanceof NoSuchMethodException) {
 446                     throw (NoSuchMethodException)cause;
 447                 }
 448                 throwAsScriptException(e);
 449                 throw new AssertionError("should not reach here");
 450             }
 451         }
 452 
 453         // Non-script object passed as selfObject
 454         throw new IllegalArgumentException(getMessage("interface.on.non.script.object"));
 455     }
 456 
 457     private Object evalImpl(final char[] buf, final ScriptContext ctxt) throws ScriptException {
 458         return evalImpl(compileImpl(buf, ctxt), ctxt);
 459     }
 460 
 461     private Object evalImpl(final ScriptFunction script, final ScriptContext ctxt) throws ScriptException {
 462         if (script == null) {
 463             return null;
 464         }
 465         final ScriptObject oldGlobal = Context.getGlobal();
 466         final ScriptObject ctxtGlobal = getNashornGlobalFrom(ctxt);
 467         final boolean globalChanged = (oldGlobal != ctxtGlobal);
 468         try {
 469             if (globalChanged) {
 470                 Context.setGlobal(ctxtGlobal);
 471             }
 472 
 473             setContextVariables(ctxt);
 474             return ScriptObjectMirror.translateUndefined(ScriptObjectMirror.wrap(ScriptRuntime.apply(script, ctxtGlobal), ctxtGlobal));
 475         } catch (final Exception e) {
 476             throwAsScriptException(e);
 477             throw new AssertionError("should not reach here");
 478         } finally {
 479             if (globalChanged) {
 480                 Context.setGlobal(oldGlobal);
 481             }
 482         }
 483     }
 484 
 485     private static void throwAsScriptException(final Exception e) throws ScriptException {
 486         if (e instanceof ScriptException) {
 487             throw (ScriptException)e;
 488         } else if (e instanceof NashornException) {
 489             final NashornException ne = (NashornException)e;
 490             final ScriptException se = new ScriptException(
 491                 ne.getMessage(), ne.getFileName(),
 492                 ne.getLineNumber(), ne.getColumnNumber());
 493             se.initCause(e);
 494             throw se;
 495         } else if (e instanceof RuntimeException) {
 496             throw (RuntimeException)e;
 497         } else {
 498             // wrap any other exception as ScriptException
 499             throw new ScriptException(e);
 500         }
 501     }
 502 
 503     private CompiledScript asCompiledScript(final ScriptFunction script) {
 504         return new CompiledScript() {
 505             @Override
 506             public Object eval(final ScriptContext ctxt) throws ScriptException {
 507                 return evalImpl(script, ctxt);
 508             }
 509             @Override
 510             public ScriptEngine getEngine() {
 511                 return NashornScriptEngine.this;
 512             }
 513         };
 514     }
 515 
 516     private ScriptFunction compileImpl(final char[] buf, final ScriptContext ctxt) throws ScriptException {
 517         final Object val = ctxt.getAttribute(ScriptEngine.FILENAME);
 518         final String fileName = (val != null) ? val.toString() : "<eval>";
 519         return compileImpl(new Source(fileName, buf), ctxt);
 520     }
 521 
 522     private ScriptFunction compileImpl(final Source source, final ScriptContext ctxt) throws ScriptException {
 523         final ScriptObject oldGlobal = Context.getGlobal();
 524         final ScriptObject ctxtGlobal = getNashornGlobalFrom(ctxt);
 525         final boolean globalChanged = (oldGlobal != ctxtGlobal);
 526         try {
 527             if (globalChanged) {
 528                 Context.setGlobal(ctxtGlobal);
 529             }
 530 
 531             return nashornContext.compileScript(source, ctxtGlobal);
 532         } catch (final Exception e) {
 533             throwAsScriptException(e);
 534             throw new AssertionError("should not reach here");
 535         } finally {
 536             if (globalChanged) {
 537                 Context.setGlobal(oldGlobal);
 538             }
 539         }
 540     }
 541 
 542     private static boolean isInterfaceImplemented(final Class<?> iface, final ScriptObject sobj) {
 543         for (final Method method : iface.getMethods()) {
 544             // ignore methods of java.lang.Object class
 545             if (method.getDeclaringClass() == Object.class) {
 546                 continue;
 547             }
 548 
 549             Object obj = sobj.get(method.getName());
 550             if (! (obj instanceof ScriptFunction)) {
 551                 return false;
 552             }
 553         }
 554         return true;
 555     }
 556 }