| /* |
| * 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.jackrabbit.usermanager.impl.post; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.InputStream; |
| import java.io.UnsupportedEncodingException; |
| import java.util.Calendar; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| |
| import javax.jcr.PropertyType; |
| import javax.jcr.RepositoryException; |
| import javax.jcr.Session; |
| import javax.jcr.Value; |
| import javax.jcr.ValueFactory; |
| |
| import org.apache.jackrabbit.api.security.user.Authorizable; |
| import org.apache.sling.api.SlingIOException; |
| import org.apache.sling.api.request.RequestParameter; |
| import org.apache.sling.commons.osgi.OsgiUtil; |
| import org.apache.sling.jackrabbit.usermanager.impl.resource.AuthorizableResourceProvider; |
| import org.apache.sling.servlets.post.Modification; |
| import org.apache.sling.servlets.post.SlingPostConstants; |
| import org.apache.sling.servlets.post.impl.helper.DateParser; |
| import org.apache.sling.servlets.post.impl.helper.RequestProperty; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * Base class for all the POST servlets for the UserManager operations |
| */ |
| public abstract class AbstractAuthorizablePostServlet extends |
| AbstractPostServlet { |
| private static final long serialVersionUID = -5918670409789895333L; |
| |
| public static final String PROP_DATE_FORMAT = "servlet.post.dateFormats"; |
| |
| private static final Logger LOG = LoggerFactory.getLogger(AbstractAuthorizablePostServlet.class); |
| |
| private DateParser dateParser; |
| |
| // ---------- SCR Integration ---------------------------------------------- |
| |
| protected void activate(Map<String, Object> props) { |
| dateParser = new DateParser(); |
| String[] dateFormats = OsgiUtil.toStringArray(props.get(PROP_DATE_FORMAT)); |
| for (String dateFormat : dateFormats) { |
| dateParser.register(dateFormat); |
| } |
| } |
| |
| protected void deactivate() { |
| dateParser = null; |
| } |
| |
| // ------ The methods below are based on the private methods from the |
| // ModifyOperation class ----- |
| |
| /** |
| * Collects the properties that form the content to be written back to the |
| * repository. |
| * @param properties the properties out of which to generate the {@link RequestProperty}s |
| * @return the list of {@link RequestProperty}s |
| */ |
| protected Collection<RequestProperty> collectContent( |
| Map<String, ?> properties) { |
| |
| boolean requireItemPrefix = requireItemPathPrefix(properties); |
| |
| // walk the request parameters and collect the properties (the key is the property path). |
| Map<String, RequestProperty> reqProperties = new HashMap<String, RequestProperty>(); |
| for (Map.Entry<String, ?> e : properties.entrySet()) { |
| final String paramName = e.getKey(); |
| |
| // do not store parameters with names starting with sling:post |
| if (paramName.startsWith(SlingPostConstants.RP_PREFIX)) { |
| continue; |
| } |
| // SLING-298: skip form encoding parameter |
| if (paramName.equals("_charset_")) { |
| continue; |
| } |
| // skip parameters that do not start with the save prefix |
| if (requireItemPrefix && !hasItemPathPrefix(paramName)) { |
| continue; |
| } |
| |
| // ensure the paramName is an absolute property path (i.e. starts with "/", where root refers to the authorizable's root, https://issues.apache.org/jira/browse/SLING-1577) |
| String propPath; |
| if (paramName.startsWith("./")) { |
| propPath = paramName.substring(1); |
| } else { |
| propPath = "/" + paramName; |
| } |
| |
| if (propPath.indexOf("..") != -1) { |
| // it is not supported to set properties potentially outside of the authorizable node |
| LOG.warn("Property path containing '..' is not supported, skipping parameter {}", SlingPostConstants.SUFFIX_COPY_FROM, paramName); |
| continue; // skip it. |
| } |
| |
| // @TypeHint example |
| // <input type="text" name="./age" /> |
| // <input type="hidden" name="./age@TypeHint" value="long" /> |
| // causes the setProperty using the 'long' property type |
| if (propPath.endsWith(SlingPostConstants.TYPE_HINT_SUFFIX)) { |
| RequestProperty prop = getOrCreateRequestProperty( |
| reqProperties, propPath, |
| SlingPostConstants.TYPE_HINT_SUFFIX); |
| |
| String typeHintValue = convertToString(e.getValue()); |
| if (typeHintValue != null) { |
| prop.setTypeHintValue(typeHintValue); |
| } |
| |
| continue; |
| } |
| |
| // @DefaultValue |
| if (propPath.endsWith(SlingPostConstants.DEFAULT_VALUE_SUFFIX)) { |
| RequestProperty prop = getOrCreateRequestProperty( |
| reqProperties, propPath, |
| SlingPostConstants.DEFAULT_VALUE_SUFFIX); |
| |
| prop.setDefaultValues(convertToRequestParameterArray(e.getValue())); |
| |
| continue; |
| } |
| |
| // SLING-130: VALUE_FROM_SUFFIX means take the value of this |
| // property from a different field |
| // @ValueFrom example: |
| // <input name="./Text@ValueFrom" type="hidden" value="fulltext" /> |
| // causes the JCR Text property to be set to the value of the |
| // fulltext form field. |
| if (propPath.endsWith(SlingPostConstants.VALUE_FROM_SUFFIX)) { |
| RequestProperty prop = getOrCreateRequestProperty( |
| reqProperties, propPath, |
| SlingPostConstants.VALUE_FROM_SUFFIX); |
| |
| // @ValueFrom params must have exactly one value, else ignored |
| String [] valueFrom = convertToStringArray(e.getValue()); |
| if (valueFrom.length == 1) { |
| String refName = valueFrom[0]; |
| RequestParameter[] refValues = convertToRequestParameterArray(refName); |
| if (refValues != null) { |
| prop.setValues(refValues); |
| } |
| } |
| |
| continue; |
| } |
| |
| // SLING-458: Allow Removal of properties prior to update |
| // @Delete example: |
| // <input name="./Text@Delete" type="hidden" /> |
| // causes the JCR Text property to be deleted before update |
| if (propPath.endsWith(SlingPostConstants.SUFFIX_DELETE)) { |
| RequestProperty prop = getOrCreateRequestProperty( |
| reqProperties, propPath, SlingPostConstants.SUFFIX_DELETE); |
| |
| prop.setDelete(true); |
| |
| continue; |
| } |
| |
| // SLING-455: @MoveFrom means moving content to another location |
| // @MoveFrom example: |
| // <input name="./Text@MoveFrom" type="hidden" value="/tmp/path" /> |
| // causes the JCR Text property to be set by moving the /tmp/path |
| // property to Text. |
| if (propPath.endsWith(SlingPostConstants.SUFFIX_MOVE_FROM)) { |
| // don't support @MoveFrom here |
| LOG.warn("Suffix {} not supported, skipping parameter {}", SlingPostConstants.SUFFIX_MOVE_FROM, paramName); |
| continue; |
| } |
| |
| // SLING-455: @CopyFrom means moving content to another location |
| // @CopyFrom example: |
| // <input name="./Text@CopyFrom" type="hidden" value="/tmp/path" /> |
| // causes the JCR Text property to be set by copying the /tmp/path |
| // property to Text. |
| if (propPath.endsWith(SlingPostConstants.SUFFIX_COPY_FROM)) { |
| // don't support @CopyFrom here |
| LOG.warn("Suffix {} not supported, skipping parameter {}", SlingPostConstants.SUFFIX_COPY_FROM, paramName); |
| continue; |
| } |
| |
| // plain property, create from values |
| RequestProperty prop = getOrCreateRequestProperty(reqProperties, |
| propPath, null); |
| prop.setValues(convertToRequestParameterArray(e.getValue())); |
| } |
| |
| return reqProperties.values(); |
| } |
| |
| /** |
| * Returns the request property for the given property path. If such a |
| * request property does not exist yet it is created and stored in the |
| * <code>props</code>. |
| * |
| * @param props The map of already seen request properties |
| * (key is the property path). |
| * @param paramPath The absolute path of the property including the |
| * <code>suffix</code> to be looked up. |
| * @param suffix The (optional) suffix to remove from the |
| * <code>paramName</code> before looking it up. |
| * @return The {@link RequestProperty} for the <code>paramName</code>. |
| */ |
| private RequestProperty getOrCreateRequestProperty( |
| Map<String, RequestProperty> props, String paramPath, String suffix) { |
| if (suffix != null && paramPath.endsWith(suffix)) { |
| paramPath = paramPath.substring(0, paramPath.length() |
| - suffix.length()); |
| } |
| |
| RequestProperty prop = props.get(paramPath); |
| if (prop == null) { |
| prop = new RequestProperty(paramPath); |
| props.put(paramPath, prop); |
| } |
| |
| return prop; |
| } |
| |
| /** |
| * Removes all properties listed as {@link RequestProperty#isDelete()} from |
| * the authorizable. |
| * |
| * @param authorizable The |
| * <code>org.apache.jackrabbit.api.security.user.Authorizable</code> |
| * that should have properties deleted. |
| * @param reqProperties The collection of request properties to check for |
| * properties to be removed. |
| * @param changes The <code>List</code> to be updated with |
| * information on deleted properties. |
| * @throws RepositoryException Is thrown if an error occurrs checking or |
| * removing properties. |
| */ |
| protected void processDeletes(Authorizable authorizable, |
| Collection<RequestProperty> reqProperties, |
| List<Modification> changes) throws RepositoryException { |
| |
| for (RequestProperty property : reqProperties) { |
| if (property.isDelete()) { |
| if (authorizable.hasProperty(property.getName())) { |
| authorizable.removeProperty(property.getName()); |
| changes.add(Modification.onDeleted(property.getPath())); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Writes back the content |
| * @param session the sessioin to write the authorizable properties |
| * @param authorizable the authorizable to modify |
| * @param reqProperties the properties to write |
| * @param changes the list of changes which is supposed to be extended |
| * |
| * @throws RepositoryException if a repository error occurs |
| */ |
| protected void writeContent(Session session, Authorizable authorizable, |
| Collection<RequestProperty> reqProperties, |
| List<Modification> changes) throws RepositoryException { |
| |
| for (RequestProperty prop : reqProperties) { |
| if (prop.hasValues()) { |
| // remove artificial "/" prepended to the prop path |
| String relativePath = prop.getPath().substring(1); |
| |
| // skip jcr special properties |
| if (relativePath.equals("jcr:primaryType") |
| || relativePath.equals("jcr:mixinTypes")) { |
| continue; |
| } |
| if (authorizable.isGroup()) { |
| if (relativePath.equals("groupId")) { |
| // skip these |
| continue; |
| } |
| } else { |
| if (relativePath.equals("userId") |
| || relativePath.equals("pwd") |
| || relativePath.equals("pwdConfirm")) { |
| // skip these |
| continue; |
| } |
| } |
| if (prop.isFileUpload()) { |
| // don't handle files for user properties for now. |
| continue; |
| // uploadHandler.setFile(parent, prop, changes); |
| } else { |
| setPropertyAsIs(session, authorizable, prop, changes); |
| } |
| } |
| } |
| } |
| |
| /** |
| * set property without processing, except for type hints |
| * |
| * @param parent the parent node |
| * @param prop the request property |
| * @throws RepositoryException if a repository error occurs. |
| */ |
| private void setPropertyAsIs(Session session, Authorizable parent, |
| RequestProperty prop, List<Modification> changes) |
| throws RepositoryException { |
| |
| String parentPath; |
| if (parent.isGroup()) { |
| parentPath = AuthorizableResourceProvider.SYSTEM_USER_MANAGER_GROUP_PREFIX |
| + parent.getID(); |
| } else { |
| parentPath = AuthorizableResourceProvider.SYSTEM_USER_MANAGER_USER_PREFIX |
| + parent.getID(); |
| } |
| |
| // no explicit typehint |
| int type = PropertyType.UNDEFINED; |
| if (prop.getTypeHint() != null) { |
| try { |
| type = PropertyType.valueFromName(prop.getTypeHint()); |
| } catch (Exception e) { |
| // ignore |
| } |
| } |
| // remove artificial "/" prepended to the prop path |
| String relativePath = prop.getPath().substring(1); |
| |
| String[] values = prop.getStringValues(); |
| if (values == null) { |
| // remove property |
| boolean removedProp = removePropertyIfExists(parent, relativePath); |
| if (removedProp) { |
| changes.add(Modification.onDeleted(parentPath + "/" |
| + relativePath)); |
| } |
| } else if (values.length == 0) { |
| // do not create new prop here, but clear existing |
| if (parent.hasProperty(relativePath)) { |
| Value val = session.getValueFactory().createValue(""); |
| parent.setProperty(relativePath, val); |
| changes.add(Modification.onModified(parentPath + "/" |
| + relativePath)); |
| } |
| } else if (values.length == 1) { |
| boolean removedProp = removePropertyIfExists(parent, relativePath); |
| // if the provided value is the empty string, we don't have to do |
| // anything. |
| if (values[0].length() == 0) { |
| if (removedProp) { |
| changes.add(Modification.onDeleted(parentPath + "/" |
| + relativePath)); |
| } |
| } else { |
| // modify property |
| if (type == PropertyType.DATE) { |
| // try conversion |
| Calendar c = dateParser.parse(values[0]); |
| if (c != null) { |
| if (prop.hasMultiValueTypeHint()) { |
| final Value[] array = new Value[1]; |
| array[0] = session.getValueFactory().createValue(c); |
| parent.setProperty(relativePath, array); |
| changes.add(Modification.onModified(parentPath |
| + "/" + relativePath)); |
| } else { |
| Value cVal = session.getValueFactory().createValue( |
| c); |
| parent.setProperty(prop.getName(), cVal); |
| changes.add(Modification.onModified(parentPath |
| + "/" + relativePath)); |
| } |
| return; |
| } |
| // fall back to default behaviour |
| } |
| if (type == PropertyType.UNDEFINED) { |
| Value val = session.getValueFactory().createValue( |
| values[0], PropertyType.STRING); |
| parent.setProperty(relativePath, val); |
| } else { |
| if (prop.hasMultiValueTypeHint()) { |
| final Value[] array = new Value[1]; |
| array[0] = session.getValueFactory().createValue( |
| values[0], type); |
| parent.setProperty(relativePath, array); |
| } else { |
| Value val = session.getValueFactory().createValue( |
| values[0], type); |
| parent.setProperty(relativePath, val); |
| } |
| } |
| changes.add(Modification.onModified(parentPath + "/" |
| + relativePath)); |
| } |
| } else { |
| removePropertyIfExists(parent, relativePath); |
| if (type == PropertyType.DATE) { |
| // try conversion |
| ValueFactory valFac = session.getValueFactory(); |
| Value[] c = dateParser.parse(values, valFac); |
| if (c != null) { |
| parent.setProperty(relativePath, c); |
| changes.add(Modification.onModified(parentPath + "/" |
| + relativePath)); |
| return; |
| } |
| // fall back to default behaviour |
| } |
| |
| Value[] vals = new Value[values.length]; |
| if (type == PropertyType.UNDEFINED) { |
| for (int i = 0; i < values.length; i++) { |
| vals[i] = session.getValueFactory().createValue(values[i]); |
| } |
| } else { |
| for (int i = 0; i < values.length; i++) { |
| vals[i] = session.getValueFactory().createValue(values[i], |
| type); |
| } |
| } |
| parent.setProperty(relativePath, vals); |
| changes.add(Modification.onModified(parentPath + "/" |
| + relativePath)); |
| } |
| |
| } |
| |
| /** |
| * Removes the property with the given name from the authorizable if it |
| * exists. |
| * |
| * @param authorizable the <code>org.apache.jackrabbit.api.security.user.Authorizable</code> |
| * that should have properties deleted. |
| * @param path the path of the property to remove |
| * @return path of the property that was removed or <code>null</code> if it |
| * was not removed |
| * @throws RepositoryException if a repository error occurs. |
| */ |
| private boolean removePropertyIfExists(Authorizable authorizable, String path) |
| throws RepositoryException { |
| if (authorizable.getProperty(path) != null) { |
| authorizable.removeProperty(path); |
| return true; |
| } |
| return false; |
| } |
| |
| // ------ These methods were copied from AbstractSlingPostOperation ------ |
| |
| /** |
| * @param name the name |
| * @return <code>true</code> if the <code>name</code> starts with either of |
| * the prefixes {@link SlingPostConstants#ITEM_PREFIX_RELATIVE_CURRENT}, |
| * {@link SlingPostConstants#ITEM_PREFIX_RELATIVE_PARENT} and |
| * {@link SlingPostConstants#ITEM_PREFIX_ABSOLUTE} |
| */ |
| protected boolean hasItemPathPrefix(String name) { |
| return name.startsWith(SlingPostConstants.ITEM_PREFIX_ABSOLUTE) |
| || name.startsWith(SlingPostConstants.ITEM_PREFIX_RELATIVE_CURRENT) |
| || name.startsWith(SlingPostConstants.ITEM_PREFIX_RELATIVE_PARENT); |
| } |
| |
| /** |
| * @param properties the request parameters |
| * @return {@code true} if any of the request parameters starts with |
| * {@link SlingPostConstants#ITEM_PREFIX_RELATIVE_CURRENT}. |
| * In this case only parameters starting with either of the prefixes |
| * {@link SlingPostConstants#ITEM_PREFIX_RELATIVE_CURRENT}, |
| * {@link SlingPostConstants#ITEM_PREFIX_RELATIVE_PARENT} |
| * and {@link SlingPostConstants#ITEM_PREFIX_ABSOLUTE} are |
| * considered as providing content to be stored. Otherwise all parameters |
| * not starting with the command prefix <code>:</code> are considered as |
| * parameters to be stored. |
| */ |
| protected final boolean requireItemPathPrefix( |
| Map<String, ?> properties) { |
| |
| boolean requirePrefix = false; |
| |
| Iterator<String> iterator = properties.keySet().iterator(); |
| while (iterator.hasNext() && !requirePrefix) { |
| String name = iterator.next(); |
| requirePrefix = name.startsWith(SlingPostConstants.ITEM_PREFIX_RELATIVE_CURRENT); |
| } |
| |
| return requirePrefix; |
| } |
| |
| |
| protected String convertToString(Object obj) { |
| if (obj == null) { |
| return null; |
| } |
| |
| if (obj instanceof String) { |
| return (String)obj; |
| } else if (obj instanceof String[]) { |
| String [] values = (String[])obj; |
| if (values.length > 0) { |
| return values[0]; |
| } |
| return null; |
| } else if (obj instanceof RequestParameter) { |
| ((RequestParameter)obj).getString(); |
| } else if (obj instanceof RequestParameter[]) { |
| RequestParameter[] values = (RequestParameter[])obj; |
| if (values.length > 0) { |
| return values[0].getString(); |
| } |
| return null; |
| } |
| return null; |
| } |
| |
| protected String[] convertToStringArray(Object obj) { |
| if (obj == null) { |
| return null; |
| } |
| |
| if (obj instanceof String) { |
| return new String[] {(String)obj}; |
| } else if (obj instanceof String[]) { |
| return (String[])obj; |
| } else if (obj instanceof RequestParameter) { |
| return new String[] {((RequestParameter)obj).getString()}; |
| } else if (obj instanceof RequestParameter[]) { |
| RequestParameter[] values = (RequestParameter[])obj; |
| String [] strValues = new String[values.length]; |
| for (int i=0; i < values.length; i++) { |
| strValues[i] = values[i].getString(); |
| } |
| return strValues; |
| } |
| return null; |
| } |
| |
| protected RequestParameter[] convertToRequestParameterArray(Object obj) { |
| if (obj == null) { |
| return null; |
| } |
| |
| if (obj instanceof String) { |
| return new RequestParameter[] { |
| new RequestParameterImpl((String)obj, null) |
| }; |
| } else if (obj instanceof String[]) { |
| String [] strValues = (String[])obj; |
| RequestParameter [] values = new RequestParameter[strValues.length]; |
| for (int i=0; i < strValues.length; i++) { |
| values[i] = new RequestParameterImpl(strValues[i], null); |
| } |
| return values; |
| } else if (obj instanceof RequestParameter) { |
| return new RequestParameter[] {(RequestParameter)obj}; |
| } else if (obj instanceof RequestParameter[]) { |
| return (RequestParameter[])obj; |
| } |
| return null; |
| } |
| |
| static class RequestParameterImpl implements RequestParameter { |
| |
| private String value; |
| private String encoding; |
| |
| private byte[] content; |
| |
| RequestParameterImpl(String value, String encoding) { |
| this.encoding = encoding; |
| this.value = value; |
| this.content = null; |
| } |
| |
| String getEncoding() { |
| return this.encoding; |
| } |
| |
| void setEncoding(String encoding) { |
| // recode this parameter by encoding the string with the current |
| // encoding and decode the bytes with the encoding |
| try { |
| this.value = getString(encoding); |
| } catch (UnsupportedEncodingException uee) { |
| throw new SlingUnsupportedEncodingException(uee); |
| } |
| this.encoding = encoding; |
| } |
| |
| /** |
| * @see org.apache.sling.api.request.RequestParameter#get() |
| */ |
| public byte[] get() { |
| if (content == null) { |
| try { |
| content = getString().getBytes(getEncoding()); |
| } catch (Exception e) { |
| // UnsupportedEncodingException, IllegalArgumentException |
| content = getString().getBytes(); |
| } |
| } |
| return content; |
| } |
| |
| /** |
| * @see org.apache.sling.api.request.RequestParameter#getContentType() |
| */ |
| public String getContentType() { |
| // none known for www-form-encoded parameters |
| return null; |
| } |
| |
| /** |
| * @see org.apache.sling.api.request.RequestParameter#getInputStream() |
| */ |
| public InputStream getInputStream() { |
| return new ByteArrayInputStream(this.get()); |
| } |
| |
| /** |
| * @see org.apache.sling.api.request.RequestParameter#getFileName() |
| */ |
| public String getFileName() { |
| // no original file name |
| return null; |
| } |
| |
| /** |
| * @see org.apache.sling.api.request.RequestParameter#getSize() |
| */ |
| public long getSize() { |
| return this.get().length; |
| } |
| |
| /** |
| * @see org.apache.sling.api.request.RequestParameter#getString() |
| */ |
| public String getString() { |
| return value; |
| } |
| |
| /** |
| * @see org.apache.sling.api.request.RequestParameter#getString(java.lang.String) |
| */ |
| public String getString(String encoding) |
| throws UnsupportedEncodingException { |
| return new String(this.get(), encoding); |
| } |
| |
| /** |
| * @see org.apache.sling.api.request.RequestParameter#isFormField() |
| */ |
| public boolean isFormField() { |
| // www-form-encoded are always form fields |
| return true; |
| } |
| |
| @Override |
| public String getName() { |
| // TODO Auto-generated method stub |
| return null; |
| } |
| |
| @Override |
| public String toString() { |
| return this.getString(); |
| } |
| } |
| |
| static class SlingUnsupportedEncodingException extends SlingIOException { |
| |
| private static final long serialVersionUID = -4482276105859280247L; |
| |
| SlingUnsupportedEncodingException(UnsupportedEncodingException uee) { |
| super(uee); |
| } |
| |
| } |
| |
| } |