1 /* 2 * Copyright (c) 2002-2007 by OpenSymphony 3 * All rights reserved. 4 */ 5 package com.opensymphony.xwork2.interceptor; 6 7 import java.util.Collection; 8 import java.util.Collections; 9 import java.util.Comparator; 10 import java.util.HashSet; 11 import java.util.Map; 12 import java.util.Set; 13 import java.util.TreeMap; 14 import java.util.regex.Matcher; 15 import java.util.regex.Pattern; 16 17 import com.opensymphony.xwork2.ActionContext; 18 import com.opensymphony.xwork2.ActionInvocation; 19 import com.opensymphony.xwork2.ValidationAware; 20 import com.opensymphony.xwork2.conversion.impl.InstantiatingNullHandler; 21 import com.opensymphony.xwork2.conversion.impl.XWorkConverter; 22 import com.opensymphony.xwork2.inject.Inject; 23 import com.opensymphony.xwork2.util.ClearableValueStack; 24 import com.opensymphony.xwork2.util.LocalizedTextUtil; 25 import com.opensymphony.xwork2.util.MemberAccessValueStack; 26 import com.opensymphony.xwork2.util.TextParseUtil; 27 import com.opensymphony.xwork2.util.ValueStack; 28 import com.opensymphony.xwork2.util.ValueStackFactory; 29 import com.opensymphony.xwork2.util.logging.Logger; 30 import com.opensymphony.xwork2.util.logging.LoggerFactory; 31 import com.opensymphony.xwork2.util.reflection.ReflectionContextState; 32 33 34 /** 35 * <!-- START SNIPPET: description --> 36 * This interceptor sets all parameters on the value stack. 37 * <p/> 38 * This interceptor gets all parameters from {@link ActionContext#getParameters()} and sets them on the value stack by 39 * calling {@link ValueStack#setValue(String, Object)}, typically resulting in the values submitted in a form 40 * request being applied to an action in the value stack. Note that the parameter map must contain a String key and 41 * often containers a String[] for the value. 42 * <p/> 43 * <p/> The interceptor takes one parameter named 'ordered'. When set to true action properties are guaranteed to be 44 * set top-down which means that top action's properties are set first. Then it's subcomponents properties are set. 45 * The reason for this order is to enable a 'factory' pattern. For example, let's assume that one has an action 46 * that contains a property named 'modelClass' that allows to choose what is the underlying implementation of model. 47 * By assuring that modelClass property is set before any model properties are set, it's possible to choose model 48 * implementation during action.setModelClass() call. Similiarily it's possible to use action.setPrimaryKey() 49 * property set call to actually load the model class from persistent storage. Without any assumption on parameter 50 * order you have to use patterns like 'Preparable'. 51 * <p/> 52 * <p/> Because parameter names are effectively OGNL statements, it is important that security be taken in to account. 53 * This interceptor will not apply any values in the parameters map if the expression contains an assignment (=), 54 * multiple expressions (,), or references any objects in the context (#). This is all done in the {@link 55 * #acceptableName(String)} method. In addition to this method, if the action being invoked implements the {@link 56 * ParameterNameAware} interface, the action will be consulted to determine if the parameter should be set. 57 * <p/> 58 * <p/> In addition to these restrictions, a flag ({@link ReflectionContextState#DENY_METHOD_EXECUTION}) is set such that 59 * no methods are allowed to be invoked. That means that any expression such as <i>person.doSomething()</i> or 60 * <i>person.getName()</i> will be explicitely forbidden. This is needed to make sure that your application is not 61 * exposed to attacks by malicious users. 62 * <p/> 63 * <p/> While this interceptor is being invoked, a flag ({@link ReflectionContextState#CREATE_NULL_OBJECTS}) is turned 64 * on to ensure that any null reference is automatically created - if possible. See the type conversion documentation 65 * and the {@link InstantiatingNullHandler} javadocs for more information. 66 * <p/> 67 * <p/> Finally, a third flag ({@link XWorkConverter#REPORT_CONVERSION_ERRORS}) is set that indicates any errors when 68 * converting the the values to their final data type (String[] -> int) an unrecoverable error occured. With this 69 * flag set, the type conversion errors will be reported in the action context. See the type conversion documentation 70 * and the {@link XWorkConverter} javadocs for more information. 71 * <p/> 72 * <p/> If you are looking for detailed logging information about your parameters, turn on DEBUG level logging for this 73 * interceptor. A detailed log of all the parameter keys and values will be reported. 74 * <p/> 75 * <p/> 76 * <b>Note:</b> Since XWork 2.0.2, this interceptor extends {@link MethodFilterInterceptor}, therefore being 77 * able to deal with excludeMethods / includeMethods parameters. See [Workflow Interceptor] 78 * (class {@link DefaultWorkflowInterceptor}) for documentation and examples on how to use this feature. 79 * <p/> 80 * <!-- END SNIPPET: description --> 81 * <p/> 82 * <p/> <u>Interceptor parameters:</u> 83 * <p/> 84 * <!-- START SNIPPET: parameters --> 85 * <p/> 86 * <ul> 87 * <p/> 88 * <li>ordered - set to true if you want the top-down property setter behaviour</li> 89 * <p/> 90 * </ul> 91 * <p/> 92 * <!-- END SNIPPET: parameters --> 93 * <p/> 94 * <p/> <u>Extending the interceptor:</u> 95 * <p/> 96 * <!-- START SNIPPET: extending --> 97 * <p/> 98 * <p/> The best way to add behavior to this interceptor is to utilize the {@link ParameterNameAware} interface in your 99 * actions. However, if you wish to apply a global rule that isn't implemented in your action, then you could extend 100 * this interceptor and override the {@link #acceptableName(String)} method. 101 * <p/> 102 * <!-- END SNIPPET: extending --> 103 * <p/> 104 * <p/> <u>Example code:</u> 105 * <p/> 106 * <pre> 107 * <!-- START SNIPPET: example --> 108 * <action name="someAction" class="com.examples.SomeAction"> 109 * <interceptor-ref name="params"/> 110 * <result name="success">good_result.ftl</result> 111 * </action> 112 * <!-- END SNIPPET: example --> 113 * </pre> 114 * 115 * @author Patrick Lightbody 116 */ 117 public class ParametersInterceptor extends MethodFilterInterceptor { 118 119 private static final Logger LOG = LoggerFactory.getLogger(ParametersInterceptor.class); 120 121 boolean ordered = false; 122 Set<Pattern> excludeParams = Collections.emptySet(); 123 Set<Pattern> acceptParams = Collections.emptySet(); 124 static boolean devMode = false; 125 126 private String acceptedParamNames = "[[\\p{Graph}\\s]&&[^,#:=]]*"; 127 private Pattern acceptedPattern = Pattern.compile(acceptedParamNames); 128 129 private ValueStackFactory valueStackFactory; 130 131 @Inject 132 public void setValueStackFactory(ValueStackFactory valueStackFactory) { 133 this.valueStackFactory = valueStackFactory; 134 } 135 136 @Inject("devMode") 137 public static void setDevMode(String mode) { 138 devMode = "true".equals(mode); 139 } 140 141 public void setAcceptParamNames(String commaDelim) { 142 Collection<String> acceptPatterns = asCollection(commaDelim); 143 if (acceptPatterns != null) { 144 acceptParams = new HashSet<Pattern>(); 145 for (String pattern : acceptPatterns) { 146 acceptParams.add(Pattern.compile(pattern)); 147 } 148 } 149 } 150 151 /** 152 * Compares based on number of '.' characters (fewer is higher) 153 */ 154 static final Comparator<String> rbCollator = new Comparator<String>() { 155 public int compare(String s1, String s2) { 156 int l1 = 0, l2 = 0; 157 for (int i = s1.length() - 1; i >= 0; i--) { 158 if (s1.charAt(i) == '.') l1++; 159 } 160 for (int i = s2.length() - 1; i >= 0; i--) { 161 if (s2.charAt(i) == '.') l2++; 162 } 163 return l1 < l2 ? -1 : (l2 < l1 ? 1 : s1.compareTo(s2)); 164 } 165 166 }; 167 168 @Override 169 public String doIntercept(ActionInvocation invocation) throws Exception { 170 Object action = invocation.getAction(); 171 if (!(action instanceof NoParameters)) { 172 ActionContext ac = invocation.getInvocationContext(); 173 final Map<String, Object> parameters = retrieveParameters(ac); 174 175 if (LOG.isDebugEnabled()) { 176 LOG.debug("Setting params " + getParameterLogMap(parameters)); 177 } 178 179 if (parameters != null) { 180 Map<String, Object> contextMap = ac.getContextMap(); 181 try { 182 ReflectionContextState.setCreatingNullObjects(contextMap, true); 183 ReflectionContextState.setDenyMethodExecution(contextMap, true); 184 ReflectionContextState.setReportingConversionErrors(contextMap, true); 185 186 ValueStack stack = ac.getValueStack(); 187 setParameters(action, stack, parameters); 188 } finally { 189 ReflectionContextState.setCreatingNullObjects(contextMap, false); 190 ReflectionContextState.setDenyMethodExecution(contextMap, false); 191 ReflectionContextState.setReportingConversionErrors(contextMap, false); 192 } 193 } 194 } 195 return invocation.invoke(); 196 } 197 198 /** 199 * Gets the parameter map to apply from wherever appropriate 200 * 201 * @param ac The action context 202 * @return The parameter map to apply 203 */ 204 protected Map<String, Object> retrieveParameters(ActionContext ac) { 205 return ac.getParameters(); 206 } 207 208 209 /** 210 * Adds the parameters into context's ParameterMap 211 * 212 * @param ac The action context 213 * @param newParams The parameter map to apply 214 * <p/> 215 * In this class this is a no-op, since the parameters were fetched from the same location. 216 * In subclasses both retrieveParameters() and addParametersToContext() should be overridden. 217 */ 218 protected void addParametersToContext(ActionContext ac, Map<String, Object> newParams) { 219 } 220 221 protected void setParameters(Object action, ValueStack stack, final Map<String, Object> parameters) { 222 ParameterNameAware parameterNameAware = (action instanceof ParameterNameAware) 223 ? (ParameterNameAware) action : null; 224 225 Map<String, Object> params; 226 Map<String, Object> acceptableParameters; 227 if (ordered) { 228 params = new TreeMap<String, Object>(getOrderedComparator()); 229 acceptableParameters = new TreeMap<String, Object>(getOrderedComparator()); 230 params.putAll(parameters); 231 } else { 232 params = new TreeMap<String, Object>(parameters); 233 acceptableParameters = new TreeMap<String, Object>(); 234 } 235 236 for (Map.Entry<String, Object> entry : params.entrySet()) { 237 String name = entry.getKey(); 238 239 boolean acceptableName = acceptableName(name) 240 && (parameterNameAware == null 241 || parameterNameAware.acceptableParameterName(name)); 242 243 if (acceptableName) { 244 acceptableParameters.put(name, entry.getValue()); 245 } 246 } 247 248 ValueStack newStack = valueStackFactory.createValueStack(stack); 249 boolean clearableStack = newStack instanceof ClearableValueStack; 250 if (clearableStack) { 251 //if the stack's context can be cleared, do that to prevent OGNL 252 //from having access to objects in the stack, see XW-641 253 ((ClearableValueStack)newStack).clearContextValues(); 254 Map<String, Object> context = newStack.getContext(); 255 ReflectionContextState.setCreatingNullObjects(context, true); 256 ReflectionContextState.setDenyMethodExecution(context, true); 257 ReflectionContextState.setReportingConversionErrors(context, true); 258 259 //keep locale from original context 260 context.put(ActionContext.LOCALE, stack.getContext().get(ActionContext.LOCALE)); 261 } 262 263 boolean memberAccessStack = newStack instanceof MemberAccessValueStack; 264 if (memberAccessStack) { 265 //block or allow access to properties 266 //see WW-2761 for more details 267 MemberAccessValueStack accessValueStack = (MemberAccessValueStack) newStack; 268 accessValueStack.setAcceptProperties(acceptParams); 269 accessValueStack.setExcludeProperties(excludeParams); 270 } 271 272 for (Map.Entry<String, Object> entry : acceptableParameters.entrySet()) { 273 String name = entry.getKey(); 274 Object value = entry.getValue(); 275 try { 276 newStack.setValue(name, value); 277 } catch (RuntimeException e) { 278 if (devMode) { 279 String developerNotification = LocalizedTextUtil.findText(ParametersInterceptor.class, "devmode.notification", ActionContext.getContext().getLocale(), "Developer Notification:\n{0}", new Object[]{ 280 "Unexpected Exception caught setting '" + name + "' on '" + action.getClass() + ": " + e.getMessage() 281 }); 282 LOG.error(developerNotification); 283 if (action instanceof ValidationAware) { 284 ((ValidationAware) action).addActionMessage(developerNotification); 285 } 286 } 287 } 288 } 289 290 if (clearableStack && (stack.getContext() != null) && (newStack.getContext() != null)) 291 stack.getContext().put(ActionContext.CONVERSION_ERRORS, newStack.getContext().get(ActionContext.CONVERSION_ERRORS)); 292 293 addParametersToContext(ActionContext.getContext(), acceptableParameters); 294 } 295 296 /** 297 * Gets an instance of the comparator to use for the ordered sorting. Override this 298 * method to customize the ordering of the parameters as they are set to the 299 * action. 300 * 301 * @return A comparator to sort the parameters 302 */ 303 protected Comparator<String> getOrderedComparator() { 304 return rbCollator; 305 } 306 307 private String getParameterLogMap(Map<String, Object> parameters) { 308 if (parameters == null) { 309 return "NONE"; 310 } 311 312 StringBuilder logEntry = new StringBuilder(); 313 for (Map.Entry entry : parameters.entrySet()) { 314 logEntry.append(String.valueOf(entry.getKey())); 315 logEntry.append(" => "); 316 if (entry.getValue() instanceof Object[]) { 317 Object[] valueArray = (Object[]) entry.getValue(); 318 logEntry.append("[ "); 319 if (valueArray.length > 0 ) { 320 for (int indexA = 0; indexA < (valueArray.length - 1); indexA++) { 321 Object valueAtIndex = valueArray[indexA]; 322 logEntry.append(String.valueOf(valueAtIndex)); 323 logEntry.append(", "); 324 } 325 logEntry.append(String.valueOf(valueArray[valueArray.length - 1])); 326 } 327 logEntry.append(" ] "); 328 } else { 329 logEntry.append(String.valueOf(entry.getValue())); 330 } 331 } 332 333 return logEntry.toString(); 334 } 335 336 protected boolean acceptableName(String name) { 337 if (isAccepted(name) && !isExcluded(name)) { 338 return true; 339 } 340 return false; 341 } 342 343 protected boolean isAccepted(String paramName) { 344 if (!this.acceptParams.isEmpty()) { 345 for (Pattern pattern : acceptParams) { 346 Matcher matcher = pattern.matcher(paramName); 347 if (matcher.matches()) { 348 return true; 349 } 350 } 351 return false; 352 } else 353 return acceptedPattern.matcher(paramName).matches(); 354 } 355 356 protected boolean isExcluded(String paramName) { 357 if (!this.excludeParams.isEmpty()) { 358 for (Pattern pattern : excludeParams) { 359 Matcher matcher = pattern.matcher(paramName); 360 if (matcher.matches()) { 361 return true; 362 } 363 } 364 } 365 return false; 366 } 367 368 /** 369 * Whether to order the parameters or not 370 * 371 * @return True to order 372 */ 373 public boolean isOrdered() { 374 return ordered; 375 } 376 377 /** 378 * Set whether to order the parameters by object depth or not 379 * 380 * @param ordered True to order them 381 */ 382 public void setOrdered(boolean ordered) { 383 this.ordered = ordered; 384 } 385 386 /** 387 * Gets a set of regular expressions of parameters to remove 388 * from the parameter map 389 * 390 * @return A set of compiled regular expression patterns 391 */ 392 protected Set getExcludeParamsSet() { 393 return excludeParams; 394 } 395 396 /** 397 * Sets a comma-delimited list of regular expressions to match 398 * parameters that should be removed from the parameter map. 399 * 400 * @param commaDelim A comma-delimited list of regular expressions 401 */ 402 public void setExcludeParams(String commaDelim) { 403 Collection<String> excludePatterns = asCollection(commaDelim); 404 if (excludePatterns != null) { 405 excludeParams = new HashSet<Pattern>(); 406 for (String pattern : excludePatterns) { 407 excludeParams.add(Pattern.compile(pattern)); 408 } 409 } 410 } 411 412 /** 413 * Return a collection from the comma delimited String. 414 * 415 * @param commaDelim the comma delimited String. 416 * @return A collection from the comma delimited String. Returns <tt>null</tt> if the string is empty. 417 */ 418 private Collection<String> asCollection(String commaDelim) { 419 if (commaDelim == null || commaDelim.trim().length() == 0) { 420 return null; 421 } 422 return TextParseUtil.commaDelimitedStringToSet(commaDelim); 423 } 424 425 }