/* * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.xwiki.rendering.internal.macro.script; import java.util.LinkedList; import java.util.List; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; import org.apache.commons.lang3.StringUtils; import org.xwiki.classloader.ExtendedURLClassLoader; import org.xwiki.component.annotation.Component; import org.xwiki.context.Execution; import org.xwiki.observation.EventListener; import org.xwiki.observation.event.CancelableEvent; import org.xwiki.observation.event.Event; import org.xwiki.rendering.macro.MacroExecutionException; import org.xwiki.rendering.macro.script.ScriptMacroParameters; import org.xwiki.script.event.ScriptEvaluatedEvent; import org.xwiki.script.event.ScriptEvaluatingEvent; import org.xwiki.security.authorization.ContextualAuthorizationManager; import org.xwiki.security.authorization.Right; /** * Replaces the context class loader by a custom one that takes into account the "jars" Script parameter that allows * to add jars that will be visible to the executing script. * * Listens to script evaluation events ({@link org.xwiki.script.event.ScriptEvaluatingEvent} and * {@link org.xwiki.script.event.ScriptEvaluatedEvent}) to set the context class loader and to restore the original * one. * * @version $Id: 1e4119f371e84ea8d8b1f783f644036be9bfff3e $ * @since 2.5M1 */ @Component @Named("scriptmacroclassloader") @Singleton public class ScriptClassLoaderHandlerListener implements EventListener { /** Key used to store the original class loader in the Execution Context. */ private static final String EXECUTION_CONTEXT_ORIG_CLASSLOADER_KEY = "originalClassLoader"; /** Key used to store the class loader used by scripts in the Execution Context, see {@link #execution}. */ private static final String EXECUTION_CONTEXT_CLASSLOADER_KEY = "scriptClassLoader"; /** Key under which the jar params used for the last macro execution are cached in the Execution Context. */ private static final String EXECUTION_CONTEXT_JARPARAMS_KEY = "scriptJarParams"; /** Used to check if programming rights is allowed. */ @Inject private ContextualAuthorizationManager authorizationManager; /** * Used to set the classLoader to be used by scripts across invocations. We save it in the Execution Context to be * sure it's the same classLoader used. */ @Inject private Execution execution; /** * Used to create a custom class loader that knows how to support JARs attached to wiki page. */ @Inject private AttachmentClassLoaderFactory attachmentClassLoaderFactory; @Override public String getName() { return "scriptmacroclassloader"; } @Override public List<Event> getEvents() { List<Event> events = new LinkedList<Event>(); events.add(new ScriptEvaluatingEvent()); events.add(new ScriptEvaluatedEvent()); return events; } @Override public void onEvent(Event event, Object source, Object data) { if (!(data instanceof ScriptMacroParameters)) { return; } if (event instanceof ScriptEvaluatingEvent) { // Set the context class loader to the script CL to ensure that any script engine using the context // classloader will work just fine. // Note: We must absolutely ensure that we always use the same context CL during the whole execution // request since JSR223 script engines (for example) that create internal class loaders need to // continue using these class loaders (where classes defined in scripts have been loaded for example). ScriptMacroParameters parameters = (ScriptMacroParameters) data; ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader(); this.execution.getContext().setProperty(EXECUTION_CONTEXT_ORIG_CLASSLOADER_KEY, originalClassLoader); try { ClassLoader newClassLoader = getClassLoader(parameters.getJars(), originalClassLoader); Thread.currentThread().setContextClassLoader(newClassLoader); } catch (Exception exception) { // abort execution ((CancelableEvent) event).cancel(exception.getMessage()); } } else if (event instanceof ScriptEvaluatedEvent) { // Restore original class loader. ClassLoader originalClassLoader = (ClassLoader) this.execution.getContext().getProperty(EXECUTION_CONTEXT_ORIG_CLASSLOADER_KEY); Thread.currentThread().setContextClassLoader(originalClassLoader); } } /** * @param jarsParameterValue the value of the macro parameters used to pass extra URLs that should be in the * execution class loader * @param parent the parent classloader for the classloader to create (if it doesn't already exist) * @return the class loader to use for executing the script * @throws MacroExecutionException in case of an error in building the class loader */ private ClassLoader getClassLoader(String jarsParameterValue, ClassLoader parent) throws MacroExecutionException { try { return findClassLoader(jarsParameterValue, parent); } catch (MacroExecutionException mee) { throw mee; } catch (Exception e) { throw new MacroExecutionException("Failed to add JAR URLs to the current class loader for [" + jarsParameterValue + "]", e); } } /** * @param jarsParameterValue the value of the macro parameters used to pass extra URLs that should be in the * execution class loader * @param parent the parent classloader for the classloader to create (if it doesn't already exist) * @return the class loader to use for executing the script * @throws Exception in case of an error in building the class loader */ private ClassLoader findClassLoader(String jarsParameterValue, ClassLoader parent) throws Exception { // We cache the Class Loader for improved performances and we check if the saved class loader had the same // jar parameters value as the current execution. If not, we compute a new class loader. ExtendedURLClassLoader cl = (ExtendedURLClassLoader) this.execution.getContext().getProperty(EXECUTION_CONTEXT_CLASSLOADER_KEY); if (cl == null) { if (StringUtils.isNotEmpty(jarsParameterValue)) { cl = createOrExtendClassLoader(true, jarsParameterValue, parent); } else { cl = this.attachmentClassLoaderFactory.createAttachmentClassLoader("", parent); } } else { String cachedJarsParameterValue = (String) this.execution.getContext().getProperty(EXECUTION_CONTEXT_JARPARAMS_KEY); if (cachedJarsParameterValue != jarsParameterValue) { cl = createOrExtendClassLoader(false, jarsParameterValue, cl); } } this.execution.getContext().setProperty(EXECUTION_CONTEXT_CLASSLOADER_KEY, cl); return cl; } /** * @param createNewClassLoader if true create a new classloader and if false extend an existing one with the passed * additional jars * @param jarsParameterValue the value of the macro parameters used to pass extra URLs that should be in the * execution class loader * @param classLoader the parent classloader for the classloader to create or the classloader to extend, depending * on the value of the createNewClassLoader parameter * @return the new classloader or the extended one * @throws Exception in case of an error in building or extending the class loader */ private ExtendedURLClassLoader createOrExtendClassLoader(boolean createNewClassLoader, String jarsParameterValue, ClassLoader classLoader) throws Exception { ExtendedURLClassLoader cl; if (canHaveJarsParameters()) { if (createNewClassLoader) { cl = this.attachmentClassLoaderFactory.createAttachmentClassLoader(jarsParameterValue, classLoader); } else { cl = (ExtendedURLClassLoader) classLoader; this.attachmentClassLoaderFactory.extendAttachmentClassLoader(jarsParameterValue, cl); } this.execution.getContext().setProperty(EXECUTION_CONTEXT_JARPARAMS_KEY, jarsParameterValue); } else { throw new MacroExecutionException( "You cannot pass additional jars since you don't have programming rights"); } return cl; } /** * Note that this method allows extending classes to override it to allow jars parameters to be used without * programming rights for example or to use some other conditions. * * @return true if the user can use the macro parameter used to pass additional JARs to the class loader used to * evaluate a script */ private boolean canHaveJarsParameters() { return this.authorizationManager.hasAccess(Right.PROGRAM); } }