/*
* 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.plugin.skinx;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xwiki.model.reference.DocumentReferenceResolver;
import org.xwiki.model.reference.EntityReferenceSerializer;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.api.Api;
import com.xpn.xwiki.internal.cache.rendering.CachedItem;
import com.xpn.xwiki.internal.cache.rendering.CachedItem.UsedExtension;
import com.xpn.xwiki.internal.cache.rendering.RenderingCacheAware;
import com.xpn.xwiki.plugin.XWikiDefaultPlugin;
import com.xpn.xwiki.plugin.XWikiPluginInterface;
import com.xpn.xwiki.web.Utils;
/**
* <p>
* Skin Extensions base plugin. It allows templates and document content to pull required clientside code in the
* generated XHTML (or whatever XML) content.
* </p>
* <p>
* The API provides a method {@link SkinExtensionPluginApi#use(String)}, which, when called, marks an extension as used
* in the current result. Later on, all the used extensions are inserted in the content, by replacing the first
* occurrence of the following string: <tt><!-- canonical.plugin.classname --></tt>, where the actual extension
* type classname is used. For example, JS extensions are inserted in place of
* <tt><!-- com.xpn.xwiki.plugin.skinx.JsSkinExtensionPlugin --></tt>.
* </p>
*
* @see SkinExtensionPluginApi
* @see JsSkinExtensionPlugin
* @see CssSkinExtensionPlugin
* @see LinkExtensionPlugin
* @version $Id: c3d056866ed3aaca50b193ce05d31445ae9872d5 $
*/
@SuppressWarnings("deprecation")
public abstract class AbstractSkinExtensionPlugin extends XWikiDefaultPlugin implements RenderingCacheAware
{
/** Log object to log messages in this class. */
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractSkinExtensionPlugin.class);
/** The name of the context key for the list of pulled extensions. */
protected final String contextKey = this.getClass().getCanonicalName();
/** The name of the context key for the additional parameters for pulled extensions. */
protected final String parametersContextKey = this.getClass().getCanonicalName() + "_parameters";
/**
* @see #getDefaultEntityReferenceSerializer()
*/
private EntityReferenceSerializer<String> defaultEntityReferenceSerializer;
/**
* @see #getCurrentDocumentReferenceResolver()
*/
private DocumentReferenceResolver<String> currentDocumentReferenceResolver;
/**
* XWiki plugin constructor.
*
* @param name The name of the plugin, which can be used for retrieving the plugin API from velocity. Unused.
* @param className The canonical classname of the plugin. Unused.
* @param context The current request context.
* @see com.xpn.xwiki.plugin.XWikiDefaultPlugin#XWikiDefaultPlugin(String,String,com.xpn.xwiki.XWikiContext)
*/
public AbstractSkinExtensionPlugin(String name, String className, XWikiContext context)
{
super(name, className, context);
}
/**
* Abstract method for obtaining a link that points to the actual pulled resource. Each type of resource has its own
* format for the link, for example Javascript uses <code><script src="/path/to/Document"></code>, while CSS
* uses <code><link rel="stylesheet" href="/path/to/Document"></code> (the actual syntax is longer, this is
* just a simplified example).
*
* @param resource the name of the wiki document holding the resource.
* @param context the current request context, needed to access the URLFactory.
* @return A <code>String</code> representation of the linking element that should be printed in the generated HTML.
*/
public abstract String getLink(String resource, XWikiContext context);
/**
* Returns the list of always used extensions of this type. Which resources are always used depends on the type of
* resource, for example document based StyleSheet extensions have a property in the object, <tt>use</tt>, which can
* have the value <tt>always</tt> to declare that an extension should always be used.
*
* @param context The current request context.
* @return A set of resource names that should be pulled in the current response. Note that this method is called
* for each request, as the list might change in time, and it can be different for each wiki in a farm.
*/
public abstract Set<String> getAlwaysUsedExtensions(XWikiContext context);
/**
* Determines if the requested document contains on page skin extension objects of this type. True if at least one
* of the extension objects has the <tt>currentPage</tt> value for the <tt>use</tt> property.
*
* @param context the current request context
* @return a boolean specifying if the current document contains on page skin extensions
*/
public abstract boolean hasPageExtensions(XWikiContext context);
@Override
public Api getPluginApi(XWikiPluginInterface plugin, XWikiContext context)
{
return new SkinExtensionPluginApi((AbstractSkinExtensionPlugin) plugin, context);
}
/**
* Mark a resource as used in the current result. A resource is registered only once per request, further calls will
* not result in additional links, even if it is pulled with different parameters.
*
* @param resource The name of the resource to pull.
* @param context The current request context.
* @see #use(String, Map, XWikiContext)
*/
public void use(String resource, XWikiContext context)
{
LOGGER.debug("Using [{}] as [{}] extension", resource, this.getName());
getPulledResources(context).add(resource);
// In case a previous call added some parameters, remove them, since the last call for a resource always
// discards previous ones.
getParametersMap(context).remove(resource);
}
/**
* Mark a skin extension document as used in the current result, together with some parameters. How the parameters
* are used, depends on the type of resource being pulled. For example, JS and CSS extensions use the parameters in
* the resulting URL, while Link extensions use the parameters as attributes of the link tag. A resource is
* registered only once per request, further calls will not result in additional links, even if it is pulled with
* different parameters. If more than one calls per request are made, the parameters used are the ones from the last
* call (or none, if the last call did not specify any parameters).
*
* @param resource The name of the resource to pull.
* @param parameters The parameters for this resource.
* @param context The current request context.
* @see #use(String, XWikiContext)
*/
public void use(String resource, Map<String, Object> parameters, XWikiContext context)
{
use(resource, context);
getParametersMap(context).put(resource, parameters);
}
/**
* Get the list of pulled resources (of the plugin's type) for the current request. The returned list is always
* valid.
*
* @param context The current request context.
* @return A set of names that holds the resources pulled in the current request.
*/
@SuppressWarnings("unchecked")
protected Set<String> getPulledResources(XWikiContext context)
{
initializeRequestListIfNeeded(context);
return (Set<String>) context.get(this.contextKey);
}
/**
* Get the map of additional parameters for each pulled resource (of the plugin's type) for the current request. The
* returned map is always valid.
*
* @param context The current request context.
* @return A map of resource parameters, where the key is the resource's name, and the value is a map holding the
* actual parameters for a given resource. If a resource was pulled without additional parameters, then no
* corresponding entry is added in this map.
*/
@SuppressWarnings("unchecked")
protected Map<String, Map<String, Object>> getParametersMap(XWikiContext context)
{
initializeRequestListIfNeeded(context);
return (Map<String, Map<String, Object>>) context.get(this.parametersContextKey);
}
/**
* Initializes the list of pulled extensions corresponding to this request, if it wasn't already initialized. This
* method is not thread safe, since a context should not be shared among threads.
*
* @param context The current context where this list is stored.
*/
protected void initializeRequestListIfNeeded(XWikiContext context)
{
if (!context.containsKey(this.contextKey)) {
context.put(this.contextKey, new LinkedHashSet<String>());
}
if (!context.containsKey(this.parametersContextKey)) {
context.put(this.parametersContextKey, new HashMap<String, Map<String, Object>>());
}
}
/**
* Composes and returns the links to the resources pulled in the current request. This method is called at the end
* of each request, once for each type of resource (subclass), and the result is placed in the generated XHTML.
*
* @param context The current request context.
* @return a XHMTL fragment with all extensions imports statements for this request. This includes both extensions
* that are defined as being "used always" and "on demand" extensions explicitly requested for this page.
* Always used extensions are always, before on demand extensions, so that on demand extensions can override
* more general elements in the always used ones.
*/
public String getImportString(XWikiContext context)
{
StringBuilder result = new StringBuilder();
// Using LinkedHashSet to preserve the extensions order.
Set<String> extensions = new LinkedHashSet<String>();
// First, we add to the import string the extensions that should always be used.
// TODO Global extensions should be able to select a set of actions for which they are enabled.
extensions.addAll(getAlwaysUsedExtensions(context));
// Then, we add On-Demand extensions for this request.
extensions.addAll(getPulledResources(context));
// Add On-Page extensions
if (hasPageExtensions(context)) {
// Make sure to use a prefixed document full name for the current document as well, or else the "extensions"
// set will not detect if it was added before and it will be added twice.
EntityReferenceSerializer<String> serializer = getDefaultEntityReferenceSerializer();
String serializedCurrentDocumentName = serializer.serialize(context.getDoc().getDocumentReference());
// Add it to the list.
extensions.add(serializedCurrentDocumentName);
}
for (String documentName : extensions) {
result.append(getLink(documentName, context));
}
return result.toString();
}
/**
* Get the parameters for a pulled resource. Note that a valid map is always returned, even if no parameters were
* given when the resource was pulled.
*
* @param resource The resource for which to retrieve the parameters.
* @param context The current request context.
* @return The parameters for the resource, as a map where the keys are the parameter names, and the values are
* corresponding parameter value. If no parameters were given, an empty map is returned.
*/
protected Map<String, Object> getParametersForResource(String resource, XWikiContext context)
{
Map<String, Object> result = getParametersMap(context).get(resource);
if (result == null) {
result = Collections.emptyMap();
}
return result;
}
/**
* Get a parameter value for a pulled resource.
*
* @param parameterName the name of the parameter to retrieve
* @param resource the resource for which to retrieve the parameter
* @param context the current request context
* @return The parameter value for the resource. If this parameter was not given, {@code null} is returned.
*/
protected Object getParameter(String parameterName, String resource, XWikiContext context)
{
return getParametersForResource(resource, context).get(parameterName);
}
/**
* This method converts the parameters for an extension to a query string that can be used with
* {@link com.xpn.xwiki.doc.XWikiDocument#getURL(String, String, String, XWikiContext) getURL()} and printed in the
* XHTML result. The parameters separator is the escaped &. The query string already starts with an
* & if at least one parameter exists.
*
* @param resource The pulled resource whose parameters should be converted.
* @param context The current request context.
* @return The constructed query string, or an empty string if there are no parameters.
*/
protected String parametersAsQueryString(String resource, XWikiContext context)
{
Map<String, Object> parameters = getParametersForResource(resource, context);
StringBuilder query = new StringBuilder();
for (Entry<String, Object> parameter : parameters.entrySet()) {
// Skip the parameter that forces the file extensions to be sent through the /skin/ action
if ("forceSkinAction".equals(parameter.getKey())) {
continue;
}
query.append("&");
query.append(sanitize(parameter.getKey()));
query.append("=");
query.append(sanitize(parameter.getValue().toString()));
}
// If the main page is requested unminified, also send unminified extensions
if ("false".equals(context.getRequest().getParameter("minify"))) {
query.append("&minify=false");
}
return query.toString();
}
/**
* Prevent "HTML Injection" by making sure the rendered text does not escape the current element. This is achieved
* by URL-encoding the following characters: '"<>
*
* @param value The string to sanitize.
* @return The unchanged string, if it does not contain special characters, or the empty string.
*/
protected String sanitize(String value)
{
String result = value;
try {
result = URLEncoder.encode(value, "UTF-8");
} catch (UnsupportedEncodingException ex) {
// Should never happen since the UTF-8 encoding is always available in the platform,
// see http://java.sun.com/j2se/1.5.0/docs/api/java/nio/charset/Charset.html
}
return result;
}
/**
* {@inheritDoc}
* <p>
* At the end of the request, insert the links to the pulled resources in the response, in the place marked by an
* XML comment of the format <tt><!-- canonical.plugin.classname --></tt>.
* </p>
*
* @see com.xpn.xwiki.plugin.XWikiDefaultPlugin#endParsing(String, XWikiContext)
*/
@Override
public String endParsing(String content, XWikiContext context)
{
// Using an XML comment is pretty safe, as extensions probably wouldn't work in other type
// of documents, like RTF, CSV or JSON.
String hook = "<!-- " + this.getClass().getCanonicalName() + " -->";
String result = content.replaceFirst(hook, getImportString(context));
return result;
}
@Override
public UsedExtension getCacheResources(XWikiContext context) {
return new CachedItem.UsedExtension(getPulledResources(context),
new HashMap<String, Map<String, Object>>(getParametersMap(context)));
}
@Override
public void restoreCacheResources(XWikiContext context,
UsedExtension extension) {
getPulledResources(context).addAll(extension.resources);
getParametersMap(context).putAll(extension.parameters);
}
/**
* Used to convert a proper Document Reference to string (standard form).
*/
protected EntityReferenceSerializer<String> getDefaultEntityReferenceSerializer()
{
if (this.defaultEntityReferenceSerializer == null) {
this.defaultEntityReferenceSerializer = Utils.getComponent(EntityReferenceSerializer.TYPE_STRING);
}
return this.defaultEntityReferenceSerializer;
}
/**
* Used to resolve a document string reference to a Document Reference.
*/
protected DocumentReferenceResolver<String> getCurrentDocumentReferenceResolver()
{
if (this.currentDocumentReferenceResolver == null) {
this.currentDocumentReferenceResolver =
Utils.getComponent(DocumentReferenceResolver.TYPE_STRING, "current");
}
return this.currentDocumentReferenceResolver;
}
}