/* * 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.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xwiki.bridge.event.DocumentCreatedEvent; import org.xwiki.bridge.event.DocumentDeletedEvent; import org.xwiki.bridge.event.DocumentUpdatedEvent; import org.xwiki.bridge.event.WikiDeletedEvent; import org.xwiki.model.EntityType; import org.xwiki.model.reference.DocumentReference; import org.xwiki.model.reference.EntityReferenceResolver; import org.xwiki.model.reference.EntityReferenceSerializer; import org.xwiki.observation.EventListener; import org.xwiki.observation.ObservationManager; import org.xwiki.observation.event.Event; import org.xwiki.security.authorization.AuthorizationManager; import org.xwiki.security.authorization.ContextualAuthorizationManager; import org.xwiki.security.authorization.Right; import com.xpn.xwiki.XWikiContext; import com.xpn.xwiki.XWikiException; import com.xpn.xwiki.doc.XWikiDocument; import com.xpn.xwiki.objects.BaseObject; import com.xpn.xwiki.objects.classes.BaseClass; import com.xpn.xwiki.web.Utils; /** * Abstract SX plugin for wiki-document-based extensions (Extensions written as object of a XWiki Extension class). * Provides a generic method to initialize the XWiki class upon plugin initialization if needed. Provide a notification * mechanism for extensions marked as "use-always". * * @version $Id: fe38e7c2bb8f22e475a36b1002c823cee4f3e07f $ * @since 1.4 * @see JsSkinExtensionPlugin * @see CssSkinExtensionPlugin */ public abstract class AbstractDocumentSkinExtensionPlugin extends AbstractSkinExtensionPlugin implements EventListener { /** * Log helper for logging messages in this class. */ protected static final Logger LOGGER = LoggerFactory.getLogger(AbstractDocumentSkinExtensionPlugin.class); /** * The name of the field that indicates whether an extension should always be used, or only when explicitly pulled. */ private static final String USE_FIELDNAME = "use"; /** * A Map with wiki/database name as keys and sets of extensions to use always for this wiki as values. */ private Map<String, Set<DocumentReference>> alwaysUsedExtensions; /** * Used to match events on "use" property. */ private final List<Event> events = new ArrayList<Event>(3); /** * 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 AbstractDocumentSkinExtensionPlugin(String name, String className, XWikiContext context) { super(name, className, context); this.events.add(new DocumentCreatedEvent()); this.events.add(new DocumentDeletedEvent()); this.events.add(new DocumentUpdatedEvent()); this.events.add(new WikiDeletedEvent()); } @Override public List<Event> getEvents() { return this.events; } /** * The name of the XClass which holds extensions of this type. * * @return A <code>String</code> representation of the XClass name, in the <code>Space.Document</code> format. */ protected abstract String getExtensionClassName(); /** * A user-friendly name for this type of resource, used in the auto-generated class document. * * @return The user-friendly name for this type of resource. */ protected abstract String getExtensionName(); /** * {@inheritDoc} * <p> * Create/update the XClass corresponding to this kind of extension, and register the listeners that update the list * of always used extensions. * </p> * * @see com.xpn.xwiki.plugin.XWikiDefaultPlugin#init(com.xpn.xwiki.XWikiContext) */ @Override public void init(XWikiContext context) { super.init(context); this.alwaysUsedExtensions = new HashMap<String, Set<DocumentReference>>(); getExtensionClass(context); Utils.getComponent(ObservationManager.class).addListener(this); } /** * {@inheritDoc} * <p> * Create/update the XClass corresponding to this kind of extension in this virtual wiki. * </p> * * @see com.xpn.xwiki.plugin.XWikiDefaultPlugin#virtualInit(com.xpn.xwiki.XWikiContext) */ @Override public void virtualInit(XWikiContext context) { super.virtualInit(context); getExtensionClass(context); } /** * {@inheritDoc} * <p> * For this kind of resources, an XObject property (<tt>use</tt>) with the value <tt>always</tt> indicates always * used extensions. The list of extensions for each wiki is lazily placed in a cache: if the extension set for the * context wiki is null, then they will be looked up in the database and added to it. The cache is invalidated using * the notification mechanism. * </p> * * @see AbstractSkinExtensionPlugin#getAlwaysUsedExtensions(XWikiContext) */ @Override public Set<String> getAlwaysUsedExtensions(XWikiContext context) { EntityReferenceSerializer<String> serializer = Utils.getComponent(EntityReferenceSerializer.TYPE_STRING); Set<DocumentReference> references = getAlwaysUsedExtensions(); Set<String> names = new HashSet<String>(references.size()); for (DocumentReference reference : references) { names.add(serializer.serialize(reference)); } return names; } /** * Returns the list of always used extensions of this type as a set of document references. For this kind of * resources, an XObject property (<tt>use</tt>) with the value <tt>always</tt> indicates always used extensions. * The list of extensions for each wiki is lazily placed in a cache: if the extension set for the context wiki is * null, then they will be looked up in the database and added to it. The cache is invalidated using the * notification mechanism. 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. * * @return a set of document references that should be pulled in the current response */ public Set<DocumentReference> getAlwaysUsedExtensions() { XWikiContext context = Utils.getContext(); // Retrieve the current wiki name from the XWiki context String currentWiki = StringUtils.defaultIfEmpty(context.getWikiId(), context.getMainXWiki()); // If we already have extensions defined for this wiki, we return them if (this.alwaysUsedExtensions.get(currentWiki) != null) { return this.alwaysUsedExtensions.get(currentWiki); } else { // Otherwise, we look them up in the database. Set<DocumentReference> extensions = new HashSet<DocumentReference>(); String query = ", BaseObject as obj, StringProperty as use where obj.className='" + getExtensionClassName() + "'" + " and obj.name=doc.fullName and use.id.id=obj.id and use.id.name='use' and use.value='always'"; try { for (DocumentReference extension : context.getWiki().getStore() .searchDocumentReferences(query, context)) { try { XWikiDocument doc = context.getWiki().getDocument(extension, context); // Only add the extension as being "always used" if the page holding it has been saved with // programming rights. if (Utils.getComponent(AuthorizationManager.class).hasAccess(Right.PROGRAM, doc.getContentAuthorReference(), doc.getDocumentReference())) { extensions.add(extension); } } catch (XWikiException e1) { LOGGER.error("Error while adding skin extension [{}] as always used. It will be ignored.", extension, e1); } } this.alwaysUsedExtensions.put(currentWiki, extensions); return extensions; } catch (XWikiException e) { LOGGER.error("Error while retrieving always used JS extensions", e); return Collections.emptySet(); } } } @Override public boolean hasPageExtensions(XWikiContext context) { XWikiDocument doc = context.getDoc(); List<BaseObject> objects = doc.getObjects(getExtensionClassName()); if (objects != null) { for (BaseObject obj : objects) { if (obj == null) { continue; } if (obj.getStringValue(USE_FIELDNAME).equals("currentPage")) { return true; } } } return false; } @Override public void use(String resource, XWikiContext context) { String canonicalResource = getCanonicalDocumentName(resource); LOGGER.debug("Using [{}] as [{}] extension", canonicalResource, this.getName()); getPulledResources(context).add(canonicalResource); // In case a previous call added some parameters, remove them, since the last call for a resource always // discards previous ones. getParametersMap(context).remove(canonicalResource); } @Override public void use(String resource, Map<String, Object> parameters, XWikiContext context) { String canonicalResource = getCanonicalDocumentName(resource); getPulledResources(context).add(canonicalResource); getParametersMap(context).put(canonicalResource, parameters); } /** * {@inheritDoc} * <p> * We must override this method since the plugin manager only calls it for classes that provide their own * implementation, and not an inherited one. * </p> * * @see AbstractSkinExtensionPlugin#endParsing(String, XWikiContext) */ @Override public String endParsing(String content, XWikiContext context) { return super.endParsing(content, context); } /** * Creates or updates the XClass used for this type of extension. Usually called on {@link #init(XWikiContext)} and * {@link #virtualInit(XWikiContext)}. * * @param context The current request context, which gives access to the wiki. * @return The XClass for this extension. */ public BaseClass getExtensionClass(XWikiContext context) { try { XWikiDocument doc = context.getWiki().getDocument(getExtensionClassName(), context); return doc.getXClass(); } catch (Exception ex) { LOGGER.error("Cannot get skin extension class [{}]", getExtensionClassName(), ex); } return null; } /** * {@inheritDoc} * <p> * Make sure to keep the {@link #alwaysUsedExtensions} map consistent when the database changes. * * @see org.xwiki.observation.EventListener#onEvent(org.xwiki.observation.event.Event, java.lang.Object, * java.lang.Object) */ @Override public void onEvent(Event event, Object source, Object data) { if (event instanceof WikiDeletedEvent) { this.alwaysUsedExtensions.remove(((WikiDeletedEvent) event).getWikiId()); } else { onDocumentEvent((XWikiDocument) source, (XWikiContext) data); } } /** * A document related event has been received. * * @param document the modified document * @param context the XWiki context */ private void onDocumentEvent(XWikiDocument document, XWikiContext context) { boolean remove = false; if (document.getObject(getExtensionClassName()) != null) { // new or already existing object if (document.getObject(getExtensionClassName(), USE_FIELDNAME, "always", false) != null) { if (Utils.getComponent(AuthorizationManager.class).hasAccess(Right.PROGRAM, document.getContentAuthorReference(), document.getDocumentReference())) { getAlwaysUsedExtensions().add(document.getDocumentReference()); return; } else { // in case the extension lost its programming rights upon this save. remove = true; } } else { // remove if exists but use onDemand remove = true; } } else if (document.getOriginalDocument().getObject(getExtensionClassName()) != null) { // object removed remove = true; } if (remove) { getAlwaysUsedExtensions().remove(document.getDocumentReference()); } } /** * Get the canonical serialization of a document name, in the {@code wiki:Space.Document} format. * * @param documentName the original document name to fix * @return fixed document name */ private String getCanonicalDocumentName(String documentName) { @SuppressWarnings("unchecked") EntityReferenceResolver<String> resolver = Utils.getComponent(EntityReferenceResolver.TYPE_STRING, "current"); @SuppressWarnings("unchecked") EntityReferenceSerializer<String> serializer = Utils.getComponent(EntityReferenceSerializer.TYPE_STRING); return serializer.serialize(resolver.resolve(documentName, EntityType.DOCUMENT)); } /** * @param documentName the Skin Extension's document name * @param context the XWiki Context * @return true if the specified document is accessible (i.e. has view rights) by the current user; false otherwise */ protected boolean isAccessible(String documentName, XWikiContext context) { return isAccessible(getCurrentDocumentReferenceResolver().resolve(documentName), context); } /** * @param documentReference the Skin Extension's document reference * @param context the XWiki Context * @return true if the specified document is accessible (i.e. has view rights) by the current user; false otherwise * @since 7.4.1 */ protected boolean isAccessible(DocumentReference documentReference, XWikiContext context) { if (!Utils.getComponent(ContextualAuthorizationManager.class).hasAccess(Right.VIEW, documentReference)) { LOGGER.debug("[{}] The current user [{}] does not have 'view' rights on the Skin Extension document [{}]", getName(), context.getUserReference(), documentReference); return false; } return true; } /** * @param documentReference the Skin Extension's document reference * @param context the XWiki Context * @return the version of the document */ private String getDocumentVersion(DocumentReference documentReference, XWikiContext context) { try { return context.getWiki().getDocument(documentReference, context).getVersion(); } catch (XWikiException e) { LOGGER.error("Failed to load document [{}].", documentReference, e); } return ""; } /** * Return the query string part with the version of the document, to add to the URL of a resource. The objective is * to generate an URL specific to this version to avoid browsers using an outdated version from their cache. * * @param documentReference the Skin Extension's document reference * @param context the XWiki Context * @return the query string part handling the version of the document */ private String getDocumentVersionQueryString(DocumentReference documentReference, XWikiContext context) { return "docVersion=" + sanitize(getDocumentVersion(documentReference, context)); } /** * Return the query string part with the language of the document (if any). * * @param context the XWiki Context * @return the query string handling the language of the document */ private String getLanguageQueryString(XWikiContext context) { Locale locale = context.getLocale(); if (locale != null) { return "language=" + sanitize(locale.toString()); } return ""; } /** * Return the URL to a document skin extension. * * @param documentReference the Skin Extension's document reference * @param documentName the Skin Extension's document name * @param pluginName the name of the plugin * @param context the XWiki Context * @return the URL to the document skin extension. * * @since 7.4.1 */ protected String getDocumentSkinExtensionURL(DocumentReference documentReference, String documentName, String pluginName, XWikiContext context) { String queryString = String.format("%s&%s%s", getLanguageQueryString(context), getDocumentVersionQueryString(documentReference, context), parametersAsQueryString(documentName, context)); return context.getWiki().getURL(documentReference, pluginName, queryString, "", context); } }