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 }