blob: 50bead1c2a4b91b12d1421301e75706723289870 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sling.jcr.jackrabbit.accessmanager.post;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.security.AccessControlList;
import javax.jcr.security.AccessControlManager;
import javax.jcr.security.AccessControlPolicy;
import javax.jcr.security.AccessControlPolicyIterator;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.request.header.MediaRangeList;
import org.apache.sling.api.resource.ResourceNotFoundException;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.api.wrappers.SlingRequestPaths;
import org.apache.sling.jcr.jackrabbit.accessmanager.impl.PrincipalAceHelper;
import org.apache.sling.servlets.post.AbstractPostResponse;
import org.apache.sling.servlets.post.HtmlResponse;
import org.apache.sling.servlets.post.JSONResponse;
import org.apache.sling.servlets.post.Modification;
import org.apache.sling.servlets.post.PostResponse;
import org.apache.sling.servlets.post.PostResponseCreator;
import org.apache.sling.servlets.post.SlingPostConstants;
import org.jetbrains.annotations.Nullable;
import org.osgi.framework.Constants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Base class for all the POST servlets for the AccessManager operations
*/
public abstract class AbstractAccessPostServlet extends SlingAllMethodsServlet {
private static final long serialVersionUID = -5918670409789895333L;
/**
* default log
*/
private final transient Logger log = LoggerFactory.getLogger(getClass());
/** Sorted list of post response creator holders. */
private final List<PostResponseCreatorHolder> postResponseCreators = new ArrayList<>();
/** Cached array of post response creators used during request processing. */
private transient PostResponseCreator[] cachedPostResponseCreators = new PostResponseCreator[0];
/* (non-Javadoc)
* @see org.apache.sling.api.servlets.SlingAllMethodsServlet#doPost(org.apache.sling.api.SlingHttpServletRequest, org.apache.sling.api.SlingHttpServletResponse)
*/
@Override
protected void doPost(SlingHttpServletRequest request,
SlingHttpServletResponse httpResponse) throws ServletException,
IOException {
// prepare the response
PostResponse response = createPostResponse(request);
response.setReferer(request.getHeader("referer"));
// calculate the paths
String path = getItemPath(request);
response.setPath(path);
// location
response.setLocation(externalizePath(request, path));
// parent location
path = getParentPath(path);
if (path != null) {
response.setParentLocation(externalizePath(request, path));
}
Session session = request.getResourceResolver().adaptTo(Session.class);
final List<Modification> changes = new ArrayList<>();
try {
handleOperation(request, response, changes);
// set changes on html response
for(Modification change : changes) {
switch ( change.getType() ) {
case MODIFY : response.onModified(change.getSource()); break;
case DELETE : response.onDeleted(change.getSource()); break;
case MOVE : response.onMoved(change.getSource(), change.getDestination()); break;
case COPY : response.onCopied(change.getSource(), change.getDestination()); break;
case CREATE : response.onCreated(change.getSource()); break;
case ORDER : response.onChange("ordered", change.getSource(), change.getDestination()); break;
default:
break;
}
}
if (session.hasPendingChanges()) {
session.save();
}
} catch (ResourceNotFoundException rnfe) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND,
rnfe.getMessage());
} catch (Exception throwable) {
log.debug(String.format("Exception while handling POST %s with %s",
request.getResource().getPath(), getClass().getName()), throwable);
response.setError(throwable);
} finally {
try {
if (session.hasPendingChanges()) {
session.refresh(false);
}
} catch (RepositoryException e) {
log.warn("RepositoryException in finally block: {}",
e.getMessage(), e);
}
}
// check for redirect URL if processing succeeded
if (response.isSuccessful()) {
String redirect = null;
try {
redirect = getRedirectUrl(request, response);
} catch (IOException e) {
if (log.isDebugEnabled()) {
log.debug(String.format("Exception while handling redirect for POST %s with %s",
request.getResource().getPath(), getClass().getName()), e);
}
// http status code for 422 Unprocessable Entity
response.setStatus(422, "invalid redirect");
response.setError(e);
}
if (redirect != null) {
httpResponse.sendRedirect(redirect); // NOSONAR
return;
}
}
// create a html response and send if unsuccessful or no redirect
response.send(httpResponse, isSetStatus(request));
}
/**
* Override if the path does not need to exist
*/
protected void validateResourcePath(Session jcrSession, String resourcePath) throws RepositoryException {
if (!allowNonExistingPaths()) {
if (resourcePath == null) {
throw new ResourceNotFoundException("Resource path was not supplied.");
}
if (!jcrSession.nodeExists(resourcePath)) {
throw new ResourceNotFoundException("Resource is not a JCR Node");
}
}
}
/**
* Creates an instance of a HtmlResponse.
* @param req The request being serviced
* @return a {@link org.apache.sling.servlets.post.JSONResponse} if any of these conditions are true:
* <ul>
* <li>the response content type is application/json
* </ul>
* or a {@link org.apache.sling.servlets.post.HtmlResponse} otherwise
* @deprecated use {@link #createPostResponse(SlingHttpServletRequest)} instead
*/
@Deprecated
protected AbstractPostResponse createHtmlResponse(SlingHttpServletRequest req) {
return (AbstractPostResponse)createPostResponse(req);
}
/**
* Creates an instance of a PostResponse.
* @param req The request being serviced
* @return a {@link org.apache.sling.servlets.post.JSONResponse} if any of these conditions are true:
* <ul>
* <li> the request has an <code>Accept</code> header of <code>application/json</code></li>
* <li>the request is a JSON POST request (see SLING-1172)</li>
* <li>the request has a request parameter <code>:accept=application/json</code></li>
* </ul>
* or a {@link org.apache.sling.api.servlets.PostResponse} otherwise
*/
PostResponse createPostResponse(final SlingHttpServletRequest req) {
for (final PostResponseCreator creator : cachedPostResponseCreators) {
final PostResponse response = creator.createPostResponse(req);
if (response != null) {
return response;
}
}
//for backward compatibility, if no "accept" request param or header is supplied
// then prefer the SlingHttpServletRequest#getResponseContentType value
MediaRangeList mediaRangeList = null;
String queryParam = req.getParameter(MediaRangeList.PARAM_ACCEPT);
if (queryParam == null || queryParam.trim().length() == 0) {
String headerValue = req.getHeader(MediaRangeList.HEADER_ACCEPT);
if (headerValue == null || headerValue.trim().length() == 0) {
//no param or header supplied, so try the response content type
mediaRangeList = new MediaRangeList(req.getResponseContentType());
}
}
// Fall through to default behavior
if (mediaRangeList == null) {
mediaRangeList = new MediaRangeList(req);
}
if (JSONResponse.RESPONSE_CONTENT_TYPE.equals(mediaRangeList.prefer("text/html", JSONResponse.RESPONSE_CONTENT_TYPE))) {
return new JSONResponse();
} else {
return new HtmlResponse();
}
}
/**
* Extending Servlet should implement this operation to do the work
*
* @param request the sling http request to process
* @param response the response
* @param changes the changes to report
* @throws RepositoryException if any errors applying the changes
*
* @deprecated use {@link #handleOperation(SlingHttpServletRequest, PostResponse, List)} instead
*/
@Deprecated
protected void handleOperation(SlingHttpServletRequest request,
AbstractPostResponse response, List<Modification> changes) throws RepositoryException {
handleOperation(request, (PostResponse)response, changes);
}
/**
* Extending Servlet should implement this operation to do the work
*
* @param request the sling http request to process
* @param response the response
* @param changes the changes to report
* @throws RepositoryException if any errors applying the changes
*/
protected abstract void handleOperation(SlingHttpServletRequest request,
PostResponse response, List<Modification> changes) throws RepositoryException;
/**
* compute redirect URL (SLING-126)
*
* @param request the sling http request to process
* @param ctx the post processor
* @return the redirect location or <code>null</code>
* @throws IOException if there is something invalid with the :redirect value
* @deprecated use {@link #getRedirectUrl(HttpServletRequest, PostResponse)} instead
*/
@Deprecated
protected String getRedirectUrl(HttpServletRequest request, AbstractPostResponse ctx) throws IOException {
return getRedirectUrl(request, (PostResponse)ctx);
}
/**
* compute redirect URL (SLING-126)
*
* @param request the sling http request to process
* @param ctx the post processor
* @return the redirect location or <code>null</code>
* @throws IOException if there is something invalid with the :redirect value
*/
protected String getRedirectUrl(HttpServletRequest request, PostResponse ctx) throws IOException {
// redirect param has priority (but see below, magic star)
String result = request.getParameter(SlingPostConstants.RP_REDIRECT_TO);
if (result != null) {
try {
URI redirectUri = new URI(result);
if (redirectUri.getAuthority() != null) {
// if it has a host information
throw new IOException("The redirect target included host information. This is not allowed for security reasons!");
}
} catch (URISyntaxException e) {
throw new IOException("The redirect target was not a valid uri");
}
if (ctx.getPath() != null) {
// redirect to created/modified Resource
int star = result.indexOf('*');
if (star >= 0) {
StringBuilder buf = new StringBuilder();
// anything before the star
if (star > 0) {
buf.append(result.substring(0, star));
}
// append the name of the manipulated node
buf.append(ResourceUtil.getName(ctx.getPath()));
// anything after the star
if (star < result.length() - 1) {
buf.append(result.substring(star + 1));
}
// use the created path as the redirect result
result = buf.toString();
} else if (result.endsWith(SlingPostConstants.DEFAULT_CREATE_SUFFIX)) {
// if the redirect has a trailing slash, append modified node
// name
result = result.concat(ResourceUtil.getName(ctx.getPath()));
}
}
}
return result;
}
protected boolean isSetStatus(SlingHttpServletRequest request) {
String statusParam = request.getParameter(SlingPostConstants.RP_STATUS);
if (statusParam == null) {
log.debug(
"getStatusMode: Parameter {} not set, assuming standard status code",
SlingPostConstants.RP_STATUS);
return true;
}
if (SlingPostConstants.STATUS_VALUE_BROWSER.equals(statusParam)) {
log.debug(
"getStatusMode: Parameter {} asks for user-friendly status code",
SlingPostConstants.RP_STATUS);
return false;
}
if (SlingPostConstants.STATUS_VALUE_STANDARD.equals(statusParam)) {
log.debug(
"getStatusMode: Parameter {} asks for standard status code",
SlingPostConstants.RP_STATUS);
return true;
}
log.debug("getStatusMode: Parameter {} set to unknown value {}, assuming standard status code",
SlingPostConstants.RP_STATUS, statusParam);
return true;
}
// ------ These methods were copied from AbstractSlingPostOperation ------
/**
* Returns the path of the resource of the request as the item path.
* <p>
* This method may be overwritten by extension if the operation has
* different requirements on path processing.
* </p>
* @param request the sling http request to process
* @return the resolved path of the found item
*/
protected String getItemPath(SlingHttpServletRequest request) {
if (allowNonExistingPaths()) {
return PrincipalAceHelper.getEffectivePath(request);
} else {
return request.getResource().getPath();
}
}
/**
* Returns an external form of the given path prepending the context path
* and appending a display extension.
*
* @param request the sling http request to process
* @param path the path to externalize
* @return the url
*/
protected String externalizePath(SlingHttpServletRequest request,
String path) {
if (path == null) {
if (allowNonExistingPaths()) {
path = PrincipalAceHelper.RESOURCE_PATH_REPOSITORY;
} else {
return null;
}
}
StringBuilder ret = new StringBuilder();
ret.append(SlingRequestPaths.getContextPath(request));
ret.append(request.getResourceResolver().map(path));
// append optional extension
String ext = request.getParameter(SlingPostConstants.RP_DISPLAY_EXTENSION);
if (ext != null && ext.length() > 0) {
if (ext.charAt(0) != '.') {
ret.append('.');
}
ret.append(ext);
}
return ret.toString();
}
/**
* Returns whether this operation can operate on paths that do
* not exist yet
*
* @return true if the resourcePath must exist, false otherwise
*/
protected boolean allowNonExistingPaths() {
return false;
}
/**
* Returns an external form of the parent path
* @param path the resource path
* @return parent path
*/
protected @Nullable String getParentPath(String path) {
if (path == null) {
// null path is ok for repository level privileges
return null;
}
return ResourceUtil.getParent(path);
}
/**
* Returns an <code>AccessControlList</code> to edit for the node at the
* given <code>resourcePath</code>.
*
* @param accessControlManager The manager providing access control lists
* @param resourcePath The node path for which to return an access control
* list
* @param mayCreate <code>true</code> if an access control list should be
* created if the node does not have one yet.
* @return The <code>AccessControlList</code> to modify to control access to
* the node.
* @throws RepositoryException If the access control manager does not
* provide a <code>AccessControlPolicy</code> which is an
* <code>AccessControlList</code>.
*/
protected AccessControlList getAccessControlList(
final AccessControlManager accessControlManager,
final String resourcePath, final boolean mayCreate)
throws RepositoryException {
// check for an existing access control list to edit
AccessControlPolicy[] policies = accessControlManager.getPolicies(resourcePath);
for (AccessControlPolicy policy : policies) {
if (policy instanceof AccessControlList) {
return (AccessControlList) policy;
}
}
// no existing access control list, try to create if allowed
if (mayCreate) {
AccessControlPolicyIterator applicablePolicies = accessControlManager.getApplicablePolicies(resourcePath);
while (applicablePolicies.hasNext()) {
AccessControlPolicy policy = applicablePolicies.nextAccessControlPolicy();
if (policy instanceof AccessControlList) {
return (AccessControlList) policy;
}
}
}
// neither an existing nor a create AccessControlList is available, fail
throw new RepositoryException(
"Unable to find or create an access control policy to update for "
+ resourcePath);
}
/**
* Returns an <code>AccessControlList</code> to edit for the node at the
* given <code>resourcePath</code>.
*
* @param accessControlManager The manager providing access control lists
* @param resourcePath The node path for which to return an access control
* list
* @param mayCreate <code>true</code> if an access control list should be
* created if the node does not have one yet.
* @return The <code>AccessControlList</code> to modify to control access to
* the node or null if one could not be located or created
* @throws RepositoryException if any errors reading the information
*/
protected AccessControlList getAccessControlListOrNull(
final AccessControlManager accessControlManager,
final String resourcePath, final boolean mayCreate)
throws RepositoryException {
AccessControlList acl = null;
// check for an existing access control list to edit
AccessControlPolicy[] policies = accessControlManager.getPolicies(resourcePath);
for (AccessControlPolicy policy : policies) {
if (policy instanceof AccessControlList) {
acl = (AccessControlList) policy;
}
}
if (acl == null && mayCreate) {
// no existing access control list, try to create if allowed
AccessControlPolicyIterator applicablePolicies = accessControlManager.getApplicablePolicies(resourcePath);
while (applicablePolicies.hasNext()) {
AccessControlPolicy policy = applicablePolicies.nextAccessControlPolicy();
if (policy instanceof AccessControlList) {
acl = (AccessControlList) policy;
}
}
}
return acl;
}
/**
* Bind a new post response creator
*
* @param creator the response creator service reference
* @param properties the component properties for the service reference
*/
// NOTE: the @Reference annotation is not inherited, so subclasses will need to override the #bindPostResponseCreator
// and #unbindPostResponseCreator methods to provide the @Reference annotation.
//
// @Reference(service = PostResponseCreator.class,
// cardinality = ReferenceCardinality.MULTIPLE,
// policy = ReferencePolicy.DYNAMIC)
protected void bindPostResponseCreator(final PostResponseCreator creator, final Map<String, Object> properties) {
final PostResponseCreatorHolder nngh = new PostResponseCreatorHolder(creator, getRanking(properties));
synchronized ( this.postResponseCreators ) {
int index = 0;
while ( index < this.postResponseCreators.size() &&
nngh.getRanking() < this.postResponseCreators.get(index).getRanking() ) {
index++;
}
if ( index == this.postResponseCreators.size() ) {
this.postResponseCreators.add(nngh);
} else {
this.postResponseCreators.add(index, nngh);
}
this.updatePostResponseCreatorCache();
}
}
/**
* Unbind a post response creator
*
* @param creator the response creator service reference
* @param properties the component properties for the service reference
*/
protected void unbindPostResponseCreator(final PostResponseCreator creator, final Map<String, Object> properties) {
synchronized ( this.postResponseCreators ) {
final Iterator<PostResponseCreatorHolder> i = this.postResponseCreators.iterator();
while ( i.hasNext() ) {
final PostResponseCreatorHolder current = i.next();
if ( current.getCreator() == creator ) {
i.remove();
}
}
this.updatePostResponseCreatorCache();
}
}
/**
* Update the post response creator cache
* This method is called by sync'ed methods, no need to add additional syncing.
*/
private void updatePostResponseCreatorCache() {
final PostResponseCreator[] localCache = new PostResponseCreator[this.postResponseCreators.size()];
int index = 0;
for(final PostResponseCreatorHolder current : this.postResponseCreators) {
localCache[index] = current.getCreator();
index++;
}
this.cachedPostResponseCreators = localCache;
}
private int getRanking(final Map<String, Object> properties) {
final Object val = properties.get(Constants.SERVICE_RANKING);
return val instanceof Integer ? (Integer)val : 0;
}
private static final class PostResponseCreatorHolder {
private final PostResponseCreator creator;
private final int ranking;
public PostResponseCreatorHolder(PostResponseCreator creator, int ranking) {
this.creator = creator;
this.ranking = ranking;
}
public PostResponseCreator getCreator() {
return creator;
}
public int getRanking() {
return ranking;
}
}
}