All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.helger.web.scope.impl.RequestWebScope Maven / Gradle / Ivy

There is a newer version: 10.1.9
Show newest version
/*
 * Copyright (C) 2014-2024 Philip Helger (www.helger.com)
 * philip[at]helger[dot]com
 *
 * Licensed 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 com.helger.web.scope.impl;

import java.text.Normalizer;
import java.time.LocalDateTime;
import java.util.Enumeration;

import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.Nonempty;
import com.helger.commons.annotation.OverrideOnDemand;
import com.helger.commons.annotation.ReturnsMutableObject;
import com.helger.commons.collection.attr.AttributeContainerAny;
import com.helger.commons.collection.attr.IAttributeContainerAny;
import com.helger.commons.collection.impl.CommonsLinkedHashSet;
import com.helger.commons.collection.impl.ICommonsOrderedSet;
import com.helger.commons.collection.iterate.EmptyEnumeration;
import com.helger.commons.concurrent.SimpleReadWriteLock;
import com.helger.commons.datetime.PDTFactory;
import com.helger.commons.http.HttpHeaderMap;
import com.helger.commons.id.factory.GlobalIDFactory;
import com.helger.commons.lang.ClassHelper;
import com.helger.commons.state.EChange;
import com.helger.commons.string.StringHelper;
import com.helger.commons.string.ToStringGenerator;
import com.helger.scope.AbstractScope;
import com.helger.scope.ScopeHelper;
import com.helger.scope.mgr.ScopeManager;
import com.helger.servlet.ServletContextPathHolder;
import com.helger.servlet.ServletSettings;
import com.helger.servlet.request.IRequestParamMap;
import com.helger.servlet.request.RequestHelper;
import com.helger.servlet.request.RequestParamMap;
import com.helger.web.fileupload.IFileItem;
import com.helger.web.scope.IRequestParamContainer;
import com.helger.web.scope.IRequestWebScope;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

/**
 * A request web scopes that does not parse multipart requests.
 *
 * @author Philip Helger
 */
public class RequestWebScope extends AbstractScope implements IRequestWebScope
{
  /**
   * Special implementation if {@link IRequestParamContainer} based on
   * {@link AttributeContainerAny}.
   *
   * @author Philip Helger
   */
  public static class ParamContainer extends AttributeContainerAny  implements IRequestParamContainer
  {}

  /**
   * The param value cleanser interface to be used globally.
   *
   * @author Philip Helger
   * @since 9.0.6
   */
  @FunctionalInterface
  public interface IParamValueCleanser
  {
    /**
     * Get the cleaned value of a parameter value.
     *
     * @param sParamName
     *        The current parameter name. May not be null.
     * @param nParamIndex
     *        The index of the value. If the parameter has multiple values this
     *        is respective index. If there is only one value, this is always 0
     *        (zero).
     * @param sParamValue
     *        The value to be cleaned. May be null.
     * @return The cleaned value. May also be null.
     */
    @Nullable
    String getCleanedValue (@Nonnull String sParamName, @Nonnegative int nParamIndex, @Nullable String sParamValue);
  }

  private static final Logger LOGGER = LoggerFactory.getLogger (RequestWebScope.class);
  private static final String REQUEST_ATTR_SCOPE_INITED = ScopeManager.SCOPE_ATTRIBUTE_PREFIX_INTERNAL +
                                                          "requestscope.inited";
  private static final SimpleReadWriteLock RW_LOCK = new SimpleReadWriteLock ();
  @GuardedBy ("RW_LOCK")
  private static IParamValueCleanser s_aParamValueCleanser = (n, i, v) -> getWithoutForbiddenCharsAndNormalized (v);

  private final LocalDateTime m_aCreationDT;
  protected final HttpServletRequest m_aHttpRequest;
  protected final HttpServletResponse m_aHttpResponse;
  private HttpHeaderMap m_aHeaders;
  private final ParamContainer m_aParams = new ParamContainer ();
  private IRequestParamMap m_aRequestParamMap;

