/*
* 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.internal.template;
import java.io.PrintWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Type;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.xwiki.component.annotation.Component;
import org.xwiki.component.manager.ComponentLookupException;
import org.xwiki.component.manager.ComponentManager;
import org.xwiki.configuration.ConfigurationSource;
import org.xwiki.environment.Environment;
import org.xwiki.filter.input.InputSource;
import org.xwiki.filter.input.InputStreamInputSource;
import org.xwiki.filter.input.ReaderInputSource;
import org.xwiki.filter.input.StringInputSource;
import org.xwiki.job.event.status.JobProgressManager;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.DocumentReferenceResolver;
import org.xwiki.properties.BeanManager;
import org.xwiki.properties.ConverterManager;
import org.xwiki.properties.PropertyException;
import org.xwiki.properties.RawProperties;
import org.xwiki.properties.annotation.PropertyId;
import org.xwiki.rendering.block.Block;
import org.xwiki.rendering.block.GroupBlock;
import org.xwiki.rendering.block.RawBlock;
import org.xwiki.rendering.block.VerbatimBlock;
import org.xwiki.rendering.block.WordBlock;
import org.xwiki.rendering.block.XDOM;
import org.xwiki.rendering.internal.transformation.MutableRenderingContext;
import org.xwiki.rendering.parser.ContentParser;
import org.xwiki.rendering.renderer.BlockRenderer;
import org.xwiki.rendering.renderer.printer.WikiPrinter;
import org.xwiki.rendering.renderer.printer.WriterWikiPrinter;
import org.xwiki.rendering.syntax.Syntax;
import org.xwiki.rendering.transformation.RenderingContext;
import org.xwiki.rendering.transformation.TransformationContext;
import org.xwiki.rendering.transformation.TransformationManager;
import org.xwiki.security.authorization.AuthorExecutor;
import org.xwiki.skin.Resource;
import org.xwiki.skin.ResourceRepository;
import org.xwiki.skin.Skin;
import org.xwiki.template.Template;
import org.xwiki.template.TemplateContent;
import org.xwiki.velocity.VelocityManager;
import com.xpn.xwiki.XWiki;
import com.xpn.xwiki.internal.skin.AbstractEnvironmentResource;
import com.xpn.xwiki.internal.skin.InternalSkinManager;
import com.xpn.xwiki.internal.skin.WikiResource;
import com.xpn.xwiki.user.api.XWikiRightService;
/**
* Internal toolkit to experiment on templates.
*
* @version $Id: f3da3fa5f9607d3128d68ae13fe09bb7ea9fecc2 $
* @since 7.0M1
*/
@Component(roles = InternalTemplateManager.class)
@Singleton
public class InternalTemplateManager
{
private static final Pattern PROPERTY_LINE = Pattern.compile("^##!(.+)=(.*)$\r?\n?", Pattern.MULTILINE);
/**
* The reference of the superadmin user.
*/
private static final DocumentReference SUPERADMIN_REFERENCE =
new DocumentReference("xwiki", XWiki.SYSTEM_SPACE, XWikiRightService.SUPERADMIN_USER);
@Inject
private Environment environment;
@Inject
private ContentParser parser;
@Inject
private VelocityManager velocityManager;
/**
* Used to execute transformations.
*/
@Inject
private TransformationManager transformationManager;
@Inject
@Named("context")
private Provider<ComponentManager> componentManagerProvider;
@Inject
private RenderingContext renderingContext;
@Inject
@Named("plain/1.0")
private BlockRenderer plainRenderer;
@Inject
@Named("xwikicfg")
private ConfigurationSource xwikicfg;
@Inject
@Named("all")
private ConfigurationSource allConfiguration;
@Inject
@Named("currentmixed")
private DocumentReferenceResolver<String> currentMixedDocumentReferenceResolver;
@Inject
private BeanManager beanManager;
@Inject
private ConverterManager converter;
@Inject
private AuthorExecutor authorExecutor;
@Inject
private InternalSkinManager skins;
@Inject
private JobProgressManager progress;
@Inject
private Logger logger;
private static abstract class AbtractTemplate<T extends TemplateContent, R extends Resource<?>> implements Template
{
protected R resource;
protected T content;
public AbtractTemplate(R resource)
{
this.resource = resource;
}
@Override
public String getId()
{
return this.resource.getId();
}
@Override
public String getPath()
{
return this.resource.getPath();
}
@Override
public TemplateContent getContent() throws Exception
{
if (this.content == null) {
// TODO: work with streams instead of forcing String
String strinContent;
try (InputSource source = this.resource.getInputSource()) {
if (source instanceof StringInputSource) {
strinContent = source.toString();
} else if (source instanceof ReaderInputSource) {
strinContent = IOUtils.toString(((ReaderInputSource) source).getReader());
} else if (source instanceof InputStreamInputSource) {
// It's impossible to know the real attachment encoding, but let's assume that they respect the
// standard and use UTF-8 (which is required for the files located on the filesystem)
strinContent = IOUtils.toString(((InputStreamInputSource) source).getInputStream(),
StandardCharsets.UTF_8);
} else {
return null;
}
}
this.content = getContentInternal(strinContent);
}
return this.content;
}
protected abstract T getContentInternal(String content) throws Exception;
@Override
public String toString()
{
return this.resource.getId();
}
}
private class EnvironmentTemplate extends AbtractTemplate<FilesystemTemplateContent, AbstractEnvironmentResource>
{
EnvironmentTemplate(AbstractEnvironmentResource resource)
{
super(resource);
}
@Override
protected FilesystemTemplateContent getContentInternal(String content)
{
return new FilesystemTemplateContent(content);
}
}
private class DefaultTemplate extends AbtractTemplate<DefaultTemplateContent, Resource<?>>
{
DefaultTemplate(Resource<?> resource)
{
super(resource);
}
@Override
protected DefaultTemplateContent getContentInternal(String content)
{
if (this.resource instanceof WikiResource) {
return new DefaultTemplateContent(content, ((WikiResource<?>) this.resource).getAuthorReference());
} else {
return new DefaultTemplateContent(content);
}
}
}
private class DefaultTemplateContent implements RawProperties, TemplateContent
{
// TODO: work with streams instead
protected String content;
protected boolean authorProvided;
protected DocumentReference authorReference;
@PropertyId("source.syntax")
public Syntax sourceSyntax;
@PropertyId("raw.syntax")
public Syntax rawSyntax;
protected Map<String, Object> properties = new HashMap<>();
DefaultTemplateContent(String content)
{
this.content = content;
init();
}
DefaultTemplateContent(String content, DocumentReference authorReference)
{
this(content);
setAuthorReference(authorReference);
}
@Override
public Syntax getSourceSyntax()
{
return this.sourceSyntax;
}
@Override
public Syntax getRawSyntax()
{
return this.rawSyntax;
}
@Override
public <T> T getProperty(String name, T def)
{
if (!this.properties.containsKey(name)) {
return def;
}
if (def != null) {
return getProperty(name, def.getClass());
}
return (T) this.properties.get(name);
}
@Override
public <T> T getProperty(String name, Type type)
{
return converter.convert(type, this.properties.get(name));
}
protected void init()
{
Matcher matcher = PROPERTY_LINE.matcher(this.content);
Map<String, String> map = new HashMap<>();
while (matcher.find()) {
String key = matcher.group(1);
String value = matcher.group(2);
map.put(key, value);
// Remove the line from the content
this.content = this.content.substring(matcher.end());
}
try {
InternalTemplateManager.this.beanManager.populate(this, map);
} catch (PropertyException e) {
// Should never happen
InternalTemplateManager.this.logger.error("Failed to populate properties of template", e);
}
// The default is xhtml to support old templates
if (this.rawSyntax == null && this.sourceSyntax == null) {
this.rawSyntax = Syntax.XHTML_1_0;
}
}
@Override
public String getContent()
{
return this.content;
}
@PropertyId("author")
@Override
public DocumentReference getAuthorReference()
{
return this.authorReference;
}
protected void setAuthorReference(DocumentReference authorReference)
{
this.authorReference = authorReference;
this.authorProvided = true;
}
// RawProperties
@Override
public void set(String propertyName, Object value)
{
this.properties.put(propertyName, value);
}
}
private class FilesystemTemplateContent extends DefaultTemplateContent
{
public FilesystemTemplateContent(String content)
{
super(content);
// Give programming right to filesystem templates by default
setPrivileged(true);
}
/**
* {@inheritDoc}
* <p>
* Allow filesystem template to indicate the user to executed them with.
* </p>
*
* @see #setAuthorReference(DocumentReference)
*/
@Override
public void setAuthorReference(DocumentReference authorReference)
{
super.setAuthorReference(authorReference);
}
/**
* Made public to be seen as bean property.
*
* @since 6.3.1
* @since 6.4M1
*/
@SuppressWarnings("unused")
public boolean isPrivileged()
{
return SUPERADMIN_REFERENCE.equals(getAuthorReference());
}
/**
* Made public to be seen as bean property.
*
* @since 6.3.1
* @since 6.4M1
*/
public void setPrivileged(boolean privileged)
{
if (privileged) {
setAuthorReference(SUPERADMIN_REFERENCE);
} else {
// Reset author
this.authorReference = null;
this.authorProvided = false;
}
}
}
private String getResourcePath(String suffixPath, String templateName, boolean testExist)
{
String templatePath = suffixPath + templateName;
// Prevent inclusion of templates from other directories
String normalizedTemplate = URI.create(templatePath).normalize().toString();
if (!normalizedTemplate.startsWith(suffixPath)) {
this.logger.warn("Direct access to template file [{}] refused. Possible break-in attempt!",
normalizedTemplate);
return null;
}
if (testExist) {
// Check if the resource exist
if (this.environment.getResource(templatePath) == null) {
return null;
}
}
return templatePath;
}
private void renderError(Throwable throwable, Writer writer)
{
XDOM xdom = generateError(throwable);
render(xdom, writer);
}
private XDOM generateError(Throwable throwable)
{
List<Block> errorBlocks = new ArrayList<Block>();
// Add short message
Map<String, String> errorBlockParams = Collections.singletonMap("class", "xwikirenderingerror");
errorBlocks.add(
new GroupBlock(Arrays.<Block>asList(new WordBlock("Failed to render step content")), errorBlockParams));
// Add complete error
StringWriter writer = new StringWriter();
throwable.printStackTrace(new PrintWriter(writer));
Block descriptionBlock = new VerbatimBlock(writer.toString(), false);
Map<String, String> errorDescriptionBlockParams =
Collections.singletonMap("class", "xwikirenderingerrordescription hidden");
errorBlocks.add(new GroupBlock(Arrays.asList(descriptionBlock), errorDescriptionBlockParams));
return new XDOM(errorBlocks);
}
private void transform(Block block)
{
TransformationContext txContext =
new TransformationContext(block instanceof XDOM ? (XDOM) block : new XDOM(Arrays.asList(block)),
this.renderingContext.getDefaultSyntax(), this.renderingContext.isRestricted());
txContext.setId(this.renderingContext.getTransformationId());
txContext.setTargetSyntax(getTargetSyntax());
try {
this.transformationManager.performTransformations(block, txContext);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @param templateName the template to parse
* @return the result of the template parsing
*/
public XDOM getXDOMNoException(String templateName)
{
XDOM xdom;
try {
xdom = getXDOM(templateName);
} catch (Throwable e) {
this.logger.error("Error while getting template [{}] XDOM", templateName, e);
xdom = generateError(e);
}
return xdom;
}
/**
* @param template the template to parse
* @return the result of the template parsing
* @since 8.3RC1
*/
public XDOM getXDOMNoException(Template template)
{
XDOM xdom;
try {
xdom = getXDOM(template);
} catch (Throwable e) {
this.logger.error("Error while getting template [{}] XDOM", template.getId(), e);
xdom = generateError(e);
}
return xdom;
}
public XDOM getXDOM(Template template) throws Exception
{
XDOM xdom;
if (template != null) {
DefaultTemplateContent content = (DefaultTemplateContent) template.getContent();
xdom = getXDOM(template, content);
} else {
xdom = new XDOM(Collections.<Block>emptyList());
}
return xdom;
}
private XDOM getXDOM(Template template, DefaultTemplateContent content) throws Exception
{
XDOM xdom;
if (content.sourceSyntax != null) {
xdom = this.parser.parse(content.content, content.sourceSyntax);
} else {
String result = evaluateContent(template, content);
xdom = new XDOM(Arrays.asList(new RawBlock(result, content.rawSyntax)));
}
return xdom;
}
public XDOM getXDOM(String templateName) throws Exception
{
Template template = getTemplate(templateName);
return getXDOM(template);
}
public String renderNoException(String template)
{
Writer writer = new StringWriter();
renderNoException(template, writer);
return writer.toString();
}
public void renderNoException(String templateName, Writer writer)
{
try {
render(templateName, writer);
} catch (Exception e) {
this.logger.error("Error while rendering template [{}]", templateName, e);
renderError(e, writer);
}
}
/**
* @since 8.3RC1
*/
public void renderNoException(Template template, Writer writer)
{
try {
render(template, writer);
} catch (Exception e) {
this.logger.error("Error while rendering template [{}]", template, e);
renderError(e, writer);
}
}
public String render(String templateName) throws Exception
{
return renderFromSkin(templateName, (Skin) null);
}
public String renderFromSkin(String templateName, String skinId) throws Exception
{
Skin skin = this.skins.getSkin(skinId);
return skin != null ? renderFromSkin(templateName, skin) : null;
}
public String renderFromSkin(String templateName, Skin skin) throws Exception
{
Writer writer = new StringWriter();
renderFromSkin(templateName, skin, writer);
return writer.toString();
}
public void render(String templateName, Writer writer) throws Exception
{
renderFromSkin(templateName, null, writer);
}
public void renderFromSkin(final String templateName, ResourceRepository reposirory, final Writer writer)
throws Exception
{
this.progress.startStep(templateName, "template.render.message", "Render template [{}]", templateName);
try {
final Template template =
reposirory != null ? getTemplate(templateName, reposirory) : getTemplate(templateName);
if (template != null) {
final DefaultTemplateContent content = (DefaultTemplateContent) template.getContent();
if (content.authorProvided) {
this.authorExecutor.call(() -> {
render(template, content, writer);
return null;
}, content.getAuthorReference());
} else {
render(template, content, writer);
}
}
} finally {
this.progress.endStep(templateName);
}
}
public void render(Template template, Writer writer) throws Exception
{
DefaultTemplateContent content = (DefaultTemplateContent) template.getContent();
render(template, content, writer);
}
private void render(Template template, DefaultTemplateContent content, Writer writer) throws Exception
{
if (content.sourceSyntax != null) {
XDOM xdom = execute(template, content);
render(xdom, writer);
} else {
evaluateContent(template, content, writer);
}
}
private void render(XDOM xdom, Writer writer)
{
WikiPrinter printer = new WriterWikiPrinter(writer);
BlockRenderer blockRenderer;
try {
blockRenderer =
this.componentManagerProvider.get().getInstance(BlockRenderer.class, getTargetSyntax().toIdString());
} catch (ComponentLookupException e) {
blockRenderer = this.plainRenderer;
}
blockRenderer.render(xdom, printer);
}
public XDOM executeNoException(String templateName)
{
XDOM xdom;
try {
xdom = execute(templateName);
} catch (Throwable e) {
this.logger.error("Error while executing template [{}]", templateName, e);
xdom = generateError(e);
}
return xdom;
}
/**
* @since 8.3RC1
*/
public XDOM executeNoException(Template template)
{
XDOM xdom;
try {
xdom = execute(template);
} catch (Throwable e) {
this.logger.error("Error while executing template [{}]", template.getId(), e);
xdom = generateError(e);
}
return xdom;
}
private XDOM execute(Template template, DefaultTemplateContent content) throws Exception
{
XDOM xdom = getXDOM(template, content);
transform(xdom);
return xdom;
}
public XDOM execute(String templateName) throws Exception
{
final Template template = getTemplate(templateName);
if (template != null) {
final DefaultTemplateContent content = (DefaultTemplateContent) template.getContent();
if (content.authorProvided) {
return this.authorExecutor.call(() -> execute(template, content), content.getAuthorReference());
} else {
return execute(template, content);
}
}
return null;
}
public XDOM execute(Template template) throws Exception
{
if (template != null) {
final DefaultTemplateContent content = (DefaultTemplateContent) template.getContent();
if (content.authorProvided) {
return this.authorExecutor.call(() -> execute(template, content), content.getAuthorReference());
} else {
return execute(template, content);
}
}
return null;
}
private String evaluateContent(Template template, DefaultTemplateContent content) throws Exception
{
Writer writer = new StringWriter();
evaluateContent(template, content, writer);
return writer.toString();
}
private void evaluateContent(Template template, DefaultTemplateContent content, Writer writer) throws Exception
{
// Use the Transformation id as the name passed to the Velocity Engine. This name is used internally
// by Velocity as a cache index key for caching macros.
String namespace = this.renderingContext.getTransformationId();
boolean renderingContextPushed = false;
if (namespace == null) {
namespace = template.getId() != null ? template.getId() : "unknown namespace";
if (this.renderingContext instanceof MutableRenderingContext) {
// Make the current velocity template id available
((MutableRenderingContext) this.renderingContext).push(this.renderingContext.getTransformation(),
this.renderingContext.getXDOM(), this.renderingContext.getDefaultSyntax(), namespace,
this.renderingContext.isRestricted(), this.renderingContext.getTargetSyntax());
renderingContextPushed = true;
}
}
try {
this.velocityManager.evaluate(writer, namespace, new StringReader(content.content));
} finally {
// Get rid of temporary rendering context
if (renderingContextPushed) {
((MutableRenderingContext) this.renderingContext).pop();
}
}
}
private Syntax getTargetSyntax()
{
Syntax targetSyntax = this.renderingContext.getTargetSyntax();
return targetSyntax != null ? targetSyntax : Syntax.PLAIN_1_0;
}
private EnvironmentTemplate getFileSystemTemplate(String suffixPath, String templateName)
{
String path = getResourcePath(suffixPath, templateName, true);
return path != null
? new EnvironmentTemplate(new TemplateEnvironmentResource(path, templateName, this.environment)) : null;
}
private Template getClassloaderTemplate(String suffixPath, String templateName)
{
return getClassloaderTemplate(Thread.currentThread().getContextClassLoader(), suffixPath, templateName);
}
private Template getClassloaderTemplate(ClassLoader classloader, String suffixPath, String templateName)
{
String templatePath = suffixPath + templateName;
URL url = classloader.getResource(templatePath);
return url != null ? new DefaultTemplate(new ClassloaderResource(url, templateName)) : null;
}
private Template createTemplate(Resource<?> resource)
{
Template template;
if (resource instanceof AbstractEnvironmentResource) {
template = new EnvironmentTemplate((AbstractEnvironmentResource) resource);
} else {
template = new DefaultTemplate(resource);
}
return template;
}
public Template getResourceTemplate(String templateName, ResourceRepository repository)
{
Resource<?> resource = repository.getLocalResource(templateName);
if (resource != null) {
return createTemplate(resource);
}
return null;
}
public Template getTemplate(String templateName, ResourceRepository repository)
{
Resource<?> resource = repository.getResource(templateName);
if (resource != null) {
return createTemplate(resource);
}
return null;
}
public Template getTemplate(String templateName)
{
Template template = null;
// Try from skin
Skin skin = this.skins.getCurrentSkin(false);
if (skin != null) {
template = getTemplate(templateName, skin);
}
// Try from base skin if no skin is set
if (skin == null) {
Skin baseSkin = this.skins.getCurrentParentSkin(false);
if (baseSkin != null) {
template = getTemplate(templateName, baseSkin);
}
}
// Try from /templates/ environment resources
if (template == null) {
template = getFileSystemTemplate("/templates/", templateName);
}
// Try from current Thread classloader
if (template == null) {
template = getClassloaderTemplate("templates/", templateName);
}
return template;
}
}