/* * 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.web; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; import java.util.Map; import javax.script.ScriptContext; import org.apache.commons.lang3.StringUtils; import org.apache.velocity.VelocityContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xwiki.model.EntityType; import org.xwiki.model.reference.DocumentReference; import org.xwiki.model.reference.DocumentReferenceResolver; import org.xwiki.model.reference.EntityReference; import org.xwiki.model.reference.EntityReferenceResolver; import org.xwiki.model.reference.EntityReferenceSerializer; import org.xwiki.model.reference.SpaceReference; import org.xwiki.query.Query; import org.xwiki.query.QueryManager; import org.xwiki.script.ScriptContextManager; import org.xwiki.velocity.VelocityManager; import com.xpn.xwiki.XWiki; import com.xpn.xwiki.XWikiContext; import com.xpn.xwiki.XWikiException; import com.xpn.xwiki.api.Document; import com.xpn.xwiki.doc.XWikiDocument; import com.xpn.xwiki.objects.BaseObject; /** * Helper class used to handle one individual create action request. * * @version $Id: 476990d74ba9cc793ae3e4dfa8fbe09ab96cbe1a $ */ public class CreateActionRequestHandler { /** * Log used to report exceptions. */ private static final Logger LOGGER = LoggerFactory.getLogger(CreateActionRequestHandler.class); /** * The name of the space reference parameter. */ private static final String SPACE_REFERENCE = "spaceReference"; /** * The name parameter. */ private static final String NAME = "name"; /** * The name of the deprecated space parameter. <br> * Note: if you change the value of this variable, change the value of {{@link #TOCREATE_SPACE} to the previous * value. * * @deprecated Use {@value #SPACE_REFERENCE} as parameter name instead. */ @Deprecated private static final String SPACE = "space"; /** * The name of the page parameter. * * @deprecated Use {@value #NAME} as parameter name instead. */ @Deprecated private static final String PAGE = "page"; /** * The value of the tocreate parameter when a space is to be created. <br> * TODO: find a way to give this constant the same value as the constant above without violating checkstyle. */ private static final String TOCREATE_SPACE = SPACE; /** * The name of the "type" parameter. */ private static final String TYPE = "type"; /** * The value of the tocreate parameter when a terminal/regular document is to be created. */ private static final String TOCREATE_TERMINAL = "terminal"; /** * The value of the tocreate parameter when a non-terminal document is to be created. */ private static final String TOCREATE_NONTERMINAL = "nonterminal"; /** * The name of the template field inside the template provider, or the template parameter which can be sent * directly, without passing through the template provider. */ private static final String TEMPLATE = "template"; /** * The name of the template provider parameter. */ private static final String TEMPLATE_PROVIDER = "templateprovider"; /** * The template provider class, to create documents from templates. */ private static final EntityReference TEMPLATE_PROVIDER_CLASS = new EntityReference("TemplateProviderClass", EntityType.DOCUMENT, new EntityReference(XWiki.SYSTEM_SPACE, EntityType.SPACE)); /** * The redirect class, used to mark pages that are redirect place-holders, i.e. hidden pages that serve only for * redirecting the user to a different page (e.g. when a page has been moved). */ private static final EntityReference REDIRECT_CLASS = new EntityReference("RedirectClass", EntityType.DOCUMENT, new EntityReference(XWiki.SYSTEM_SPACE, EntityType.SPACE)); /** * The property name for the spaces in the template provider object. * * @deprecated since 8.3M2. Use {@link #TP_CREATION_RESTRICTIONS_PROPERTY} or * {@value #TP_VISIBILITY_RESTRICTIONS_PROPERTY} instead for the explicit restriction you need to add. */ @Deprecated private static final String SPACES_PROPERTY = "spaces"; /** * The key used to add exceptions on the context, to be read by the template. */ private static final String EXCEPTION = "createException"; /** * Current entity reference resolver hint. */ private static final String CURRENT_RESOLVER_HINT = "current"; /** * Current entity reference resolver hint. */ private static final String CURRENT_MIXED_RESOLVER_HINT = "currentmixed"; /** * Local entity reference serializer hint. */ private static final String LOCAL_SERIALIZER_HINT = "local"; private static final String TP_TERMINAL_PROPERTY = TOCREATE_TERMINAL; private static final String TP_TYPE_PROPERTY = TYPE; private static final String TP_TYPE_PROPERTY_SPACE_VALUE = SPACE; private static final String TP_CREATION_RESTRICTIONS_PROPERTY = "creationRestrictions"; private static final String TP_VISIBILITY_RESTRICTIONS_PROPERTY = "visibilityRestrictions"; private static final String TP_CREATION_RESTRICTIONS_ARE_SUGGESTIONS_PROPERTY = "creationRestrictionsAreSuggestions"; /** * Space homepage document name. */ private static final String WEBHOME = "WebHome"; private ScriptContextManager scriptContextManager; private SpaceReference spaceReference; private String name; private boolean isSpace; private XWikiContext context; private XWikiDocument document; private XWikiRequest request; private BaseObject templateProvider; private List<Document> availableTemplateProviders; private String type; /** * @param context the XWikiContext for which to handle the request. */ public CreateActionRequestHandler(XWikiContext context) { this.context = context; this.document = context.getDoc(); this.request = context.getRequest(); } /** * Process the request and extract from the given parameters the data needed to create the new document. * * @throws XWikiException if problems occur */ public void processRequest() throws XWikiException { // Get the template provider for creating this document, if any template provider is specified DocumentReferenceResolver<EntityReference> referenceResolver = Utils.getComponent(DocumentReferenceResolver.TYPE_REFERENCE, CURRENT_RESOLVER_HINT); DocumentReference templateProviderClassReference = referenceResolver.resolve(TEMPLATE_PROVIDER_CLASS); templateProvider = getTemplateProvider(templateProviderClassReference); // Get the available templates, in the current space, to check if all conditions to create a new document are // met availableTemplateProviders = loadAvailableTemplateProviders(document.getDocumentReference().getLastSpaceReference(), templateProviderClassReference, context); // Get the type of document to create type = request.get(TYPE); // Since this template can be used for creating a Page or a Space, check the passed "tocreate" parameter // which can be either "page" or "space". If no parameter is passed then we default to creating a Page. String toCreate = request.getParameter("tocreate"); if (document.isNew()) { processNewDocument(toCreate); } else { // We are on an existing document... if (request.getParameter(SPACE) != null || request.getParameter(PAGE) != null) { // We are in Backwards Compatibility mode and we are using the deprecated parameter names. processDeprecatedParameters(toCreate); } else { // Determine the new document values from the request. String spaceReferenceParameter = request.getParameter(SPACE_REFERENCE); // We can have an empty spaceReference parameter symbolizing that we are creating a top level space or // non-terminal document. if (StringUtils.isNotEmpty(spaceReferenceParameter)) { EntityReferenceResolver<String> genericResolver = Utils.getComponent(EntityReferenceResolver.TYPE_STRING, CURRENT_RESOLVER_HINT); EntityReference resolvedEntityReference = genericResolver.resolve(spaceReferenceParameter, EntityType.SPACE); spaceReference = new SpaceReference(resolvedEntityReference); } // Note: We leave the spaceReference variable intentionally null to symbolize a top level space or // non-terminal document. name = request.getParameter(NAME); // Determine the type of document we are creating (terminal vs non-terminal). if (TOCREATE_TERMINAL.equals(toCreate) || TOCREATE_NONTERMINAL.equals(toCreate)) { // Look at the request to see what the user wanted to create (terminal or non-terminal). isSpace = !TOCREATE_TERMINAL.equals(toCreate); } else if (templateProvider != null) { // A template provider is specified. Use it and extract the type of document. boolean providerTerminal = getTemplateProviderTerminalValue(); isSpace = !providerTerminal; } else { // Default to creating non-terminal documents. isSpace = true; } } } } /** * @param toCreate the value of the "tocreate" request parameter */ private void processNewDocument(String toCreate) { // Current space and page name. spaceReference = document.getDocumentReference().getLastSpaceReference(); name = document.getDocumentReference().getName(); // Determine if the current document is in a top-level space. EntityReference parentSpaceReference = spaceReference.getParent(); boolean isTopLevelSpace = parentSpaceReference.extractReference(EntityType.SPACE) == null; // Remember this since we might update it below. String originalName = name; // Since WebHome is a convention, determine the real name and parent of our document. if (WEBHOME.equals(name)) { // Determine its name from the space name. name = spaceReference.getName(); // Determine its space reference by looking at the space's parent. if (!isTopLevelSpace) { // The parent reference is a space reference. Use it. spaceReference = new SpaceReference(parentSpaceReference); } else { // Top level document, i.e. the parent reference is a wiki reference. Clear the spaceReference variable // so that this case is properly handled later on (as if an empty value was passed as parameter in the // request). spaceReference = null; } } // Determine the type of document we are creating (terminal vs non-terminal). if (TOCREATE_TERMINAL.equals(toCreate) || TOCREATE_NONTERMINAL.equals(toCreate)) { // Look at the request to see what the user wanted to create (terminal or non-terminal). isSpace = !TOCREATE_TERMINAL.equals(toCreate); } else if (templateProvider != null) { // A template provider is specified. Use it and extract the type of document. boolean providerTerminal = getTemplateProviderTerminalValue(); isSpace = !providerTerminal; } else { // Last option is to check the document's original name and see if it was "WebHome". isSpace = WEBHOME.equals(originalName); } } /** * @return */ private boolean getTemplateProviderTerminalValue() { boolean providerTerminal; int providerTerminalValue = templateProvider.getIntValue(TP_TERMINAL_PROPERTY, -1); if (providerTerminalValue == -1) { // Backwards compatibility with providers that did not have the "terminal" property. We are deducing it // from the value of the "type" property. String providerType = templateProvider.getStringValue(TP_TYPE_PROPERTY); if (TP_TYPE_PROPERTY_SPACE_VALUE.equals(providerType)) { providerTerminal = false; } else { // 'page' or NULL both resolve to true, for backwards compatibility reasons. providerTerminal = true; } } else { // Use the "terminal" value from the template provider. providerTerminal = (1 == providerTerminalValue); } return providerTerminal; } /** * @param toCreate the value of the "tocreate" request parameter */ private void processDeprecatedParameters(String toCreate) { // Note: The most important details is that the deprecated "space" parameter stores unescaped space // names, not references! String spaceParameter = request.getParameter(SPACE); isSpace = TOCREATE_SPACE.equals(toCreate); if (isSpace) { // Always creating top level spaces in this mode. Adapt to the new implementation. spaceReference = null; name = spaceParameter; } else { if (StringUtils.isNotEmpty(spaceParameter)) { // Always creating documents in top level spaces in this mode. spaceReference = new SpaceReference(spaceParameter, document.getDocumentReference().getWikiReference()); } name = request.getParameter(PAGE); } } /** * @param templateProviderClass the class of the template provider object * @return the object which holds the template provider to be used for creation * @throws XWikiException in case anything goes wrong manipulating documents */ private BaseObject getTemplateProvider(DocumentReference templateProviderClass) throws XWikiException { BaseObject result = null; // resolver to use to resolve references received in request parameters DocumentReferenceResolver<String> referenceResolver = Utils.getComponent(DocumentReferenceResolver.TYPE_STRING, CURRENT_MIXED_RESOLVER_HINT); // set the template, from the template provider param String templateProviderDocReferenceString = request.getParameter(TEMPLATE_PROVIDER); if (!StringUtils.isEmpty(templateProviderDocReferenceString)) { // parse this document reference DocumentReference templateProviderRef = referenceResolver.resolve(templateProviderDocReferenceString); // get the document of the template provider and the object XWikiDocument templateProviderDoc = context.getWiki().getDocument(templateProviderRef, context); result = templateProviderDoc.getXObject(templateProviderClass); } return result; } /** * @param spaceReference the space to check if there are available templates for * @param context the context of the current request * @param templateClassReference the reference to the template provider class * @return the available template providers for the passed space, as {@link Document}s */ private List<Document> loadAvailableTemplateProviders(SpaceReference spaceReference, DocumentReference templateClassReference, XWikiContext context) { XWiki wiki = context.getWiki(); List<Document> templates = new ArrayList<Document>(); try { // resolver to use to resolve references received in request parameters DocumentReferenceResolver<String> resolver = Utils.getComponent(DocumentReferenceResolver.TYPE_STRING, CURRENT_MIXED_RESOLVER_HINT); QueryManager queryManager = Utils.getComponent((Type) QueryManager.class, "secure"); Query query = queryManager.createQuery("from doc.object(XWiki.TemplateProviderClass) as template " + "where doc.fullName not like 'XWiki.TemplateProviderTemplate' " + "order by template.name", Query.XWQL); // TODO: Extend the above query to include a filter on the type and allowed spaces properties so we can // remove the java code below, thus improving performance by not loading all the documents, but only the // documents we need. List<String> templateProviderDocNames = query.execute(); for (String templateProviderName : templateProviderDocNames) { // get the document and template provider object DocumentReference reference = resolver.resolve(templateProviderName); XWikiDocument templateDoc = wiki.getDocument(reference, context); BaseObject templateObject = templateDoc.getXObject(templateClassReference); // Check the template provider's visibility restrictions. if (isTemplateProviderAllowedInSpace(templateObject, spaceReference, TP_VISIBILITY_RESTRICTIONS_PROPERTY)) { // create a Document and put it in the list templates.add(new Document(templateDoc, context)); } } } catch (Exception e) { LOGGER.warn("There was an error getting the available templates for space {0}", spaceReference, e); } return templates; } private boolean isTemplateProviderAllowedInSpace(BaseObject templateObject, SpaceReference spaceReference, String restrictionsProperty) { // Handle the special case for creation restrictions when they are only suggestions and can be ignored. if (TP_CREATION_RESTRICTIONS_PROPERTY.equals(restrictionsProperty) && templateObject.getIntValue(TP_CREATION_RESTRICTIONS_ARE_SUGGESTIONS_PROPERTY, 0) == 1) { return true; } // Check the allowed spaces list. List<String> restrictions = getTemplateProviderRestrictions(templateObject, restrictionsProperty); if (restrictions.size() > 0) { EntityReferenceSerializer<String> localSerializer = Utils.getComponent(EntityReferenceSerializer.TYPE_STRING, LOCAL_SERIALIZER_HINT); String spaceStringReference = localSerializer.serialize(spaceReference); for (String allowedSpace : restrictions) { // Exact match or parent space (i.e. prefix) match. if (allowedSpace.equals(spaceStringReference) || StringUtils.startsWith(spaceStringReference, String.format("%s.", allowedSpace))) { return true; } } // No match, not allowed. return false; } // No creation restrictions exist, allowed by default. return true; } private List<String> getTemplateProviderRestrictions(BaseObject templateObject, String restrictionsProperty) { List<String> creationRestrictions = templateObject.getListValue(restrictionsProperty); if (creationRestrictions.size() == 0) { // Backwards compatibility for template providers created before 8.3M2, where the "spaces" property handled // both visibility and creation. creationRestrictions = templateObject.getListValue(SPACES_PROPERTY); } return creationRestrictions; } /** * @return the document reference of the new document to be created, {@code null} if a no document can be created * (because the conditions are not met) */ public DocumentReference getNewDocumentReference() { DocumentReference result = null; if (StringUtils.isEmpty(name)) { // Can`t do anything without a name. return null; } // The new values, after the processing needed for ND below, to be used when creating the document reference. SpaceReference newSpaceReference = spaceReference; String newName = name; // Special handling for old spaces or new Nested Documents. if (isSpace) { EntityReference parentSpaceReference = spaceReference; if (parentSpaceReference == null) { parentSpaceReference = context.getDoc().getDocumentReference().getWikiReference(); } // The new space's reference. newSpaceReference = new SpaceReference(name, parentSpaceReference); // The new document's name set to the new space's homepage. In Nested Documents, this leads to the new ND's // reference name. newName = WEBHOME; } // Proceed with creating the document... if (newSpaceReference == null) { // No space specified, nothing to do. This can be the case for terminal documents, since non-terminal // documents can be top-level. return null; } // Check whether there is a template parameter set, be it an empty one. If a page should be created and there is // no template parameter, it means the create action is not supposed to be executed, but only display the // available templates and let the user choose // If there's no passed template, check if there are any available templates. If none available, then the fact // that there is no template is ok. if (hasTemplate() || availableTemplateProviders.isEmpty()) { result = new DocumentReference(newName, newSpaceReference); } return result; } /** * @return if a template or a template provider have been set (it can be empty however) */ public boolean hasTemplate() { return request.getParameter(TEMPLATE_PROVIDER) != null || request.getParameter(TEMPLATE) != null; } /** * Verifies if the creation inside the specified spaceReference is allowed by the current template provider. If the * creation is not allowed, an exception will be set on the context. * * @return {@code true} if the creation is allowed, {@code false} otherwise */ public boolean isTemplateProviderAllowedToCreateInCurrentSpace() { // Check that the chosen space is allowed with the given template, if not: // - Cancel the redirect // - Set an error on the context, to be read by the create.vm if (templateProvider != null) { // Check using the template provider's creation restrictions. if (!isTemplateProviderAllowedInSpace(templateProvider, spaceReference, TP_CREATION_RESTRICTIONS_PROPERTY)) { // put an exception on the context, for create.vm to know to display an error Object[] args = {templateProvider.getStringValue(TEMPLATE), spaceReference, name}; XWikiException exception = new XWikiException(XWikiException.MODULE_XWIKI_STORE, XWikiException.ERROR_XWIKI_APP_TEMPLATE_NOT_AVAILABLE, "Template {0} cannot be used in space {1} when creating page {2}", null, args); ScriptContext scontext = getCurrentScriptContext(); scontext.setAttribute(EXCEPTION, exception, ScriptContext.ENGINE_SCOPE); scontext.setAttribute("createAllowedSpaces", getTemplateProviderRestrictions(templateProvider, TP_CREATION_RESTRICTIONS_PROPERTY), ScriptContext.ENGINE_SCOPE); return false; } } // For all other cases, creation is allowed. return true; } /** * @param newDocument the new document to check if it already exists * @return true if the document already exists (i.e. is not usable) and set an exception in the velocity context; * false otherwise. */ public boolean isDocumentAlreadyExisting(XWikiDocument newDocument) { // if the document exists don't create it, put the exception on the context so that the template gets it and // re-requests the page and space, else create the document and redirect to edit if (!isEmptyDocument(newDocument)) { ScriptContext scontext = getCurrentScriptContext(); // Expose to the template reference of the document that already exist so that it can propose to view or // edit it. scontext.setAttribute("existingDocumentReference", newDocument.getDocumentReference(), ScriptContext.ENGINE_SCOPE); // Throw an exception. Object[] args = {newDocument.getDocumentReference()}; XWikiException documentAlreadyExists = new XWikiException(XWikiException.MODULE_XWIKI_STORE, XWikiException.ERROR_XWIKI_APP_DOCUMENT_NOT_EMPTY, "Cannot create document {0} because it already has content", null, args); scontext.setAttribute(EXCEPTION, documentAlreadyExists, ScriptContext.ENGINE_SCOPE); return true; } return false; } /** * Checks if a document is empty, that is, if a document with that name could be created from a template. <br> * TODO: move this function to a more accessible place, to be used by the readFromTemplate method as well, so that * we have consistency. * * @param document the document to check * @return {@code true} if the document is empty (i.e. a document with the same name can be created (from * template)), {@code false} otherwise */ private boolean isEmptyDocument(XWikiDocument document) { // If it's a new document or a redirect placeholder, it's fine. if (document.isNew() || document.getXObject(REDIRECT_CLASS) != null) { return true; } // FIXME: the code below is not really what users might expect. Overriding an existing document (even if no // content or objects) is not really nice to do. Should be removed. // otherwise, check content and objects (only empty newline content allowed and no objects) String content = document.getContent(); if (!content.equals("\n") && !content.equals("") && !content.equals("\\\\")) { return false; } // go through all the objects and when finding the first one which is not null (because of the remove gaps), // return false, we cannot re-create this doc for (Map.Entry<DocumentReference, List<BaseObject>> objList : document.getXObjects().entrySet()) { for (BaseObject obj : objList.getValue()) { if (obj != null) { return false; } } } return true; } /** * @return the {@link VelocityContext} for the context we are handling * @deprecated since 8.3M1, use {@link #getCurrentScriptContext()} instead */ @Deprecated public VelocityContext getVelocityContext() { return Utils.getComponent(VelocityManager.class).getVelocityContext(); } /** * @return the current script context * @since 8.3M1 */ protected ScriptContext getCurrentScriptContext() { if (this.scriptContextManager == null) { this.scriptContextManager = Utils.getComponent(ScriptContextManager.class); } return this.scriptContextManager.getCurrentScriptContext(); } /** * @return the space reference where the new document will be created */ public SpaceReference getSpaceReference() { return spaceReference; } /** * @return the name of the new document. See {@link #isSpace()} */ public String getName() { return name; } /** * @return true if the new document is a space (i.e. Nested Document and the name means space name) or false if it's * a terminal regular document (i.e. Nested Spaces document and the name means document name) */ public boolean isSpace() { return isSpace; } /** * @return the available template providers for the space from where we are creating the new document */ public List<Document> getAvailableTemplateProviders() { return availableTemplateProviders; } /** * @return the currently used template provider read from the request, or {@code null} if none was set */ public BaseObject getTemplateProvider() { return templateProvider; } /** * @return the type of document to create, read from the request, or {@code null} if none was set * @since 7.2 */ public String getType() { return type; } }