  /**
   * @return The current value cleanser function. May be null. By
   *         default {@link #getWithoutForbiddenCharsAndNormalized(String)} is
   *         invoked.
   * @since 9.0.6
   * @see #setParamValueCleanser(IParamValueCleanser)
   */
  @Nullable
  public static IParamValueCleanser getParamValueCleanser ()
  {
    return RW_LOCK.readLockedGet ( () -> s_aParamValueCleanser);
  }

  /**
   * Set the param value cleanser function that is applied on all parameter
   * values. By default only
   * {@link #getWithoutForbiddenCharsAndNormalized(String)} is invoked.
   *
   * @param aParamValueCleanser
   *        The function to be applied. May be null. The function
   *        itself must be able to handle null values.
   * @since 9.0.6
   * @see #getParamValueCleanser()
   */
  public static void setParamValueCleanser (@Nullable final IParamValueCleanser aParamValueCleanser)
  {
    RW_LOCK.writeLocked ( () -> s_aParamValueCleanser = aParamValueCleanser);
  }

  @Nonnull
  @Nonempty
  private static String _createScopeID (@Nonnull final HttpServletRequest aHttpRequest)
  {
    ValueEnforcer.notNull (aHttpRequest, "HttpRequest");

    return GlobalIDFactory.getNewIntID () + "@" + RequestHelper.getRequestURIDecoded (aHttpRequest);
  }

  public RequestWebScope (@Nonnull final HttpServletRequest aHttpRequest,
                          @Nonnull final HttpServletResponse aHttpResponse)
  {
    super (_createScopeID (aHttpRequest));

    m_aCreationDT = PDTFactory.getCurrentLocalDateTime ();
    m_aHttpRequest = aHttpRequest;
    m_aHttpResponse = ValueEnforcer.notNull (aHttpResponse, "HttpResponse");

    // done initialization
    if (ScopeHelper.isDebugRequestScopeLifeCycle ())
      LOGGER.info ("Created request web scope '" +
                   super.getID () +
                   "' of class " +
                   ClassHelper.getClassLocalName (RequestWebScope.class),
                   ScopeHelper.getDebugException ());
  }

  @Nonnull
  public final LocalDateTime getCreationDateTime ()
  {
    return m_aCreationDT;
  }

  /**
   * Callback method to add special parameters.
   *
   * @return {@link EChange#CHANGED} if some attributes were added,
   *         false if not. If special attributes were added,
   *         existing attributes are kept and will not be overwritten with HTTP
   *         servlet request parameters!
   */
  @OverrideOnDemand
  @Nonnull
  protected EChange addSpecialRequestParams ()
  {
    return EChange.UNCHANGED;
  }

  /**
   * Check if the provided char is forbidden in a request value or not.
   *
   * @param c
   *        Char to check
   * @return true if it is forbidden, false if not.
   * @see #getWithoutForbiddenChars(String)
   * @since 9.0.6
   */
  public static boolean isForbiddenParamValueChar (final char c)
  {
    // INVALID_VALUE_CHAR_XML10 + 0x7f
    return (c >= 0x0 && c <= 0x8) ||
           (c >= 0xb && c <= 0xc) ||
           (c >= 0xe && c <= 0x1f) ||
           (c == 0x7f) ||
           // Surrogate chars
           // (c >= 0xd800 && c <= 0xdfff) ||
           (c >= 0xfffe && c <= 0xffff);
  }

  /**
   * Remove all chars from the input that cannot be serialized as XML.
   *
   * @param s
   *        The source value. May be null.
   * @return null if the source value is null.
   * @see #isForbiddenParamValueChar(char)
   * @since 9.0.4
   */
  @Nullable
  public static String getWithoutForbiddenChars (@Nullable final String s)
  {
    if (s == null)
      return null;

    final StringBuilder aCleanValue = new StringBuilder (s.length ());
    int nForbidden = 0;

    final ICommonsOrderedSet  aInvalidChars = new CommonsLinkedHashSet <> ();
    for (final char c : s.toCharArray ())
      if (isForbiddenParamValueChar (c))
      {
        nForbidden++;
        aInvalidChars.add (Character.valueOf (c));
      }
      else
        aCleanValue.append (c);
    if (nForbidden == 0)
    {
      // Return "as-is"
      return s;
    }
    LOGGER.warn ("Removed " +
                 nForbidden +
                 " forbidden character(s) from a request parameter value! Invalid chars are: " +
                 StringHelper.imploder ()
                             .separator (", ")
                             .source (aInvalidChars,
                                      x -> "0x" + StringHelper.getHexStringLeadingZero (x.charValue (), 2))
                             .build ());

    return aCleanValue.toString ();
  }

