Component.java
/*
* 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.struts2.components;
import org.apache.struts2.ActionContext;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.security.NotExcludedAcceptedPatternsChecker;
import org.apache.struts2.util.TextParseUtil;
import org.apache.struts2.util.ValueStack;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.MethodUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.StrutsException;
import org.apache.struts2.dispatcher.mapper.ActionMapper;
import org.apache.struts2.dispatcher.mapper.ActionMapping;
import org.apache.struts2.util.ComponentUtils;
import org.apache.struts2.util.FastByteArrayOutputStream;
import org.apache.struts2.views.annotations.StrutsTagAttribute;
import org.apache.struts2.views.util.UrlHelper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Stack;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* Base class to extend for UI components.
* <br>
* This class is a good extension point when building reusable UI components.
*/
public class Component {
private static final Logger LOG = LogManager.getLogger(Component.class);
public static final String COMPONENT_STACK = "__component_stack";
/**
* Caches information about common tag's attributes to reduce scanning for annotation @StrutsTagAttribute
*/
protected static ConcurrentMap<Class<?>, Collection<String>> standardAttributesMap = new ConcurrentHashMap<>();
protected boolean devMode = false;
protected boolean escapeHtmlBody = false;
protected ValueStack stack;
protected Map<String, Object> attributes;
protected ActionMapper actionMapper;
protected boolean throwExceptionOnELFailure;
protected boolean performClearTagStateForTagPoolingServers = false;
private UrlHelper urlHelper;
private NotExcludedAcceptedPatternsChecker notExcludedAcceptedPatterns;
/**
* Constructor.
*
* @param stack OGNL value stack.
*/
public Component(ValueStack stack) {
this.stack = stack;
this.attributes = new LinkedHashMap<>();
getComponentStack().push(this);
}
/**
* Gets the name of this component.
*
* @return the name of this component.
*/
private String getComponentName() {
Class<?> c = getClass();
String name = c.getName();
int dot = name.lastIndexOf('.');
return name.substring(dot + 1).toLowerCase();
}
@Inject(value = StrutsConstants.STRUTS_DEVMODE, required = false)
public void setDevMode(String devMode) {
this.devMode = BooleanUtils.toBoolean(devMode);
}
@Inject
public void setActionMapper(ActionMapper mapper) {
this.actionMapper = mapper;
}
@Inject(StrutsConstants.STRUTS_EL_THROW_EXCEPTION)
public void setThrowExceptionsOnELFailure(String throwException) {
this.throwExceptionOnELFailure = BooleanUtils.toBoolean(throwException);
}
@Inject(value = StrutsConstants.STRUTS_UI_ESCAPE_HTML_BODY, required = false)
public void setEscapeHtmlBody(String escapeHtmlBody) {
this.escapeHtmlBody = BooleanUtils.toBoolean(escapeHtmlBody);
}
@Inject
public void setUrlHelper(UrlHelper urlHelper) {
this.urlHelper = urlHelper;
}
@Inject
public void setNotExcludedAcceptedPatterns(NotExcludedAcceptedPatternsChecker notExcludedAcceptedPatterns) {
this.notExcludedAcceptedPatterns = notExcludedAcceptedPatterns;
}
/**
* Gets the OGNL value stack associated with this component.
*
* @return the OGNL value stack associated with this component.
*/
public ValueStack getStack() {
return stack;
}
/**
* Gets the component stack of this component.
*
* @return the component stack of this component, never <tt>null</tt>.
*/
public Stack<Component> getComponentStack() {
Stack<Component> componentStack = (Stack<Component>) stack.getContext().get(COMPONENT_STACK);
if (componentStack == null) {
componentStack = new Stack<>();
stack.getContext().put(COMPONENT_STACK, componentStack);
}
return componentStack;
}
/**
* Callback for the start tag of this component.
* Should the body be evaluated?
*
* @param writer the output writer.
* @return true if the body should be evaluated
*/
public boolean start(Writer writer) {
return true;
}
/**
* Callback for the end tag of this component.
* Should the body be evaluated again?
* <br>
* <b>NOTE:</b> will pop component stack.
*
* @param writer the output writer.
* @param body the rendered body.
* @return true if the body should be evaluated again
*/
public boolean end(Writer writer, String body) {
return end(writer, body, true);
}
/**
* Callback for the start tag of this component.
* Should the body be evaluated again?
* <br>
* <b>NOTE:</b> has a parameter to determine to pop the component stack.
*
* @param writer the output writer.
* @param body the rendered body.
* @param popComponentStack should the component stack be popped?
* @return true if the body should be evaluated again
*/
protected boolean end(Writer writer, String body, boolean popComponentStack) {
assert (body != null);
try {
writer.write(body);
} catch (IOException e) {
throw new StrutsException("IOError while writing the body: " + e.getMessage(), e);
}
if (popComponentStack) {
popComponentStack();
}
return false;
}
/**
* Pops the component stack.
*/
protected void popComponentStack() {
getComponentStack().pop();
}
/**
* Finds the nearest ancestor of this component stack.
*
* @param clazz the class to look for, or if assignable from.
* @return the component if found, <tt>null</tt> if not.
*/
protected Component findAncestor(Class<?> clazz) {
Stack<Component> componentStack = getComponentStack();
int currPosition = componentStack.search(this);
if (currPosition >= 0) {
int start = componentStack.size() - currPosition - 1;
for (int i = start; i >= 0; i--) {
Component component = componentStack.get(i);
if (clazz.isAssignableFrom(component.getClass()) && component != this) {
return component;
}
}
}
return null;
}
/**
* Evaluates the OGNL stack to find a String value.
*
* @param expr OGNL expression.
* @return the String value found.
*/
protected String findString(String expr) {
return (String) findValue(expr, String.class);
}
/**
* Evaluates the OGNL stack to find a String value.
* <br>
* If the given expression is <tt>null</tt> a error is logged and a <code>RuntimeException</code> is thrown
* constructed with a messaged based on the given field and errorMsg parameter.
*
* @param expr OGNL expression.
* @param field field name used when throwing <code>RuntimeException</code>.
* @param errorMsg error message used when throwing <code>RuntimeException</code>.
* @return the String value found.
* @throws StrutsException is thrown in case of expression is null.
*/
protected String findString(String expr, String field, String errorMsg) {
if (expr == null) {
throw fieldError(field, errorMsg, null);
} else {
return findString(expr);
}
}
/**
* Constructs a <code>RuntimeException</code> based on the given information.
* <br>
* A message is constructed and logged at ERROR level before being returned
* as a <code>RuntimeException</code>.
*
* @param field field name used when throwing <code>RuntimeException</code>.
* @param errorMsg error message used when throwing <code>RuntimeException</code>.
* @param e the caused exception, can be <tt>null</tt>.
* @return the constructed <code>StrutsException</code>.
*/
protected StrutsException fieldError(String field, String errorMsg, Exception e) {
String msg = "tag '" + getComponentName() + "', field '" + field +
(attributes != null && attributes.containsKey("name") ? "', name '" + attributes.get("name") : "") +
"': " + errorMsg;
throw new StrutsException(msg, e);
}
/**
* Finds a value from the OGNL stack based on the given expression.
* Will always evaluate <code>expr</code> against stack except when <code>expr</code>
* is null. If %{...} is applied, simply strip it off.
*
* @param expression the expression. Returns <tt>null</tt> if expr is null.
* @return the value, <tt>null</tt> if not found.
*/
protected Object findValue(String expression) {
if (expression == null) {
return null;
}
expression = stripExpression(expression);
return getStack().findValue(expression, throwExceptionOnELFailure);
}
/**
* If %{...} is applied, simply strip the "%{" and "}" off.
*
* @param expression the expression (must be not null)
* @return the stripped expression
*/
protected String stripExpression(String expression) {
return ComponentUtils.stripExpression(expression);
}
/**
* Adds the surrounding %{ } to the expression for proper processing.
*
* @param expr the expression.
* @return the modified expression wrapped with %{...}
*/
protected String completeExpression(String expr) {
if (expr == null) {
return null;
}
if (ComponentUtils.isExpression(expr)) {
LOG.warn("Expression {} is already an expression!", expr);
return expr;
}
return "%{" + expr + "}";
}
/**
* <p>
* Evaluates the OGNL stack to find an Object value.
* </p>
*
* <p>
* Function just like <code>findValue(String)</code> except that if the
* given expression is <tt>null</tt> a error is logged and
* a <code>RuntimeException</code> is thrown constructed with a
* messaged based on the given field and errorMsg parameter.
* </p>
*
* @param expr OGNL expression.
* @param field field name used when throwing <code>RuntimeException</code>.
* @param errorMsg error message used when throwing <code>RuntimeException</code>.
* @return the Object found, is never <tt>null</tt>.
* @throws StrutsException is thrown in case of not found in the OGNL stack, or expression is <tt>null</tt>.
*/
protected Object findValue(String expr, String field, String errorMsg) {
if (expr == null) {
throw fieldError(field, errorMsg, null);
} else {
Object value = null;
Exception problem = null;
try {
value = findValue(expr);
} catch (Exception e) {
problem = e;
}
if (value == null) {
throw fieldError(field, errorMsg, problem);
}
return value;
}
}
/**
* Evaluates the OGNL stack to find an Object of the given type. Will evaluate
* <code>expression</code> the portion wrapped with %{...} against stack if
* evaluating to String.class, else the whole <code>expression</code> is evaluated
* against the stack.
*
* @param expression OGNL expression.
* @param toType the type expected to find.
* @return the Object found, or <tt>null</tt> if not found.
*/
protected Object findValue(String expression, Class<?> toType) {
if (toType == String.class) {
if (ComponentUtils.containsExpression(expression)) {
return TextParseUtil.translateVariables('%', expression, stack);
} else {
return expression;
}
} else {
String strippedExpression = stripExpression(expression);
return getStack().findValue(strippedExpression, toType, throwExceptionOnELFailure);
}
}
/**
* Renders an action URL by consulting the {@link org.apache.struts2.dispatcher.mapper.ActionMapper}.
*
* @param action the action
* @param namespace the namespace
* @param method the method
* @param req HTTP request
* @param res HTTP response
* @param parameters parameters
* @param scheme http or https
* @param includeContext should the context path be included or not
* @param encodeResult should the url be encoded
* @param forceAddSchemeHostAndPort should the scheme host and port be forced
* @param escapeAmp should ampersand (&) be escaped to &amp;
* @return the action url.
*/
protected String determineActionURL(String action, String namespace, String method,
HttpServletRequest req, HttpServletResponse res, Map<String, Object> parameters, String scheme,
boolean includeContext, boolean encodeResult, boolean forceAddSchemeHostAndPort,
boolean escapeAmp) {
String finalAction = findString(action);
String finalMethod = method != null ? findString(method) : null;
String finalNamespace = determineNamespace(namespace, getStack(), req);
ActionMapping mapping = new ActionMapping(finalAction, finalNamespace, finalMethod, parameters);
String uri = actionMapper.getUriFromActionMapping(mapping);
return urlHelper.buildUrl(uri, req, res, parameters, scheme, includeContext, encodeResult, forceAddSchemeHostAndPort, escapeAmp);
}
/**
* Determines the namespace of the current page being renderdd. Useful for Form, URL, and href generations.
*
* @param namespace the namespace
* @param stack OGNL value stack
* @param req HTTP request
* @return the namepsace of the current page being rendered, is never <tt>null</tt>.
*/
protected String determineNamespace(String namespace, ValueStack stack, HttpServletRequest req) {
String result;
if (namespace == null) {
result = getNamespace(stack);
} else {
result = findString(namespace);
}
if (result == null) {
result = "";
}
return result;
}
protected String getNamespace(ValueStack stack) {
ActionContext context = ActionContext.of(stack.getContext());
ActionInvocation invocation = context.getActionInvocation();
return invocation.getProxy().getNamespace();
}
/**
* Pushes this component's parameter Map as well as the component itself on to the stack
* and then copies the supplied parameters over. Because the component's parameter Map is
* pushed before the component itself, any key-value pair that can't be assigned to component
* will be set in the parameters Map.
*
* @param attributesToCopy the attributes to copy.
*/
public void copyAttributes(Map<String, Object> attributesToCopy) {
stack.push(attributes);
stack.push(this);
try {
for (Map.Entry<String, Object> entry : attributesToCopy.entrySet()) {
String key = entry.getKey();
if (key.indexOf('-') >= 0) {
// UI component attributes may contain hypens (e.g. data-ajax), but ognl
// can't handle that, and there can't be a component property with a hypen
// so into the attributes map it goes. See WW-4493
attributes.put(key, entry.getValue());
} else {
stack.setValue(key, entry.getValue());
}
}
} finally {
stack.pop();
stack.pop();
}
}
/**
* Constructs a string representation of the given exception.
*
* @param t the exception
* @return the exception as a string.
*/
protected String toString(Throwable t) {
try (FastByteArrayOutputStream bout = new FastByteArrayOutputStream();
PrintWriter wrt = new PrintWriter(bout)) {
t.printStackTrace(wrt);
return bout.toString();
}
}
/**
* Gets the attributes.
*
* @return the attributes. It's never <tt>null</tt>.
*/
public Map<String, Object> getAttributes() {
return attributes;
}
/**
* Adds all the given attributes to this component's own attributes.
*
* @param additionalAttributes the attributes to add.
*/
public void addAllAttributes(Map<String, Object> additionalAttributes) {
attributes.putAll(additionalAttributes);
}
/**
* Adds the given key and value to this component's own parameter.
* <br>
* If the provided key is <tt>null</tt> nothing happens.
* If the provided value is <tt>null</tt> any existing parameter with
* the given key name is removed.
*
* @param key the key of the new parameter to add.
* @param value the value associated with the key.
*/
public void addParameter(String key, Object value) {
if (key != null) {
Map<String, Object> params = getAttributes();
if (value == null) {
params.remove(key);
} else {
params.put(key, value);
}
}
}
/**
* Overwrite to set if body should be used.
*
* @return always false for this component.
*/
public boolean usesBody() {
return false;
}
/**
* Override to set if body content should be HTML-escaped.
*
* @return always true (default) for this component.
* @since 6.0.0
*/
public boolean escapeHtmlBody() {
return escapeHtmlBody;
}
/**
* Checks if provided name is a valid tag's attribute
*
* @param attrName String name of attribute
* @return true if attribute with the same name was already defined
*/
public boolean isValidTagAttribute(String attrName) {
return getStandardAttributes().contains(attrName);
}
/**
* If needed caches all methods annotated by given annotation to avoid further scans
*
* @return list of attributes
*/
protected Collection<String> getStandardAttributes() {
Class<?> clz = getClass();
Collection<String> standardAttributes = standardAttributesMap.get(clz);
if (standardAttributes == null) {
Collection<Method> methods = MethodUtils.getMethodsListWithAnnotation(clz, StrutsTagAttribute.class,
true, true);
standardAttributes = new HashSet<>(methods.size());
for (Method m : methods) {
standardAttributes.add(StringUtils.uncapitalize(m.getName().substring(3)));
}
standardAttributesMap.putIfAbsent(clz, standardAttributes);
}
return standardAttributes;
}
/**
* Request that the tag state be cleared during {@link org.apache.struts2.views.jsp.StrutsBodyTagSupport#doEndTag()} processing,
* which may help with certain edge cases with tag logic running on servers that implement JSP Tag Pooling.
*
* <em>Note:</em> All Tag classes that extend {@link org.apache.struts2.views.jsp.StrutsBodyTagSupport} must implement a setter for
* this attribute (same name), and it must be defined at the Tag class level.
* Defining a setter in the superclass alone is insufficient (results in "Cannot find a setter method for the attribute").
* <p>
* See {@link org.apache.struts2.views.jsp.StrutsBodyTagSupport#clearTagStateForTagPoolingServers() for additional details.
*
* @param performClearTagStateForTagPoolingServers true if tag state should be cleared, false otherwise.
*/
@StrutsTagAttribute(description = "Whether to clear all tag state during doEndTag() processing (if applicable)", type = "Boolean", defaultValue = "false")
public void setPerformClearTagStateForTagPoolingServers(boolean performClearTagStateForTagPoolingServers) {
this.performClearTagStateForTagPoolingServers = performClearTagStateForTagPoolingServers;
}
public boolean getPerformClearTagStateForTagPoolingServers() {
return this.performClearTagStateForTagPoolingServers;
}
/**
* Checks if expression doesn't contain vulnerable code
*
* @param expression of the component
* @return true|false
* @since 6.0.0
*/
protected boolean isAcceptableExpression(String expression) {
NotExcludedAcceptedPatternsChecker.IsAllowed isAllowed = notExcludedAcceptedPatterns.isAllowed(expression);
if (isAllowed.isAllowed()) {
return true;
}
LOG.warn("Expression [{}] isn't allowed by pattern [{}]! See Accepted / Excluded patterns at\n" +
"https://struts.apache.org/security/", expression, isAllowed.getAllowedPattern());
return false;
}
}