/*
* 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;
}
}