  /**
   * First normalize the input according to Unicode rules, so that "O 0xcc 0x88"
   * (O with COMBINING DIAERESIS) becomes "Ö" (Capital O with umlaut).
   *
   * @param s
   *        Source string. May be null.
   * @return null if the source string is null.
   * @see Normalizer#normalize(CharSequence, java.text.Normalizer.Form)
   * @see #getWithoutForbiddenChars(String)
   * @since 9.0.6
   */
  @Nullable
  public static String getWithoutForbiddenCharsAndNormalized (@Nullable final String s)
  {
    if (s == null)
      return null;

    // Removed forbidden chars first
    final String sValue = getWithoutForbiddenChars (s);
    if (sValue == null)
      return null;

    // than normalize
    return Normalizer.normalize (sValue, Normalizer.Form.NFKC);
  }

  public final void initScope ()
  {
    // Avoid double initialization of a scope, because for file uploads, the
    // parameters can only be extracted once!
    // As the parameters are stored directly in the HTTP request, we're not
    // loosing any data here!
    final IAttributeContainerAny  aAttrs = attrs ();
    if (aAttrs.getAndSetFlag (REQUEST_ATTR_SCOPE_INITED))
    {
      LOGGER.warn ("Scope was already inited: " + toString ());
      return;
    }
    final IRequestParamContainer aParams = params ();

    // where some extra items (like file items) handled?
    final boolean bAddedSpecialRequestParams = addSpecialRequestParams ().isChanged ();

    // Retrieve once (because locked)
    final IParamValueCleanser aParamValueCleanser = getParamValueCleanser ();

    // set parameters as attributes (handles GET and POST parameters)
    // This may throw an exception, if the payload is invalid
    Enumeration  aEnum;
    try
    {
      aEnum = m_aHttpRequest.getParameterNames ();
    }
    catch (final Exception ex)
    {
      aEnum = new EmptyEnumeration <> ();
    }
    while (aEnum.hasMoreElements ())
    {
      final String sParamName = aEnum.nextElement ();

      // Avoid double setting a parameter!
      // If an existing file item with this name is present, don't parse it as a
      // String again
      if (bAddedSpecialRequestParams && aParams.containsKey (sParamName))
        continue;

      // Check if it is a single value or not
      final String [] aParamValues = m_aHttpRequest.getParameterValues (sParamName);
      final int nParamValues = aParamValues.length;
      if (nParamValues == 1)
      {
        // Convert from String[] to String

        String sValue = aParamValues[0];
        if (aParamValueCleanser != null)
        {
          // Adopt value if needed
          sValue = aParamValueCleanser.getCleanedValue (sParamName, 0, sValue);
        }
        aParams.putIn (sParamName, sValue);
      }
      else
      {
        // Use String[] as is
        final String [] aPreProcessedValues = new String [nParamValues];
        for (int i = 0; i < nParamValues; ++i)
        {
          String sValue = aParamValues[i];
          if (aParamValueCleanser != null)
          {
            // Adopt value if needed
            sValue = aParamValueCleanser.getCleanedValue (sParamName, i, sValue);
          }
          aPreProcessedValues[i] = sValue;
        }
        aParams.putIn (sParamName, aPreProcessedValues);
      }
    }
    // done initialization
    if (ScopeHelper.isDebugRequestScopeLifeCycle ())
      LOGGER.info ("Initialized request web scope '" + getID () + "' of class " + ClassHelper.getClassLocalName (this),
                   ScopeHelper.getDebugException ());
  }

  @Override
  protected void postDestroy ()
  {
    // Delete all temporary files (if any)
    for (final Object o : params ().values ())
      if (o instanceof IFileItem)
        ((IFileItem) o).onEndOfRequest ();

    if (ScopeHelper.isDebugRequestScopeLifeCycle ())
      LOGGER.info ("Destroyed request web scope '" + getID () + "' of class " + ClassHelper.getClassLocalName (this),
                   ScopeHelper.getDebugException ());
  }

