/* * 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.apache.sling.scripting.javascript.internal; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import javax.script.Bindings; import javax.script.Compilable; import javax.script.CompiledScript; import javax.script.ScriptContext; import javax.script.ScriptEngine; import javax.script.ScriptEngineFactory; import javax.script.ScriptException; import org.apache.commons.io.IOUtils; import org.apache.sling.api.scripting.SlingBindings; import org.apache.sling.api.scripting.SlingScriptHelper; import org.apache.sling.commons.classloader.DynamicClassLoader; import org.apache.sling.scripting.api.AbstractSlingScriptEngine; import org.apache.sling.scripting.api.CachedScript; import org.apache.sling.scripting.api.ScriptCache; import org.apache.sling.scripting.api.ScriptNameAware; import org.apache.sling.scripting.core.ScriptNameAwareReader; import org.apache.sling.scripting.javascript.io.EspReader; import org.mozilla.javascript.ClassCache; import org.mozilla.javascript.Context; import org.mozilla.javascript.ImporterTopLevel; import org.mozilla.javascript.JavaScriptException; import org.mozilla.javascript.Script; import org.mozilla.javascript.ScriptRuntime; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.Undefined; import org.mozilla.javascript.WrapFactory; import org.mozilla.javascript.Wrapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A ScriptEngine that uses the Rhino interpreter to process Sling requests with * server-side javascript. */ public class RhinoJavaScriptEngine extends AbstractSlingScriptEngine implements Compilable { private static final Logger LOGGER = LoggerFactory.getLogger(RhinoJavaScriptEngine.class); private static final String NO_SCRIPT_NAME = "NO_SCRIPT_NAME"; private Scriptable rootScope; private ScriptCache scriptCache; public RhinoJavaScriptEngine(ScriptEngineFactory factory, Scriptable rootScope, ScriptCache scriptCache) { super(factory); this.rootScope = rootScope; this.scriptCache = scriptCache; } public CompiledScript compile(String script) throws ScriptException { Reader reader = null; try { reader = new InputStreamReader(IOUtils.toInputStream(script)); return compile(reader); } finally { IOUtils.closeQuietly(reader); } } public CompiledScript compile(Reader scriptReader) throws ScriptException { final String scriptName = getScriptName(scriptReader); CachedScript cachedScript = scriptCache.getScript(scriptName); if (cachedScript != null) { LOGGER.debug("Detected cached script for {}.", scriptName); return cachedScript.getCompiledScript(); } else { scriptReader = wrapReaderIfEspScript(scriptReader, scriptName); try { final Context rhinoContext = Context.enter(); rhinoContext.setOptimizationLevel(optimizationLevel()); if (!ScriptRuntime.hasTopCall(rhinoContext)) { // setup the context for use WrapFactory wrapFactory = ((RhinoJavaScriptEngineFactory) getFactory()).getWrapFactory(); rhinoContext.setWrapFactory(wrapFactory); } final int lineNumber = 1; final Object securityDomain = null; final Script script = rhinoContext.compileReader(scriptReader, scriptName, lineNumber, securityDomain); final SlingCompiledScript slingCompiledScript = new SlingCompiledScript(script, this); cachedScript = new CachedScript() { @Override public String getScriptPath() { return scriptName; } @Override public CompiledScript getCompiledScript() { return slingCompiledScript; } }; // SLING-4935 avoid caching scripts for which we cannot determine a name if (!scriptName.equals(NO_SCRIPT_NAME)) { scriptCache.putScript(cachedScript); } LOGGER.debug("Added {} script to Script Cache.", scriptName); return slingCompiledScript; } catch (IOException e) { final ScriptException se = new ScriptException("Failure running script " + scriptName + ": " + e.getMessage()); se.initCause(e); throw se; } finally { Context.exit(); } } } public Object eval(Reader scriptReader, ScriptContext scriptContext) throws ScriptException { String scriptName = getScriptName(scriptReader); Reader reader = wrapReaderIfEspScript(scriptReader, scriptName); if (!(scriptReader instanceof ScriptNameAware)) { if (NO_SCRIPT_NAME.equals(scriptName)) { String script = (String) scriptContext.getBindings(ScriptContext.ENGINE_SCOPE).get(ScriptEngine.FILENAME); if (script != null) { for (String extension : getFactory().getExtensions()) { if (script.endsWith(extension)) { scriptName = script; reader = new ScriptNameAwareReader(reader, scriptName); break; } } } } } return compile(reader).eval(scriptContext); } private Reader wrapReaderIfEspScript(Reader scriptReader, String scriptName) { if (scriptName.endsWith(RhinoJavaScriptEngineFactory.ESP_SCRIPT_EXTENSION)) { scriptReader = new EspReader(scriptReader); } return scriptReader; } private Map<String, Object> setBoundProperties(Scriptable scope, Bindings bindings) { Map<String, Object> replacedProperties = new HashMap<String, Object>(); for (Object entryObject : bindings.entrySet()) { Entry<?, ?> entry = (Entry<?, ?>) entryObject; String name = (String) entry.getKey(); Object value = entry.getValue(); if (value != null) { // get the current property value, if set if (ScriptableObject.hasProperty(scope, name)) { replacedProperties.put(name, ScriptableObject.getProperty( scope, name)); } // wrap the new value and set it Object wrapped = ScriptRuntime.toObject(scope, value); ScriptableObject.putProperty(scope, name, wrapped); } } return replacedProperties; } private void getBoundProperties(Scriptable scope, Bindings bindings) { Object[] ids = scope.getIds(); for (Object id : ids) { if (id instanceof String) { String key = (String) id; Object value = scope.get(key, scope); if (value != Scriptable.NOT_FOUND) { if (value instanceof Wrapper) { bindings.put(key, ((Wrapper) value).unwrap()); } else { bindings.put(key, value); } } } } } private void resetBoundProperties(Scriptable scope, Map<String, Object> properties) { if (scope != null && properties != null && properties.size() > 0) { for (Entry<String, Object> entry : properties.entrySet()) { ScriptableObject.putProperty(scope, entry.getKey(), entry.getValue()); } } } private int optimizationLevel() { return ((RhinoJavaScriptEngineFactory)getFactory()).getOptimizationLevel(); } private class SlingCompiledScript extends CompiledScript { private final Script script; private final ScriptEngine engine; SlingCompiledScript(Script script, ScriptEngine engine) { this.script = script; this.engine = engine; } @Override public Object eval(ScriptContext scriptContext) throws ScriptException { Bindings bindings = scriptContext.getBindings(ScriptContext.ENGINE_SCOPE); // container for replaced properties Map<String, Object> replacedProperties = null; Scriptable scope = null; boolean isTopLevelCall = false; // create a rhino Context and execute the script try { final Context rhinoContext = Context.enter(); rhinoContext.setOptimizationLevel(optimizationLevel()); if (ScriptRuntime.hasTopCall(rhinoContext)) { // reuse the top scope if we are included scope = ScriptRuntime.getTopCallScope(rhinoContext); } else { // create the request top scope, use the ImporterToplevel here // to support the importPackage and importClasses functions scope = new ImporterTopLevel(); // Set the global scope to be our prototype scope.setPrototype(rootScope); // We want "scope" to be a new top-level scope, so set its // parent scope to null. This means that any variables created // by assignments will be properties of "scope". scope.setParentScope(null); // setup the context for use WrapFactory wrapFactory = ((RhinoJavaScriptEngineFactory) getFactory()).getWrapFactory(); rhinoContext.setWrapFactory(wrapFactory); // this is the top level call isTopLevelCall = true; } // add initial properties to the scope replacedProperties = setBoundProperties(scope, bindings); Object result = script.exec(rhinoContext, scope); if (result instanceof Wrapper) { result = ((Wrapper) result).unwrap(); } return (result instanceof Undefined) ? null : result; } catch (JavaScriptException t) { // prevent variables to be pushed back in case of errors isTopLevelCall = false; final ScriptException se = new ScriptException(t.details(), t.sourceName(), t.lineNumber()); // log the script stack trace ((Logger) bindings.get(SlingBindings.LOG)).error(t.getScriptStackTrace()); // set the exception cause Object value = t.getValue(); if (value != null) { if (value instanceof Wrapper) { value = ((Wrapper) value).unwrap(); } if (value instanceof Throwable) { se.initCause((Throwable) value); } } // if the cause could not be set, overwrite the stack trace if (se.getCause() == null) { se.setStackTrace(t.getStackTrace()); } throw se; } catch (Throwable t) { // prevent variables to be pushed back in case of errors isTopLevelCall = false; String scriptName = getScriptName(scriptContext); final ScriptException se = new ScriptException("Failure running script " + scriptName + ": " + t.getMessage()); se.initCause(t); throw se; } finally { // if we are the top call (the Context is now null) we have to // play back any properties from the scope back to the bindings if (isTopLevelCall) { getBoundProperties(scope, bindings); } // if properties have been replaced, reset them resetBoundProperties(scope, replacedProperties); ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); if (classLoader instanceof DynamicClassLoader) { DynamicClassLoader dynamicClassLoader = (DynamicClassLoader) classLoader; if (!dynamicClassLoader.isLive()) { /** * if the class loader on this thread is a dynamic class loader and it's dirty we should clear Rhino's class cache * to avoid class loader leaks */ if (scope != null) { ClassCache classCache = ClassCache.get(scope); classCache.clearCaches(); LOGGER.info("Detected dirty class loader on thread {}. Emptying Rhino's class cache.", Thread.currentThread() .getName()); } } } Context.exit(); } } @Override public ScriptEngine getEngine() { return engine; } } private String getScriptName(Reader scriptReader) { if(scriptReader instanceof ScriptNameAware){ return ((ScriptNameAware) scriptReader).getScriptName(); } return NO_SCRIPT_NAME; } private String getScriptName(ScriptContext scriptContext) { Bindings bindings = scriptContext.getBindings(ScriptContext.ENGINE_SCOPE); String scriptName = (String) bindings.get(ScriptEngine.FILENAME); if (scriptName != null && !"".equals(scriptName)) { return scriptName; } SlingScriptHelper sling = (SlingScriptHelper) bindings.get(SlingBindings.SLING); if (sling != null) { return sling.getScript().getScriptResource().getPath(); } return NO_SCRIPT_NAME; } }