/* * Copyright (C) 2005-2007 Alfresco Software Limited. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * As a special exception to the terms and conditions of version 2.0 of * the GPL, you may redistribute this Program in connection with Free/Libre * and Open Source Software ("FLOSS") applications as described in Alfresco's * FLOSS exception. You should have recieved a copy of the text describing * the FLOSS exception, and it is also available here: * http://www.alfresco.com/legal/licensing" */ package org.alfresco.repo.jscript; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.StringTokenizer; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.model.ContentModel; import org.alfresco.repo.processor.BaseProcessor; import org.alfresco.scripts.ScriptException; import org.alfresco.scripts.ScriptResourceHelper; import org.alfresco.scripts.ScriptResourceLoader; import org.alfresco.service.cmr.model.FileInfo; import org.alfresco.service.cmr.model.FileNotFoundException; import org.alfresco.service.cmr.repository.ContentIOException; import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.ProcessorExtension; import org.alfresco.service.cmr.repository.ScriptLocation; import org.alfresco.service.cmr.repository.ScriptProcessor; import org.alfresco.service.cmr.repository.StoreRef; import org.alfresco.service.namespace.QName; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.mozilla.javascript.Context; import org.mozilla.javascript.ImporterTopLevel; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.WrapFactory; import org.springframework.util.FileCopyUtils; /** * Implementation of the ScriptEninge using the Rhino JavaScript engine. * * @author Kevin Roast */ public class RhinoScriptProcessor extends BaseProcessor implements ScriptProcessor, ScriptResourceLoader { private static final Log logger = LogFactory.getLog(RhinoScriptProcessor.class); private static final String PATH_CLASSPATH = "classpath:"; /** Wrap Factory */ private static WrapFactory wrapFactory = new RhinoWrapFactory(); /** Base Value Converter */ private ValueConverter valueConverter = new ValueConverter(); /** Store into which to resolve cm:name based script paths */ private StoreRef storeRef; /** Store root path to resolve cm:name based scripts path from */ private String storePath; /** * Set the default store reference * * @param storeRef The default store reference */ public void setStoreUrl(String storeRef) { this.storeRef = new StoreRef(storeRef); } /** * @param storePath The store path to set. */ public void setStorePath(String storePath) { this.storePath = storePath; } /** * @see org.alfresco.service.cmr.repository.ScriptProcessor#execute(org.alfresco.service.cmr.repository.ScriptLocation, java.util.Map) */ public Object execute(ScriptLocation location, Map model) { try { ByteArrayOutputStream os = new ByteArrayOutputStream(); FileCopyUtils.copy(location.getInputStream(), os); // both streams are closed byte[] bytes = os.toByteArray(); // create the script string from the byte[] return executeScriptImpl(resolveScriptImports(new String(bytes)), model, location.isSecure()); } catch (Throwable err) { throw new ScriptException("Failed to execute script '" + location.toString() + "': " + err.getMessage(), err); } } /** * @see org.alfresco.service.cmr.repository.ScriptProcessor#execute(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName, java.util.Map) */ public Object execute(NodeRef nodeRef, QName contentProp, Map model) { try { if (this.services.getNodeService().exists(nodeRef) == false) { throw new AlfrescoRuntimeException("Script Node does not exist: " + nodeRef); } if (contentProp == null) { contentProp = ContentModel.PROP_CONTENT; } ContentReader cr = this.services.getContentService().getReader(nodeRef, contentProp); if (cr == null || cr.exists() == false) { throw new AlfrescoRuntimeException("Script Node content not found: " + nodeRef); } return executeScriptImpl(resolveScriptImports(cr.getContentString()), model, false); } catch (Throwable err) { throw new ScriptException("Failed to execute script '" + nodeRef.toString() + "': " + err.getMessage(), err); } } /** * @see org.alfresco.service.cmr.repository.ScriptProcessor#execute(java.lang.String, java.util.Map) */ public Object execute(String location, Map model) { try { InputStream stream = getClass().getClassLoader().getResourceAsStream(location); if (stream == null) { throw new AlfrescoRuntimeException("Unable to load classpath resource: " + location); } ByteArrayOutputStream os = new ByteArrayOutputStream(); FileCopyUtils.copy(stream, os); // both streams are closed byte[] bytes = os.toByteArray(); return executeScriptImpl(resolveScriptImports(new String(bytes, "UTF-8")), model, true); } catch (Throwable err) { throw new ScriptException("Failed to execute script '" + location + "': " + err.getMessage(), err); } } /** * @see org.alfresco.service.cmr.repository.ScriptProcessor#executeString(java.lang.String, java.util.Map) */ public Object executeString(String script, Map model) { try { return executeScriptImpl(resolveScriptImports(script), model, true); } catch (Throwable err) { throw new ScriptException("Failed to execute supplied script: " + err.getMessage(), err); } } /** * Resolve the imports in the specified script. Supported include directives are of the following form: *
     * 
     * 
     * 
     * 
* Either a classpath resource, NodeRef or cm:name path based script can be includes. Multiple includes * of the same script are dealt with correctly and nested includes of scripts is fully supported. *

* Note that for performance reasons the script import directive syntax and placement in the file * is very strict. The import lines must always be first in the file - even before any comments. * Immediately that the script service detects a non-import line it will assume the rest of the * file is executable script and no longer attempt to search for any further import directives. Therefore * all imports should be at the top of the script, one following the other, in the correct syntax and with * no comments present - the only separators valid between import directives is white space. * * @param script The script content to resolve imports in * * @return a valid script with all nested includes resolved into a single script instance */ private String resolveScriptImports(String script) { return ScriptResourceHelper.resolveScriptImports(script, this, logger); } /** * Load a script content from the specific resource path. * * @param resource Resources can be of the form: *

     * classpath:alfresco/includeme.js
     * workspace://SpacesStore/6f73de1b-d3b4-11db-80cb-112e6c2ea048
     * /Company Home/Data Dictionary/Scripts/includeme.js
     * 
* * @return the content from the resource, null if not recognised format * * @throws AlfrescoRuntimeException on any IO or ContentIO error */ public String loadScriptResource(String resource) { String result = null; if (resource.startsWith(PATH_CLASSPATH)) { try { // load from classpath String scriptClasspath = resource.substring(PATH_CLASSPATH.length()); InputStream stream = getClass().getClassLoader().getResourceAsStream(scriptClasspath); if (stream == null) { throw new AlfrescoRuntimeException("Unable to load included script classpath resource: " + resource); } ByteArrayOutputStream os = new ByteArrayOutputStream(); FileCopyUtils.copy(stream, os); // both streams are closed byte[] bytes = os.toByteArray(); // create the string from the byte[] using encoding if necessary result = new String(bytes, "UTF-8"); } catch (IOException err) { throw new AlfrescoRuntimeException("Unable to load included script classpath resource: " + resource); } } else { NodeRef scriptRef; if (resource.startsWith("/")) { // resolve from default SpacesStore as cm:name based path // TODO: remove this once FFS correctly allows name path resolving from store root! NodeRef rootNodeRef = this.services.getNodeService().getRootNode(this.storeRef); List nodes = this.services.getSearchService().selectNodes( rootNodeRef, this.storePath, null, this.services.getNamespaceService(), false); if (nodes.size() == 0) { throw new AlfrescoRuntimeException("Unable to find store path: " + this.storePath); } StringTokenizer tokenizer = new StringTokenizer(resource, "/"); List elements = new ArrayList(6); if (tokenizer.hasMoreTokens()) { tokenizer.nextToken(); } while (tokenizer.hasMoreTokens()) { elements.add(tokenizer.nextToken()); } try { FileInfo fileInfo = this.services.getFileFolderService().resolveNamePath(nodes.get(0), elements); scriptRef = fileInfo.getNodeRef(); } catch (FileNotFoundException err) { throw new AlfrescoRuntimeException("Unable to load included script repository resource: " + resource); } } else { scriptRef = new NodeRef(resource); } // load from NodeRef default content property try { ContentReader cr = this.services.getContentService().getReader(scriptRef, ContentModel.PROP_CONTENT); if (cr == null || cr.exists() == false) { throw new AlfrescoRuntimeException("Included Script Node content not found: " + resource); } result = cr.getContentString(); } catch (ContentIOException err) { throw new AlfrescoRuntimeException("Unable to load included script repository resource: " + resource); } } return result; } /** * Execute the supplied script content. Adds the default data model and custom configured root * objects into the root scope for access by the script. * * @param script The script to execute. * @param model Data model containing objects to be added to the root scope. * @param secure True if the script is considered secure and may access java.* libs directly * * @return result of the script execution, can be null. * * @throws AlfrescoRuntimeException */ private Object executeScriptImpl(String script, Map model, boolean secure) throws AlfrescoRuntimeException { long startTime = 0; if (logger.isDebugEnabled()) { startTime = System.currentTimeMillis(); } // Convert the model model = convertToRhinoModel(model); // check that rhino script engine is available Context cx = Context.enter(); try { // The easiest way to embed Rhino is just to create a new scope this way whenever // you need one. However, initStandardObjects is an expensive method to call and it // allocates a fair amount of memory. cx.setWrapFactory(wrapFactory); Scriptable scope; if (!secure) { scope = cx.initStandardObjects(); // remove security issue related objects - this ensures the script may not access // unsecure java.* libraries or import any other classes for direct access - only // the configured root host objects will be available to the script writer scope.delete("Packages"); scope.delete("getClass"); scope.delete("java"); } else { // allow access to all libraries and objects, including the importer // @see http://www.mozilla.org/rhino/ScriptingJava.html scope = new ImporterTopLevel(cx); } // there's always a model, if only to hold the util objects if (model == null) { model = new HashMap(); } // add the global scripts for (ProcessorExtension ex : this.processorExtensions.values()) { model.put(ex.getExtensionName(), ex); } // insert supplied object model into root of the default scope for (String key : model.keySet()) { // set the root scope on appropriate objects // this is used to allow native JS object creation etc. Object obj = model.get(key); if (obj instanceof Scopeable) { ((Scopeable)obj).setScope(scope); } // convert/wrap each object to JavaScript compatible Object jsObject = Context.javaToJS(obj, scope); // insert into the root scope ready for access by the script ScriptableObject.putProperty(scope, key, jsObject); } // execute the script Object result = cx.evaluateString(scope, script, "AlfrescoScript", 1, null); // extract java object result if wrapped by Rhino return valueConverter.convertValueForRepo((Serializable)result); } catch (Throwable err) { throw new AlfrescoRuntimeException(err.getMessage(), err); } finally { Context.exit(); if (logger.isDebugEnabled()) { long endTime = System.currentTimeMillis(); logger.debug("Time to execute script: " + (endTime - startTime) + "ms"); } } } /** * Converts the passed model into a Rhino model * * @param model the model * * @return Map the converted model */ private Map convertToRhinoModel(Map model) { Map newModel = null; if (model != null) { newModel = new HashMap(model.size()); for (Map.Entry entry : model.entrySet()) { if (entry.getValue() instanceof NodeRef) { newModel.put(entry.getKey(), new ScriptNode((NodeRef)entry.getValue(), this.services)); } else { newModel.put(entry.getKey(), entry.getValue()); } } } else { newModel = new HashMap(0); } return newModel; } /** * Rhino script value wraper */ private static class RhinoWrapFactory extends WrapFactory { /* (non-Javadoc) * @see org.mozilla.javascript.WrapFactory#wrapAsJavaObject(org.mozilla.javascript.Context, org.mozilla.javascript.Scriptable, java.lang.Object, java.lang.Class) */ public Scriptable wrapAsJavaObject(Context cx, Scriptable scope, Object javaObject, Class staticType) { if (javaObject instanceof Map && !(javaObject instanceof ScriptableHashMap)) { return new NativeMap(scope, (Map)javaObject); } return super.wrapAsJavaObject(cx, scope, javaObject, staticType); } } }