/* * 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 com.xpn.xwiki.render; import java.io.Reader; import java.io.Writer; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import javax.inject.Inject; import javax.inject.Provider; import javax.inject.Singleton; import javax.script.ScriptContext; import org.apache.commons.io.output.NullWriter; import org.apache.velocity.VelocityContext; import org.apache.velocity.runtime.RuntimeConstants; import org.apache.velocity.runtime.RuntimeSingleton; import org.slf4j.Logger; import org.xwiki.component.annotation.Component; import org.xwiki.component.phase.Initializable; import org.xwiki.component.phase.InitializationException; import org.xwiki.context.Execution; import org.xwiki.context.ExecutionContext; import org.xwiki.observation.EventListener; import org.xwiki.observation.ObservationManager; import org.xwiki.observation.event.Event; import org.xwiki.script.ScriptContextManager; import org.xwiki.security.authorization.AuthorExecutor; import org.xwiki.skin.Skin; import org.xwiki.skin.SkinManager; import org.xwiki.template.Template; import org.xwiki.template.TemplateManager; import org.xwiki.template.event.TemplateDeletedEvent; import org.xwiki.template.event.TemplateEvent; import org.xwiki.template.event.TemplateUpdatedEvent; import org.xwiki.velocity.VelocityConfiguration; import org.xwiki.velocity.VelocityEngine; import org.xwiki.velocity.VelocityFactory; import org.xwiki.velocity.VelocityManager; import org.xwiki.velocity.XWikiVelocityException; import org.xwiki.velocity.XWikiWebappResourceLoader; import org.xwiki.velocity.internal.VelocityExecutionContextInitializer; import com.xpn.xwiki.XWikiContext; import com.xpn.xwiki.api.DeprecatedContext; /** * Note: This class should be moved to the Velocity module. However this is not possible right now since we need to * populate the Velocity Context with XWiki objects that are located in the Core (such as the XWiki object for example) * and since the Core needs to call the Velocity module this would cause a circular dependency. * * @version $Id: 92a0b566350dccacf76f54fe0c8eacee2b7125f2 $ * @since 1.5M1 */ @Component @Singleton // TODO: refactor to move it in xwiki-commons, the dependencies on the model are actually quite minor public class DefaultVelocityManager implements VelocityManager, Initializable { /** * The name of the Velocity configuration property that specifies the ResourceLoader name that Velocity should use * when locating templates. */ private static final String RESOURCE_LOADER = "resource.loader"; /** * The name of the Velocity configuration property that specifies the ResourceLoader class to use to locate Velocity * templates. */ private static final String RESOURCE_LOADER_CLASS = "xwiki.resource.loader.class"; private static final String VELOCITYENGINE_CACHEKEY_NAME = "velocity.engine.key"; private static final List<Event> EVENTS = Arrays.<Event>asList(new TemplateUpdatedEvent(), new TemplateDeletedEvent()); /** * Used to access the current {@link org.xwiki.context.ExecutionContext}. */ @Inject private Execution execution; /** * Used to access the current {@link XWikiContext}. */ @Inject private Provider<XWikiContext> xcontextProvider; /** * Used to get the current script context. */ @Inject private ScriptContextManager scriptContextManager; @Inject private VelocityFactory velocityFactory; @Inject private VelocityConfiguration velocityConfiguration; /** * Accessing it trough {@link Provider} since {@link TemplateManager} depends on {@link VelocityManager}. */ @Inject private Provider<TemplateManager> templates; @Inject private SkinManager skinManager; @Inject private ObservationManager observation; @Inject private AuthorExecutor authorExecutor; @Inject private Logger logger; /** * Binding that should stay on Velocity side only. */ private final Set<String> reservedBindings = new HashSet<>(); @Override public void initialize() throws InitializationException { this.observation.addListener(new EventListener() { @Override public void onEvent(Event event, Object source, Object data) { if (event instanceof TemplateEvent) { TemplateEvent templateEvent = (TemplateEvent) event; DefaultVelocityManager.this.velocityFactory.removeVelocityEngine(templateEvent.getId()); } } @Override public String getName() { return DefaultVelocityManager.class.getName(); } @Override public List<Event> getEvents() { return EVENTS; } }); // Set reserved bindings // "context" is a reserved binding in JSR223 world this.reservedBindings.add("context"); // Macros directive this.reservedBindings.add("macro"); // Foreach directive this.reservedBindings.add("foreach"); this.reservedBindings.add(this.velocityConfiguration.getProperties().getProperty(RuntimeConstants.COUNTER_NAME, RuntimeSingleton.getString(RuntimeConstants.COUNTER_NAME))); this.reservedBindings.add(this.velocityConfiguration.getProperties().getProperty(RuntimeConstants.HAS_NEXT_NAME, RuntimeSingleton.getString(RuntimeConstants.HAS_NEXT_NAME))); // Evaluate directive this.reservedBindings.add("evaluate"); // TryCatch directive this.reservedBindings.add("exception"); this.reservedBindings.add("try"); // Default directive this.reservedBindings.add("define"); // The name of the context variable used for the template-level scope this.reservedBindings.add("template"); } @Override public VelocityContext getVelocityContext() { ScriptVelocityContext velocityContext; // Make sure the velocity context support ScriptContext synchronization VelocityContext currentVelocityContext = getCurrentVelocityContext(); if (currentVelocityContext instanceof ScriptVelocityContext) { velocityContext = (ScriptVelocityContext) currentVelocityContext; } else { velocityContext = new ScriptVelocityContext(currentVelocityContext, this.reservedBindings); this.execution.getContext().setProperty(VelocityExecutionContextInitializer.VELOCITY_CONTEXT_ID, velocityContext); } // Synchronize with ScriptContext ScriptContext scriptContext = this.scriptContextManager.getScriptContext(); velocityContext.setScriptContext(scriptContext); // Velocity specific bindings XWikiContext xcontext = this.xcontextProvider.get(); // Add the "context" binding which is deprecated since 1.9.1. velocityContext.put("context", new DeprecatedContext(xcontext)); return velocityContext; } @Override public VelocityContext getCurrentVelocityContext() { // The Velocity Context is set in VelocityExecutionContextInitializer, when the XWiki Request is initialized // so we are guaranteed it is defined when this method is called. return (VelocityContext) this.execution.getContext() .getProperty(VelocityExecutionContextInitializer.VELOCITY_CONTEXT_ID); } /** * @return the key used to cache the Velocity Engines. We have one Velocity Engine per skin which has a macros.vm * file on the filesystem. Right now we don't support macros.vm defined in custom skins in wiki pages. */ private Template getVelocityEngineMacrosTemplate() { Template template = null; Map<String, Template> templateCache = null; Skin currentSkin = this.skinManager.getCurrentSkin(true); // Generating this key is very expensive so we cache it in the context ExecutionContext econtext = this.execution.getContext(); if (econtext != null) { templateCache = (Map<String, Template>) econtext.getProperty(VELOCITYENGINE_CACHEKEY_NAME); if (templateCache == null) { templateCache = new HashMap<>(); econtext.setProperty(VELOCITYENGINE_CACHEKEY_NAME, templateCache); } else { template = templateCache.get(currentSkin.getId()); } } if (template == null) { template = this.templates.get().getTemplate("macros.vm"); if (templateCache != null) { templateCache.put(currentSkin.getId(), template); } } return template; } /** * @return the Velocity Engine corresponding to the current execution context. More specifically returns the * Velocity Engine for the current skin since each skin has its own Velocity Engine so that each skin can * have global velocimacros defined * @throws XWikiVelocityException in case of an error while creating a Velocity Engine */ @Override public VelocityEngine getVelocityEngine() throws XWikiVelocityException { // Note: For improved performance we cache the Velocity Engines in order not to // recreate them all the time. The key we use is the location to the skin's macro.vm // file since caching on the skin would create more Engines than needed (some skins // don't have a macros.vm file and some skins inherit from others). // Create a Velocity context using the Velocity Manager associated to the current skin's // macros.vm // Get the location of the skin's macros.vm file XWikiContext xcontext = this.xcontextProvider.get(); final Template template; if (xcontext != null && xcontext.getWiki() != null) { template = getVelocityEngineMacrosTemplate(); } else { template = null; } String cacheKey = template != null ? template.getId() : "default"; // Get the Velocity Engine to use VelocityEngine velocityEngine = this.velocityFactory.getVelocityEngine(cacheKey); if (velocityEngine == null) { // Note 1: This block is synchronized to prevent threads from creating several instances of // Velocity Engines (for the same skin). // Note 2: We do this instead of marking the whole method as synchronized since it seems this method is // called quite often and we would incur the synchronization penalty. Ideally the engine should be // created only when a new skin is created and not be on the main execution path. synchronized (this) { velocityEngine = this.velocityFactory.getVelocityEngine(cacheKey); if (velocityEngine == null) { // Gather the global Velocity macros that we want to have. These are skin dependent. Properties properties = new Properties(); // If the user hasn't specified any custom Velocity Resource Loader to use, use the XWiki Resource // Loader if (!this.velocityConfiguration.getProperties().containsKey(RESOURCE_LOADER)) { properties.setProperty(RESOURCE_LOADER, "xwiki"); properties.setProperty(RESOURCE_LOADER_CLASS, XWikiWebappResourceLoader.class.getName()); } if (xcontext != null && xcontext.getWiki() != null) { // Note: if you don't want any template to be used set the property named // xwiki.render.velocity.macrolist to an empty string value. String macroList = xcontext.getWiki().Param("xwiki.render.velocity.macrolist"); if (macroList == null) { macroList = "/templates/macros.vm"; } properties.put(RuntimeConstants.VM_LIBRARY, macroList); } velocityEngine = this.velocityFactory.createVelocityEngine(cacheKey, properties); if (template != null) { // Local macros template // We execute it ourself to support any kind of template, Velocity only support resource // template by default try { final VelocityEngine finalVelocityEngine = velocityEngine; this.authorExecutor.call(() -> { finalVelocityEngine.evaluate(new VelocityContext(), NullWriter.NULL_WRITER, "", template.getContent().getContent()); return null; }, template.getContent().getAuthorReference()); } catch (Exception e) { this.logger.error("Failed to evaluate macros templates [{}]", template.getPath(), e); } } } } } return velocityEngine; } @Override public boolean evaluate(Writer out, String templateName, Reader source) throws XWikiVelocityException { // Get up to date Velocity context VelocityContext velocityContext = getVelocityContext(); // Execute Velocity context return getVelocityEngine().evaluate(velocityContext, out, templateName, source); } }