  @Nonnull
  public final HttpHeaderMap headers ()
  {
    HttpHeaderMap ret = m_aHeaders;
    if (ret == null)
      ret = m_aHeaders = RequestHelper.getRequestHeaderMap (m_aHttpRequest);
    return ret;
  }

  @Nonnull
  @ReturnsMutableObject
  public final IRequestParamContainer params ()
  {
    return m_aParams;
  }

  @Nonnull
  public final IRequestParamMap getRequestParamMap ()
  {
    IRequestParamMap ret = m_aRequestParamMap;
    if (ret == null)
      ret = m_aRequestParamMap = RequestParamMap.create (params ());
    return ret;
  }

  /**
   * This is a heuristic method to determine whether a request is for a file
   * (e.g. x.jsp) or for a servlet. This method return true if the
   * last dot is after the last slash
   *
   * @param sServletPath
   *        The non-null servlet path to check
   * @return true if it is assumed that the request is file based,
   *         false if it can be assumed to be a regular servlet.
   */
  public static boolean isFileBasedRequest (@Nonnull final String sServletPath)
  {
    final int nLastDot = sServletPath.lastIndexOf ('.');
    if (nLastDot < 0)
      return false;
    final int nLastSlash = sServletPath.lastIndexOf ('/');
    if (nLastSlash < 0)
    {
      // for e.g. "abc.def"
      return true;
    }
    // true for e.g. "/path/paths/abc.def"
    // false for e.g. "/path/pa.th/def"
    return nLastDot > nLastSlash;
  }

  /**
   * @return Returns the portion of the request URI that indicates the context
   *         of the request. The context path always comes first in a request
   *         URI. The path starts with a "/" character but does not end with a
   *         "/" character. For servlets in the default (root) context, this
   *         method returns "". The container does not decode this string. E.g.
   *         /context or an empty string for the root context.
   *         Never with a trailing slash.
   * @see #getFullContextPath()
   */
  @Nonnull
  public String getContextPath ()
  {
    // Always use the context path from the global web scope because it can be
    // customized!
    return ServletContextPathHolder.getContextPath ();
  }

  @Nonnull
  public String getContextAndServletPath ()
  {
    final String sServletPath = getServletPath ();
    if (isFileBasedRequest (sServletPath))
      return getContextPath () + sServletPath;
    // For servlets that are not files, we need to append a trailing slash
    return getContextPath () + sServletPath + '/';
  }

  @Nonnull
  public String getFullContextAndServletPath ()
  {
    final String sServletPath = getServletPath ();
    if (isFileBasedRequest (sServletPath))
      return getFullContextPath () + sServletPath;
    // For servlets, we need to append a trailing slash
    return getFullContextPath () + sServletPath + '/';
  }

  @Nonnull
  public final HttpServletRequest getRequest ()
  {
    return m_aHttpRequest;
  }

  @Nonnull
  public final HttpServletResponse getResponse ()
  {
    return m_aHttpResponse;
  }

  @Nonnull
  public String encodeURL (@Nonnull final String sURL)
  {
    if (ServletSettings.isEncodeURLs ())
      return getResponse ().encodeURL (sURL);
    // Return "as-is"
    return sURL;
  }

  @Nonnull
  public String encodeRedirectURL (@Nonnull final String sURL)
  {
    if (ServletSettings.isEncodeURLs ())
      return getResponse ().encodeRedirectURL (sURL);
    // Return "as-is"
    return sURL;
  }

  @Override
  public boolean equals (final Object o)
  {
    if (o == this)
      return true;
    return super.equals (o);
  }

  @Override
  public int hashCode ()
  {
    return super.hashCode ();
  }

  @Override
  public String toString ()
  {
    return ToStringGenerator.getDerived (super.toString ())
                            .append ("HttpRequest", m_aHttpRequest)
                            .append ("HttpResponse", m_aHttpResponse)
                            .append ("Headers", m_aHeaders)
                            .append ("Params", m_aParams)
                            .append ("RequestParamMap", m_aRequestParamMap)
                            .getToString ();
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy