/*
* 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 org.xwiki.wiki.script;
import java.util.ArrayList;
import java.util.Collection;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.xwiki.component.annotation.Component;
import org.xwiki.context.Execution;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.DocumentReferenceResolver;
import org.xwiki.model.reference.EntityReferenceSerializer;
import org.xwiki.model.reference.WikiReference;
import org.xwiki.script.service.ScriptService;
import org.xwiki.script.service.ScriptServiceManager;
import org.xwiki.security.authorization.AccessDeniedException;
import org.xwiki.security.authorization.AuthorizationException;
import org.xwiki.security.authorization.AuthorizationManager;
import org.xwiki.security.authorization.Right;
import org.xwiki.url.internal.standard.StandardURLConfiguration;
import org.xwiki.wiki.configuration.WikiConfiguration;
import org.xwiki.wiki.descriptor.WikiDescriptor;
import org.xwiki.wiki.descriptor.WikiDescriptorManager;
import org.xwiki.wiki.internal.descriptor.document.WikiDescriptorDocumentHelper;
import org.xwiki.wiki.manager.WikiManager;
import org.xwiki.wiki.manager.WikiManagerException;
import com.xpn.xwiki.XWikiContext;
/**
* Script service to manage wikis.
*
* @version $Id: 8284b6e60c5225954b1e4db90a6684f118a46f99 $
* @since 5.3M2
*/
@Component
@Named(WikiManagerScriptService.ROLEHINT)
@Singleton
public class WikiManagerScriptService implements ScriptService
{
/**
* Hint of the component.
*/
public static final String ROLEHINT = "wiki";
/**
* Field name of the last API exception inserted in context.
*/
@Deprecated
public static final String CONTEXT_LASTEXCEPTION = "lastexception";
/**
* The key under which the last encountered error is stored in the current execution context.
*/
private static final String WIKIERROR_KEY = "scriptservice.wiki.error";
@Inject
private WikiManager wikiManager;
@Inject
private WikiDescriptorManager wikiDescriptorManager;
@Inject
private Provider<XWikiContext> xcontextProvider;
/**
* Execution context.
*/
@Inject
private Execution execution;
@Inject
private AuthorizationManager authorizationManager;
@Inject
private DocumentReferenceResolver<String> documentReferenceResolver;
@Inject
private EntityReferenceSerializer<String> entityReferenceSerializer;
@Inject
private ScriptServiceManager scriptServiceManager;
@Inject
private StandardURLConfiguration standardURLConfiguration;
@Inject
private WikiConfiguration wikiConfiguration;
@Inject
private WikiDescriptorDocumentHelper wikiDescriptorDocumentHelper;
/**
* Logging tool.
*/
@Inject
private Logger logger;
/**
* Get a sub script service related to wiki. (Note that we're voluntarily using an API name of "get" to make it
* extra easy to access Script Services from Velocity (since in Velocity writing <code>$services.wiki.name</code> is
* equivalent to writing <code>$services.wiki.get("name")</code>). It also makes it a short and easy API name for
* other scripting languages.
*
* @param serviceName id of the script service
* @return the service asked or null if none could be found
*/
public ScriptService get(String serviceName)
{
return scriptServiceManager.get(ROLEHINT + '.' + serviceName);
}
/**
* Get the error generated while performing the previously called action.
*
* @return an eventual exception or {@code null} if no exception was thrown
*/
public Exception getLastError()
{
return (Exception) this.execution.getContext().getProperty(WIKIERROR_KEY);
}
/**
* Store a caught exception in the context, so that it can be later retrieved using {@link #getLastError()}.
*
* @param e the exception to store, can be {@code null} to clear the previously stored exception
* @see #getLastError()
*/
private void setLastError(Exception e)
{
this.execution.getContext().setProperty(WIKIERROR_KEY, e);
}
// TODO: move to new API a soon as a proper helper is provided
private void checkProgrammingRights() throws AuthorizationException
{
XWikiContext xcontext = this.xcontextProvider.get();
authorizationManager.checkAccess(Right.PROGRAM, xcontext.getDoc().getAuthorReference(), xcontext.getDoc()
.getDocumentReference());
}
/**
* Create a new wiki.
*
* @param wikiId unique identifier of the new wiki
* @param wikiAlias default alias of the new wiki
* @param ownerId Id of the user that will own the wiki
* @param failOnExist Fail the operation if the wiki id already exists
* @return the wiki descriptor of the new wiki, or null if problems occur
*/
public WikiDescriptor createWiki(String wikiId, String wikiAlias, String ownerId, boolean failOnExist)
{
WikiDescriptor descriptor = null;
XWikiContext context = xcontextProvider.get();
try {
// Check if the current script has the programing rights
checkProgrammingRights();
// Check right access
WikiReference mainWikiReference = new WikiReference(getMainWikiId());
authorizationManager.checkAccess(Right.CREATE_WIKI, context.getUserReference(), mainWikiReference);
if (!failOnExist) {
authorizationManager.checkAccess(Right.PROGRAM, context.getUserReference(), mainWikiReference);
}
// Create the wiki
descriptor = wikiManager.create(wikiId, wikiAlias, failOnExist);
// Set the owner
descriptor.setOwnerId(ownerId);
wikiDescriptorManager.saveDescriptor(descriptor);
} catch (Exception e) {
error(e);
}
return descriptor;
}
/**
* Delete the specified wiki.
*
* @param wikiId unique identifier of the wiki to delete
* @return true if the wiki has been successfully deleted
*/
public boolean deleteWiki(String wikiId)
{
// Test if the script has the programming right
XWikiContext context = xcontextProvider.get();
try {
// Check if the current script has the programming rights
checkProgrammingRights();
// Test right
if (!canDeleteWiki(entityReferenceSerializer.serialize(context.getUserReference()), wikiId)) {
throw new AuthorizationException("You don't have the right to delete the wiki");
}
// Delete the wiki
wikiManager.delete(wikiId);
// Return success
return true;
} catch (Exception e) {
error(String.format("Failed to delete wiki [%s]", wikiId), e);
}
return false;
}
/**
* Test if a given user can delete a given wiki.
*
* @param userId the id of the user to test
* @param wikiId the id of the wiki
* @return whether or not the user can delete the specified wiki
*/
public boolean canDeleteWiki(String userId, String wikiId)
{
try {
// Get target wiki descriptor
WikiDescriptor descriptor = wikiDescriptorManager.getById(wikiId);
if (descriptor == null) {
error(new Exception(String.format("Could not find descriptor for wiki [%s]]", wikiId)));
return false;
}
// Get the full reference of the given user
DocumentReference userReference = documentReferenceResolver.resolve(userId);
String fullUserId = entityReferenceSerializer.serialize(userReference);
// If the user is the owner
String owner = descriptor.getOwnerId();
if (fullUserId.equals(owner)) {
return true;
}
// If the user is an admin
WikiReference wikiReference = new WikiReference(wikiId);
if (authorizationManager.hasAccess(Right.ADMIN, userReference, wikiReference)) {
return true;
}
} catch (WikiManagerException e) {
error(String.format("Error while getting the descriptor of wiki [%s]", wikiId), e);
}
return false;
}
/**
* Get a wiki descriptor from one of its alias.
*
* @param wikiAlias alias to search
* @return the wiki descriptor corresponding to the alias, or null if no descriptors match the alias
*/
public WikiDescriptor getByAlias(String wikiAlias)
{
WikiDescriptor descriptor = null;
try {
descriptor = wikiDescriptorManager.getByAlias(wikiAlias);
} catch (WikiManagerException e) {
error(e);
}
return descriptor;
}
/**
* Get a wiki descriptor from its unique identifier.
*
* @param wikiId unique identifier of the wiki to search
* @return the wiki descriptor corresponding to the Id, or null if no descriptors match the id
*/
public WikiDescriptor getById(String wikiId)
{
WikiDescriptor descriptor = null;
try {
descriptor = wikiDescriptorManager.getById(wikiId);
} catch (WikiManagerException e) {
error(e);
}
return descriptor;
}
/**
* Get all the wiki descriptors.
*
* @return the list of all wiki descriptors
*/
public Collection<WikiDescriptor> getAll()
{
Collection<WikiDescriptor> wikis;
try {
wikis = wikiDescriptorManager.getAll();
} catch (WikiManagerException e) {
error(e);
wikis = new ArrayList<WikiDescriptor>();
}
return wikis;
}
/**
* Get all the wiki identifiers.
*
* @return the list of all wiki identifiers
* @since 6.2M1
*/
public Collection<String> getAllIds()
{
Collection<String> wikis;
try {
wikis = wikiDescriptorManager.getAllIds();
} catch (WikiManagerException e) {
error(e);
wikis = new ArrayList<String>();
}
return wikis;
}
/**
* Test if a wiki exists.
*
* @param wikiId unique identifier to test
* @return true if a wiki with this Id exists on the system or null if some error occurs.
*/
public Boolean exists(String wikiId)
{
try {
return wikiDescriptorManager.exists(wikiId);
} catch (WikiManagerException e) {
error(e);
return null;
}
}
/**
* Check if the wikiId is valid and available (the name is not already taken for technical reasons).
*
* @param wikiId the Id to test
* @return true if the Id is valid and available or null if some error occurs
*/
public Boolean idAvailable(String wikiId)
{
try {
return wikiManager.idAvailable(wikiId);
} catch (WikiManagerException e) {
error(e);
return null;
}
}
/**
* @return the descriptor of the main wiki or null if problems occur
*/
public WikiDescriptor getMainWikiDescriptor()
{
WikiDescriptor descriptor = null;
try {
descriptor = wikiDescriptorManager.getMainWikiDescriptor();
} catch (WikiManagerException e) {
error(e);
}
return descriptor;
}
/**
* @return the Id of the main wiki
*/
public String getMainWikiId()
{
return wikiDescriptorManager.getMainWikiId();
}
/**
* @return the id of the current wiki
*/
public String getCurrentWikiId()
{
return wikiDescriptorManager.getCurrentWikiId();
}
/**
* @return the descriptor of the current wiki
*/
public WikiDescriptor getCurrentWikiDescriptor()
{
WikiDescriptor descriptor = null;
try {
descriptor = wikiDescriptorManager.getCurrentWikiDescriptor();
} catch (WikiManagerException e) {
error(e);
}
return descriptor;
}
/**
* Save the specified descriptor (if you have the right).
*
* @param descriptor descriptor to save
* @return true if it succeed
*/
public boolean saveDescriptor(WikiDescriptor descriptor)
{
XWikiContext context = xcontextProvider.get();
boolean isAllowed;
try {
// Get the wiki owner
WikiDescriptor oldDescriptor = wikiDescriptorManager.getById(descriptor.getId());
WikiReference wikiReference = descriptor.getReference();
if (oldDescriptor != null) {
// Users that can edit the wiki's descriptor document are allowed to use this API as well. This
// includes global admins.
DocumentReference descriptorDocument =
wikiDescriptorDocumentHelper.getDocumentReferenceFromId(oldDescriptor.getId());
isAllowed = authorizationManager.hasAccess(Right.EDIT, context.getUserReference(), descriptorDocument);
String currentOwner = oldDescriptor.getOwnerId();
if (!isAllowed) {
// The current owner can edit anything.
isAllowed = entityReferenceSerializer.serialize(context.getUserReference()).equals(currentOwner);
}
if (!isAllowed) {
// Local admins can edit the descriptor, except for the "ownerId" field, which should be
// editable only by the current owner or main wiki admins.
String newOwner = descriptor.getOwnerId();
isAllowed =
authorizationManager.hasAccess(Right.ADMIN, context.getUserReference(), wikiReference)
&& StringUtils.equals(newOwner, currentOwner);
}
} else {
// Saving a descriptor that did not already exist should be reserved to global admins
isAllowed =
authorizationManager.hasAccess(Right.ADMIN, context.getUserReference(), new WikiReference(
wikiDescriptorManager.getMainWikiId()));
}
if (!isAllowed) {
// Exhausted all options. Deny access for the current user to edit the descriptor.
throw new AccessDeniedException(context.getUserReference(), wikiReference);
} else {
// Execute the operation.
wikiDescriptorManager.saveDescriptor(descriptor);
}
return true;
} catch (Exception e) {
error(e);
return false;
}
}
/**
* Tell if the path mode is used for subwikis.
* <p>
* Example:
*
* <pre>
* {@code
* wiki alias: subwiki
* URL if path mode is enabled:
* /xwiki/wiki/subwiki/
* URL if path mode is disabled:
* http://subwiki/
* }
* </pre>
*
* @return either or not the path mode is enabled
*/
public boolean isPathMode()
{
return standardURLConfiguration.isPathBasedMultiWiki();
}
/**
* @return the default suffix to append to new wiki aliases.
*/
public String getAliasSuffix()
{
return wikiConfiguration.getAliasSuffix();
}
/**
* Log exception and store it in the context.
*
* @param e the caught exception
*/
private void error(Exception e)
{
error(null, e);
}
/**
* Log exception and store it in the context.
*
* @param errorMessage error message
* @param e the caught exception
*/
private void error(String errorMessage, Exception e)
{
String errorMessageToLog = errorMessage;
if (errorMessageToLog == null) {
errorMessageToLog = e.getMessage();
}
// Log exception.
logger.error(errorMessageToLog, e);
// Store exception in context.
setLastError(e);
// Deprecated
this.execution.getContext().setProperty(CONTEXT_LASTEXCEPTION, e);
}
/**
* @return the last exception, or null if there is not
* @deprecated since 5.4RC1 use {@link #getLastError()} ()} instead
*/
@Deprecated
public Exception getLastException()
{
return (Exception) this.execution.getContext().getProperty(CONTEXT_LASTEXCEPTION);
}
}