// **** GENERATED CODE, DO NOT MODIFY ****
// This file was generated via preprocessing from input:
// java/com/google/common/html/types/SafeHtmls.java.tpl
// ***************************************
/*
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * 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.google.common.html.types;

import static com.google.common.html.types.BuilderUtils.coerceToInterchangeValid;
import static com.google.common.html.types.BuilderUtils.escapeHtmlInternal;

import com.google.common.annotations.GwtCompatible;
import com.google.common.base.Preconditions;
import java.util.Arrays;
import javax.annotation.CheckReturnValue;

/**
 * Protocol conversions, builders and factory methods for {@link SafeHtml}.
 */
@CheckReturnValue
@GwtCompatible
public final class SafeHtmls {

  /**
   * Deserializes a SafeHtmlProto into a SafeHtml instance.
   *
   *
Protocol-message forms are intended to be opaque. The fields of the protocol message should
   * be considered encapsulated and are not intended for direct inspection or manipulation. Protocol
   * message forms of this type should be produced by {@link #toProto(SafeHtml)} or its
   * equivalent in other implementation languages.
   *
   *
Important: It is unsafe to invoke this method on a protocol message that has been
   * received from an entity outside the application's trust domain. Data coming from the browser
   * is outside the application's trust domain.
   */
  public static SafeHtml fromProto(SafeHtmlProto proto) {
    return create(proto.getPrivateDoNotAccessOrElseSafeHtmlWrappedValue());
  }

  /**
   * Wraps a SafeScript inside a <script type="text/javascript"> tag.
   */
  public static SafeHtml fromScript(SafeScript script) {
    return create("");
  }

  /**
   * Wraps a SafeScript inside a <script type="text/javascript"> tag.
   * The tag has a nonce attribute populated from the provided CSP nonce value.
   */
  public static SafeHtml fromScriptWithCspNonce(SafeScript script, String cspNonce) {
    return create("");
  }

  /**
   * Creates a <script type="text/javascript" src="url"><script> where the
   * {@code src} attribute points to the given {@code trustedResourceUrl}.
   */
  public static SafeHtml fromScriptUrl(TrustedResourceUrl trustedResourceUrl) {
    String escapedUrl = htmlEscapeInternal(trustedResourceUrl.getTrustedResourceUrlString());
    return create("");
  }

  /**
   * Creates a <script defer type="text/javascript" src="url"><script> where the
   * {@code src} attribute points to the given {@code trustedResourceUrl}.
   */
  public static SafeHtml fromScriptUrlDeferred(TrustedResourceUrl trustedResourceUrl) {
    String escapedUrl = htmlEscapeInternal(trustedResourceUrl.getTrustedResourceUrlString());
    return create("");
  }

  /**
   * Creates a <script type="text/javascript" src="url"><script> where the
   * {@code src} attribute points to the given {@code trustedResourceUrl}.
   * The tag has a nonce attribute populated from the provided CSP nonce value.
   */
  public static SafeHtml fromScriptUrlWithCspNonce(TrustedResourceUrl trustedResourceUrl,
      String cspNonce) {
    String escapedUrl = htmlEscapeInternal(trustedResourceUrl.getTrustedResourceUrlString());
    return create("");
  }

  /**
   * Creates a <script defer type="text/javascript" src="url"><script> where the
   * {@code src} attribute points to the given {@code trustedResourceUrl}.
   * The tag has a nonce attribute populated from the provided CSP nonce value.
   */
  public static SafeHtml fromScriptUrlWithCspNonceDeferred(TrustedResourceUrl trustedResourceUrl,
      String cspNonce) {
    String escapedUrl = htmlEscapeInternal(trustedResourceUrl.getTrustedResourceUrlString());
    return create("");
  }

  /**
   * Wraps a SafeStyleSheet inside a <style type="text/css"> tag.
   */
  public static SafeHtml fromStyleSheet(SafeStyleSheet safeStyleSheet) {
    Preconditions.checkArgument(!safeStyleSheet.getSafeStyleSheetString().contains("<"));
    return create("");
  }

  /**
   * Creates a <style type="text/css" src="url"><style> where the
   * {@code src} attribute points to the given {@code trustedResourceUrl}.
   */
  public static SafeHtml fromStyleUrl(TrustedResourceUrl trustedResourceUrl) {
    String escapedUrl = htmlEscapeInternal(trustedResourceUrl.getTrustedResourceUrlString());
    return create("");
  }

  /**
   * Serializes a SafeHtml into its opaque protocol message representation.
   *
   *
Protocol message forms of this type are intended to be opaque. The fields of the returned
   * protocol message should be considered encapsulated and are not intended for direct inspection
   * or manipulation. Protocol messages can be converted back into a SafeHtml using
   * {@link #fromProto(SafeHtmlProto)}.
   */
  public static SafeHtmlProto toProto(SafeHtml safeHtml) {
    return SafeHtmlProto.newBuilder().setPrivateDoNotAccessOrElseSafeHtmlWrappedValue(
        safeHtml.getSafeHtmlString()).build();
  }

  /**
   * Converts, by HTML-escaping, an arbitrary string into a contract-compliant {@link SafeHtml}.
   */
  public static SafeHtml htmlEscape(String text) {
    return create(htmlEscapeInternal(text));
  }

  /**
   * Converts an arbitrary string into an HTML comment by HTML-escaping the contents and embedding
   * the result between HTML comment markers.
   *
   *
Escaping is needed because Internet Explorer supports conditional comments and so may render
   * HTML markup within comments.
   */
   public static SafeHtml comment(String text) {
     return create("");
   }

  /**
   * Creates a new SafeHtml which contains, in order, the string representations of the given
   * {@code htmls}.
   */
  public static SafeHtml concat(SafeHtml... htmls) {
    return concat(Arrays.asList(htmls));
  }

  /**
   * Creates a new SafeHtml which contains, in order, the string representations of the given
   * {@code htmls}.
   */
  public static SafeHtml concat(Iterable<SafeHtml> htmls) {
    int concatLength = 0;
    for (SafeHtml html : htmls) {
      concatLength += html.getSafeHtmlString().length();
    }

    StringBuilder result = new StringBuilder(concatLength);
    for (SafeHtml html : htmls) {
      result.append(html.getSafeHtmlString());
    }
    return create(result.toString());
  }

  // Default visibility for use by SafeHtmlBuilder.
  static SafeHtml create(String html) {
    return new SafeHtml(html);
  }

  private static String htmlEscapeInternal(String text) {
    return escapeHtmlInternal(coerceToInterchangeValid(text));
  }

  private SafeHtmls() { }
}


// **** GENERATED CODE, DO NOT MODIFY ****
// This file was generated via preprocessing from input:
// java/com/google/common/html/types/SafeHtmlBuilder.java.tpl
// Please make changes to that file and run
// java/com/google/common/html/types/gen_srcs.sh
// to regenerate the .java files.
// ***************************************
/*
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * 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.google.common.html.types;

import static com.google.common.html.types.BuilderUtils.coerceToInterchangeValid;
import static com.google.common.html.types.BuilderUtils.escapeHtmlInternal;

import com.google.common.annotations.GwtCompatible;
import com.google.errorprone.annotations.CompileTimeConstant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import javax.annotation.CheckReturnValue;
import javax.annotation.Generated;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;

/**
 * Builder for HTML elements which conform to the {@link SafeHtml} contract. Supports setting
 * element name, individual attributes and content.
 *
 *
While this builder disallows some invalid HTML constructs (for example, void elements with
 * content) it does not guarantee well-formed HTML (for example, attribute values are not strictly
 * enforced if they pose no security risk). A large number of runtime failures are possible and it
 * is therefore recommended to thoroughly unit test code using this builder.
 */
@Generated(value = "//java/com/google/common/html/types:gen_srcs.sh")
@GwtCompatible
@NotThreadSafe
public final class SafeHtmlBuilder {
  // Keep these regular expressions compatible across Java and JavaScript native implementations.
  // They are uncompiled because we couldn't depend on java.util.regex.Pattern or
  // com.google.gwt.regexp.shared.RegExp
  private static final String VALID_ELEMENT_NAMES_REGEXP = "[a-z0-9-]+";
  private static final String VALID_DATA_ATTRIBUTES_REGEXP = "data-[a-zA-Z-]+";

  private static final Set<String> UNSUPPORTED_ELEMENTS =
      createUnmodifiableSet(
          "applet",
          "base",
          "embed",
          "math",
          "meta",
          "object",
          "script",
          "style",
          "svg",
          "template");

  private static final Set<String> VOID_ELEMENTS =
      createUnmodifiableSet(
          "area", "br", "col", "hr", "img", "input", "keygen", "link", "param", "source", "track",
          "wbr");

  private final String elementName;
  /** We use LinkedHashMap to maintain attribute insertion order. */
  private final Map<String, String> attributes = new LinkedHashMap<>();

  private final List<SafeHtml> contents = new ArrayList<>();

  private boolean useSlashOnVoid = false;

  private enum AttributeContract {
    SAFE_URL,
    TRUSTED_RESOURCE_URL
  }

  /** Contract of the value currently assigned to the {@code href} attribute. */
  private AttributeContract hrefValueContract = AttributeContract.TRUSTED_RESOURCE_URL;

  /**
   * Creates a builder for the given {@code elementName}, which must consist only of lowercase
   * letters, digits and {@code -}.
   *
   *
If {@code elementName} is not a void element then the string representation of the builder
   * is {@code <elementName[optional attributes]>[optional content]</elementName>}. If {@code
   * elementName} is a void element then the string representation is {@code <elementName[optional
   * attributes]>}. Contents between the element's start and end tag can be set via, for example,
   * {@code appendContent()}.
   *
   *
{@code embed}, {@code object}, {@code script}, {@code style}, {@code template} are not
   * supported because their content has special semantics, and they can result the execution of
   * code not under application control. Some of these have dedicated creation methods.
   *
   * @throws IllegalArgumentException if {@code elementName} contains invalid characters or is not
   *     supported
   * @see http://whatwg.org/html/syntax.html#void-elements
   */
  public SafeHtmlBuilder(@CompileTimeConstant final String elementName) {
    if (elementName == null) {
      throw new NullPointerException();
    }
    if (!elementName.matches(VALID_ELEMENT_NAMES_REGEXP)) {
      throw new IllegalArgumentException(
          "Invalid element name \""
              + elementName
              + "\". "
              + "Only lowercase letters, numbers and '-' allowed.");
    }
    if (UNSUPPORTED_ELEMENTS.contains(elementName)) {
      throw new IllegalArgumentException("Element \"" + elementName + "\" is not supported.");
    }
    this.elementName = elementName;
  }

  /**
   * Causes the builder to use a slash on the tag of a void element, emitting e.g. {@code
}
   * instead of the default {@code
}. Slashes are required if rendering XHTML and optional in
   * HTML 5.
   *
   *
This setting has no effect for non-void elements.
   *
   * @see http://www.w3.org/TR/html5/syntax.html#start-tags
   */
  public SafeHtmlBuilder useSlashOnVoid() {
    useSlashOnVoid = true;
    return this;
  }
  /** These elements are whitelisted to use action with a SafeUrl value. */
  private static final Set<String> ACTION_SAFE_URL_ELEMENT_WHITELIST =
      createUnmodifiableSet("form");

  /**
   * Sets the {@code action} attribute for this element.
   *
   *
The attribute {@code action} with a {@code SafeUrl} value is allowed on these elements:
   *
   *

   *
{@code form}
   *

   *
   * @throws IllegalArgumentException if the {@code action} attribute with a {@code SafeUrl} value
   *     is not allowed on this element
   */
  public SafeHtmlBuilder setAction(SafeUrl value) {
    if (!ACTION_SAFE_URL_ELEMENT_WHITELIST.contains(elementName)) {
      throw new IllegalArgumentException(
          "Attribute \"action\" with a SafeUrl value can only be used "
              + "by one of the following elements: "
              + ACTION_SAFE_URL_ELEMENT_WHITELIST);
    }
    return setAttribute("action", value.getSafeUrlString());
  }

  /** Sets the {@code align} attribute for this element. */
  public SafeHtmlBuilder setAlign(String value) {
    return setAttribute("align", value);
  }

  /** Sets the {@code alt} attribute for this element. */
  public SafeHtmlBuilder setAlt(String value) {
    return setAttribute("alt", value);
  }

  /** Sets the {@code autocapitalize} attribute for this element. */
  public SafeHtmlBuilder setAutocapitalize(String value) {
    return setAttribute("autocapitalize", value);
  }

  /** Sets the {@code autocomplete} attribute for this element. */
  public SafeHtmlBuilder setAutocomplete(String value) {
    return setAttribute("autocomplete", value);
  }

  /** Sets the {@code autofocus} attribute for this element. */
  public SafeHtmlBuilder setAutofocus(String value) {
    return setAttribute("autofocus", value);
  }

  /** Sets the {@code bgcolor} attribute for this element. */
  public SafeHtmlBuilder setBgcolor(String value) {
    return setAttribute("bgcolor", value);
  }

  /** Sets the {@code border} attribute for this element. */
  public SafeHtmlBuilder setBorder(String value) {
    return setAttribute("border", value);
  }

  /** Sets the {@code checked} attribute for this element. */
  public SafeHtmlBuilder setChecked(String value) {
    return setAttribute("checked", value);
  }

  /** These elements are whitelisted to use cite with a SafeUrl value. */
  private static final Set<String> CITE_SAFE_URL_ELEMENT_WHITELIST =
      createUnmodifiableSet("blockquote", "del", "ins", "q");

  /**
   * Sets the {@code cite} attribute for this element.
   *
   *
The attribute {@code cite} with a {@code SafeUrl} value is allowed on these elements:
   *
   *

   *
{@code blockquote}
   *
{@code del}
   *
{@code ins}
   *
{@code q}
   *

   *
   * @throws IllegalArgumentException if the {@code cite} attribute with a {@code SafeUrl} value is
   *     not allowed on this element
   */
  public SafeHtmlBuilder setCite(SafeUrl value) {
    if (!CITE_SAFE_URL_ELEMENT_WHITELIST.contains(elementName)) {
      throw new IllegalArgumentException(
          "Attribute \"cite\" with a SafeUrl value can only be used "
              + "by one of the following elements: "
              + CITE_SAFE_URL_ELEMENT_WHITELIST);
    }
    return setAttribute("cite", value.getSafeUrlString());
  }

  /** Sets the {@code class} attribute for this element. */
  public SafeHtmlBuilder setClass(String value) {
    return setAttribute("class", value);
  }

  /** Sets the {@code color} attribute for this element. */
  public SafeHtmlBuilder setColor(String value) {
    return setAttribute("color", value);
  }

  /** Sets the {@code cols} attribute for this element. */
  public SafeHtmlBuilder setCols(String value) {
    return setAttribute("cols", value);
  }

  /** Sets the {@code colspan} attribute for this element. */
  public SafeHtmlBuilder setColspan(String value) {
    return setAttribute("colspan", value);
  }

  /** Values that can be passed to {@link #setDir(DirValue)}. */
  public enum DirValue {

    /** Value of {@code auto}. */
    AUTO("auto"),
    /** Value of {@code ltr}. */
    LTR("ltr"),
    /** Value of {@code rtl}. */
    RTL("rtl");

    private final String value;

    private DirValue(String value) {
      this.value = value;
    }

    @Override
    public String toString() {
      return value;
    }
  }

  /** Sets the {@code dir} attribute for this element. */
  public SafeHtmlBuilder setDir(DirValue value) {
    return setAttribute("dir", value.toString());
  }

  /** Sets the {@code disabled} attribute for this element. */
  public SafeHtmlBuilder setDisabled(String value) {
    return setAttribute("disabled", value);
  }

  /** Sets the {@code draggable} attribute for this element. */
  public SafeHtmlBuilder setDraggable(String value) {
    return setAttribute("draggable", value);
  }

  /** Sets the {@code face} attribute for this element. */
  public SafeHtmlBuilder setFace(String value) {
    return setAttribute("face", value);
  }

  /** Sets the {@code for} attribute for this element. */
  public SafeHtmlBuilder setFor(@CompileTimeConstant final String value) {
    return setAttribute("for", value);
  }

  /**
   * Sets the {@code for} attribute for this element to a {@link CompileTimeConstant} {@code prefix}
   * and a {@code value} joined by a hyphen.
   *
   * @throws IllegalArgumentException if {@code prefix} is an empty string
   */
  public SafeHtmlBuilder setForWithPrefix(@CompileTimeConstant final String prefix, String value) {
    if (prefix.trim().length() == 0) {
      throw new IllegalArgumentException("Prefix cannot be empty string");
    }
    return setAttribute("for", prefix + "-" + value);
  }

  /** These elements are whitelisted to use formaction with a SafeUrl value. */
  private static final Set<String> FORMACTION_SAFE_URL_ELEMENT_WHITELIST =
      createUnmodifiableSet("button", "input");

  /**
   * Sets the {@code formaction} attribute for this element.
   *
   *
The attribute {@code formaction} with a {@code SafeUrl} value is allowed on these elements:
   *
   *

   *
{@code button}
   *
{@code input}
   *

   *
   * @throws IllegalArgumentException if the {@code formaction} attribute with a {@code SafeUrl}
   *     value is not allowed on this element
   */
  public SafeHtmlBuilder setFormaction(SafeUrl value) {
    if (!FORMACTION_SAFE_URL_ELEMENT_WHITELIST.contains(elementName)) {
      throw new IllegalArgumentException(
          "Attribute \"formaction\" with a SafeUrl value can only be used "
              + "by one of the following elements: "
              + FORMACTION_SAFE_URL_ELEMENT_WHITELIST);
    }
    return setAttribute("formaction", value.getSafeUrlString());
  }

  /** These elements are whitelisted to use formmethod with a String value. */
  private static final Set<String> FORMMETHOD_STRING_ELEMENT_WHITELIST =
      createUnmodifiableSet("button", "input");

  /**
   * Sets the {@code formmethod} attribute for this element.
   *
   *
The attribute {@code formmethod} with a {@code String} value is allowed on these elements:
   *
   *

   *
{@code button}
   *
{@code input}
   *

   *
   * @throws IllegalArgumentException if the {@code formmethod} attribute with a {@code String}
   *     value is not allowed on this element
   */
  public SafeHtmlBuilder setFormmethod(String value) {
    if (!FORMMETHOD_STRING_ELEMENT_WHITELIST.contains(elementName)) {
      throw new IllegalArgumentException(
          "Attribute \"formmethod\" with a String value can only be used "
              + "by one of the following elements: "
              + FORMMETHOD_STRING_ELEMENT_WHITELIST);
    }
    return setAttribute("formmethod", value);
  }

  /** Sets the {@code frameborder} attribute for this element. */
  public SafeHtmlBuilder setFrameborder(String value) {
    return setAttribute("frameborder", value);
  }

  /** Sets the {@code height} attribute for this element. */
  public SafeHtmlBuilder setHeight(String value) {
    return setAttribute("height", value);
  }

  /** Sets the {@code hidden} attribute for this element. */
  public SafeHtmlBuilder setHidden(String value) {
    return setAttribute("hidden", value);
  }

  /** These elements are whitelisted to use href with a SafeUrl value. */
  private static final Set<String> HREF_SAFE_URL_ELEMENT_WHITELIST =
      createUnmodifiableSet("a", "area");
  /**
   * On {@code link} elements, the {@code href} attribute may be set to {@code SafeUrl} values only
   * for these values of the {@code rel} attribute.
   */
  private static final Set<String> LINK_HREF_SAFE_URL_REL_WHITELIST =
      createUnmodifiableSet(
          "alternate",
          "author",
          "bookmark",
          "canonical",
          "cite",
          "help",
          "icon",
          "license",
          "next",
          "prefetch",
          "prerender",
          "prev",
          "search",
          "subresource");

  /**
   * Sets the {@code href} attribute for this element.
   *
   *
The attribute {@code href} with a {@code SafeUrl} value is allowed on these elements:
   *
   *

   *
{@code a}
   *
{@code area}
   *
{@code link}
   *

   *
   *
On {@code link} elements, {@code href} may only be set to a SafeUrl value if {@code rel} is
   * one of the following values:
   *
   *

   *
{@code alternate}
   *
{@code author}
   *
{@code bookmark}
   *
{@code canonical}
   *
{@code cite}
   *
{@code help}
   *
{@code icon}
   *
{@code license}
   *
{@code next}
   *
{@code prefetch}
   *
{@code prerender}
   *
{@code prev}
   *
{@code search}
   *
{@code subresource}
   *

   *
   * @throws IllegalArgumentException if the {@code href} attribute with a {@code SafeUrl} value is
   *     not allowed on this element
   * @throws IllegalArgumentException if this a {@code link} element and the value of {@code rel}
   *     does not allow the SafeUrl contract on {@code href}
   */
  public SafeHtmlBuilder setHref(SafeUrl value) {
    if (!HREF_SAFE_URL_ELEMENT_WHITELIST.contains(elementName) && !elementName.equals("link")) {
      throw new IllegalArgumentException(
          "Attribute \"href\" with a SafeUrl value can only be used "
              + "by one of the following elements: "
              + HREF_SAFE_URL_ELEMENT_WHITELIST);
    }
    if (elementName.equals("link")) {
      checkLinkDependentAttributes(attributes.get("rel"), AttributeContract.SAFE_URL);
    }
    hrefValueContract = AttributeContract.SAFE_URL;
    return setAttribute("href", value.getSafeUrlString());
  }

  /** Sets the {@code href} attribute for this element. */
  public SafeHtmlBuilder setHref(TrustedResourceUrl value) {
    hrefValueContract = AttributeContract.TRUSTED_RESOURCE_URL;
    return setAttribute("href", value.getTrustedResourceUrlString());
  }

  /** These elements are whitelisted to use icon with a SafeUrl value. */
  private static final Set<String> ICON_SAFE_URL_ELEMENT_WHITELIST =
      createUnmodifiableSet("menuitem");

  /**
   * Sets the {@code icon} attribute for this element.
   *
   *
The attribute {@code icon} with a {@code SafeUrl} value is allowed on these elements:
   *
   *

   *
{@code menuitem}
   *

   *
   * @throws IllegalArgumentException if the {@code icon} attribute with a {@code SafeUrl} value is
   *     not allowed on this element
   */
  public SafeHtmlBuilder setIcon(SafeUrl value) {
    if (!ICON_SAFE_URL_ELEMENT_WHITELIST.contains(elementName)) {
      throw new IllegalArgumentException(
          "Attribute \"icon\" with a SafeUrl value can only be used "
              + "by one of the following elements: "
              + ICON_SAFE_URL_ELEMENT_WHITELIST);
    }
    return setAttribute("icon", value.getSafeUrlString());
  }

  /** Sets the {@code id} attribute for this element. */
  public SafeHtmlBuilder setId(@CompileTimeConstant final String value) {
    return setAttribute("id", value);
  }

  /**
   * Sets the {@code id} attribute for this element to a {@link CompileTimeConstant} {@code prefix}
   * and a {@code value} joined by a hyphen.
   *
   * @throws IllegalArgumentException if {@code prefix} is an empty string
   */
  public SafeHtmlBuilder setIdWithPrefix(@CompileTimeConstant final String prefix, String value) {
    if (prefix.trim().length() == 0) {
      throw new IllegalArgumentException("Prefix cannot be empty string");
    }
    return setAttribute("id", prefix + "-" + value);
  }

  /** Sets the {@code ismap} attribute for this element. */
  public SafeHtmlBuilder setIsmap(String value) {
    return setAttribute("ismap", value);
  }

  /** Sets the {@code label} attribute for this element. */
  public SafeHtmlBuilder setLabel(String value) {
    return setAttribute("label", value);
  }

  /** Sets the {@code lang} attribute for this element. */
  public SafeHtmlBuilder setLang(String value) {
    return setAttribute("lang", value);
  }

  /** Sets the {@code list} attribute for this element. */
  public SafeHtmlBuilder setList(@CompileTimeConstant final String value) {
    return setAttribute("list", value);
  }

  /**
   * Sets the {@code list} attribute for this element to a {@link CompileTimeConstant} {@code
   * prefix} and a {@code value} joined by a hyphen.
   *
   * @throws IllegalArgumentException if {@code prefix} is an empty string
   */
  public SafeHtmlBuilder setListWithPrefix(@CompileTimeConstant final String prefix, String value) {
    if (prefix.trim().length() == 0) {
      throw new IllegalArgumentException("Prefix cannot be empty string");
    }
    return setAttribute("list", prefix + "-" + value);
  }

  /** Sets the {@code loop} attribute for this element. */
  public SafeHtmlBuilder setLoop(String value) {
    return setAttribute("loop", value);
  }

  /** Sets the {@code max} attribute for this element. */
  public SafeHtmlBuilder setMax(String value) {
    return setAttribute("max", value);
  }

  /** Sets the {@code maxlength} attribute for this element. */
  public SafeHtmlBuilder setMaxlength(String value) {
    return setAttribute("maxlength", value);
  }

  /** These elements are whitelisted to use media with a String value. */
  private static final Set<String> MEDIA_STRING_ELEMENT_WHITELIST =
      createUnmodifiableSet("link", "source");

  /**
   * Sets the {@code media} attribute for this element.
   *
   *
The attribute {@code media} with a {@code String} value is allowed on these elements:
   *
   *

   *
{@code link}
   *
{@code source}
   *

   *
   * @throws IllegalArgumentException if the {@code media} attribute with a {@code String} value is
   *     not allowed on this element
   */
  public SafeHtmlBuilder setMedia(String value) {
    if (!MEDIA_STRING_ELEMENT_WHITELIST.contains(elementName)) {
      throw new IllegalArgumentException(
          "Attribute \"media\" with a String value can only be used "
              + "by one of the following elements: "
              + MEDIA_STRING_ELEMENT_WHITELIST);
    }
    return setAttribute("media", value);
  }

  /** These elements are whitelisted to use method with a String value. */
  private static final Set<String> METHOD_STRING_ELEMENT_WHITELIST = createUnmodifiableSet("form");

  /**
   * Sets the {@code method} attribute for this element.
   *
   *
The attribute {@code method} with a {@code String} value is allowed on these elements:
   *
   *

   *
{@code form}
   *

   *
   * @throws IllegalArgumentException if the {@code method} attribute with a {@code String} value is
   *     not allowed on this element
   */
  public SafeHtmlBuilder setMethod(String value) {
    if (!METHOD_STRING_ELEMENT_WHITELIST.contains(elementName)) {
      throw new IllegalArgumentException(
          "Attribute \"method\" with a String value can only be used "
              + "by one of the following elements: "
              + METHOD_STRING_ELEMENT_WHITELIST);
    }
    return setAttribute("method", value);
  }

  /** Sets the {@code min} attribute for this element. */
  public SafeHtmlBuilder setMin(String value) {
    return setAttribute("min", value);
  }

  /** Sets the {@code multiple} attribute for this element. */
  public SafeHtmlBuilder setMultiple(String value) {
    return setAttribute("multiple", value);
  }

  /** Sets the {@code muted} attribute for this element. */
  public SafeHtmlBuilder setMuted(String value) {
    return setAttribute("muted", value);
  }

  /** Sets the {@code name} attribute for this element. */
  public SafeHtmlBuilder setName(@CompileTimeConstant final String value) {
    return setAttribute("name", value);
  }

  /**
   * Sets the {@code name} attribute for this element to a {@link CompileTimeConstant} {@code
   * prefix} and a {@code value} joined by a hyphen.
   *
   * @throws IllegalArgumentException if {@code prefix} is an empty string
   */
  public SafeHtmlBuilder setNameWithPrefix(@CompileTimeConstant final String prefix, String value) {
    if (prefix.trim().length() == 0) {
      throw new IllegalArgumentException("Prefix cannot be empty string");
    }
    return setAttribute("name", prefix + "-" + value);
  }

  /** These elements are whitelisted to use pattern with a String value. */
  private static final Set<String> PATTERN_STRING_ELEMENT_WHITELIST =
      createUnmodifiableSet("input");

  /**
   * Sets the {@code pattern} attribute for this element.
   *
   *
The attribute {@code pattern} with a {@code String} value is allowed on these elements:
   *
   *

   *
{@code input}
   *

   *
   * @throws IllegalArgumentException if the {@code pattern} attribute with a {@code String} value
   *     is not allowed on this element
   */
  public SafeHtmlBuilder setPattern(String value) {
    if (!PATTERN_STRING_ELEMENT_WHITELIST.contains(elementName)) {
      throw new IllegalArgumentException(
          "Attribute \"pattern\" with a String value can only be used "
              + "by one of the following elements: "
              + PATTERN_STRING_ELEMENT_WHITELIST);
    }
    return setAttribute("pattern", value);
  }

  /** Sets the {@code placeholder} attribute for this element. */
  public SafeHtmlBuilder setPlaceholder(String value) {
    return setAttribute("placeholder", value);
  }

  /** These elements are whitelisted to use poster with a SafeUrl value. */
  private static final Set<String> POSTER_SAFE_URL_ELEMENT_WHITELIST =
      createUnmodifiableSet("video");

  /**
   * Sets the {@code poster} attribute for this element.
   *
   *
The attribute {@code poster} with a {@code SafeUrl} value is allowed on these elements:
   *
   *

   *
{@code video}
   *

   *
   * @throws IllegalArgumentException if the {@code poster} attribute with a {@code SafeUrl} value
   *     is not allowed on this element
   */
  public SafeHtmlBuilder setPoster(SafeUrl value) {
    if (!POSTER_SAFE_URL_ELEMENT_WHITELIST.contains(elementName)) {
      throw new IllegalArgumentException(
          "Attribute \"poster\" with a SafeUrl value can only be used "
              + "by one of the following elements: "
              + POSTER_SAFE_URL_ELEMENT_WHITELIST);
    }
    return setAttribute("poster", value.getSafeUrlString());
  }

  /** Sets the {@code preload} attribute for this element. */
  public SafeHtmlBuilder setPreload(String value) {
    return setAttribute("preload", value);
  }

  /**
   * Sets the {@code rel} attribute for this element.
   *
   *
If this is a {@code link}} element, and {@code href} has been set from a {@link SafeUrl},
   * then {@code value} has to be an allowed value. See {@link #setHref(SafeHtml)}.
   *
   * @throws IllegalArgumentException if this is a {@code link} element and this value of {@code
   *     rel} is not allowed
   */
  public SafeHtmlBuilder setRel(String value) {
    checkLinkDependentAttributes(value, hrefValueContract);
    return setAttribute("rel", value);
  }

  /** Sets the {@code required} attribute for this element. */
  public SafeHtmlBuilder setRequired(String value) {
    return setAttribute("required", value);
  }

  /** Sets the {@code reversed} attribute for this element. */
  public SafeHtmlBuilder setReversed(String value) {
    return setAttribute("reversed", value);
  }

  /** Sets the {@code role} attribute for this element. */
  public SafeHtmlBuilder setRole(String value) {
    return setAttribute("role", value);
  }

  /** Sets the {@code rows} attribute for this element. */
  public SafeHtmlBuilder setRows(String value) {
    return setAttribute("rows", value);
  }

  /** Sets the {@code rowspan} attribute for this element. */
  public SafeHtmlBuilder setRowspan(String value) {
    return setAttribute("rowspan", value);
  }

  /** Sets the {@code selected} attribute for this element. */
  public SafeHtmlBuilder setSelected(String value) {
    return setAttribute("selected", value);
  }

  /** Sets the {@code shape} attribute for this element. */
  public SafeHtmlBuilder setShape(String value) {
    return setAttribute("shape", value);
  }

  /** Sets the {@code size} attribute for this element. */
  public SafeHtmlBuilder setSize(String value) {
    return setAttribute("size", value);
  }

  /** Sets the {@code sizes} attribute for this element. */
  public SafeHtmlBuilder setSizes(String value) {
    return setAttribute("sizes", value);
  }

  /** Sets the {@code span} attribute for this element. */
  public SafeHtmlBuilder setSpan(String value) {
    return setAttribute("span", value);
  }

  /** Sets the {@code spellcheck} attribute for this element. */
  public SafeHtmlBuilder setSpellcheck(String value) {
    return setAttribute("spellcheck", value);
  }

  /** These elements are whitelisted to use src with a SafeUrl value. */
  private static final Set<String> SRC_SAFE_URL_ELEMENT_WHITELIST =
      createUnmodifiableSet("audio", "img", "input", "source", "video");

  /**
   * Sets the {@code src} attribute for this element.
   *
   *
The attribute {@code src} with a {@code SafeUrl} value is allowed on these elements:
   *
   *

   *
{@code audio}
   *
{@code img}
   *
{@code input}
   *
{@code source}
   *
{@code video}
   *

   *
   * @throws IllegalArgumentException if the {@code src} attribute with a {@code SafeUrl} value is
   *     not allowed on this element
   */
  public SafeHtmlBuilder setSrc(SafeUrl value) {
    if (!SRC_SAFE_URL_ELEMENT_WHITELIST.contains(elementName)) {
      throw new IllegalArgumentException(
          "Attribute \"src\" with a SafeUrl value can only be used "
              + "by one of the following elements: "
              + SRC_SAFE_URL_ELEMENT_WHITELIST);
    }
    return setAttribute("src", value.getSafeUrlString());
  }

  /** Sets the {@code src} attribute for this element. */
  public SafeHtmlBuilder setSrc(TrustedResourceUrl value) {
    return setAttribute("src", value.getTrustedResourceUrlString());
  }

  /** These elements are whitelisted to use srcdoc with a SafeHtml value. */
  private static final Set<String> SRCDOC_SAFE_HTML_ELEMENT_WHITELIST =
      createUnmodifiableSet("iframe");

  /**
   * Sets the {@code srcdoc} attribute for this element.
   *
   *
The attribute {@code srcdoc} with a {@code SafeHtml} value is allowed on these elements:
   *
   *

   *
{@code iframe}
   *

   *
   * @throws IllegalArgumentException if the {@code srcdoc} attribute with a {@code SafeHtml} value
   *     is not allowed on this element
   */
  public SafeHtmlBuilder setSrcdoc(SafeHtml value) {
    if (!SRCDOC_SAFE_HTML_ELEMENT_WHITELIST.contains(elementName)) {
      throw new IllegalArgumentException(
          "Attribute \"srcdoc\" with a SafeHtml value can only be used "
              + "by one of the following elements: "
              + SRCDOC_SAFE_HTML_ELEMENT_WHITELIST);
    }
    return setAttribute("srcdoc", value.getSafeHtmlString());
  }

  /** Sets the {@code start} attribute for this element. */
  public SafeHtmlBuilder setStart(String value) {
    return setAttribute("start", value);
  }

  /** Sets the {@code step} attribute for this element. */
  public SafeHtmlBuilder setStep(String value) {
    return setAttribute("step", value);
  }

  /** Sets the {@code style} attribute for this element. */
  public SafeHtmlBuilder setStyle(SafeStyle value) {
    return setAttribute("style", value.getSafeStyleString());
  }

  /** Sets the {@code summary} attribute for this element. */
  public SafeHtmlBuilder setSummary(String value) {
    return setAttribute("summary", value);
  }

  /** Sets the {@code tabindex} attribute for this element. */
  public SafeHtmlBuilder setTabindex(String value) {
    return setAttribute("tabindex", value);
  }

  /** Values that can be passed to {@link #setTarget(TargetValue)}. */
  public enum TargetValue {

    /** Value of {@code _blank}. */
    BLANK("_blank"),
    /** Value of {@code _self}. */
    SELF("_self");

    private final String value;

    private TargetValue(String value) {
      this.value = value;
    }

    @Override
    public String toString() {
      return value;
    }
  }

  /** Sets the {@code target} attribute for this element. */
  public SafeHtmlBuilder setTarget(TargetValue value) {
    return setAttribute("target", value.toString());
  }

  /** Sets the {@code title} attribute for this element. */
  public SafeHtmlBuilder setTitle(String value) {
    return setAttribute("title", value);
  }

  /** Sets the {@code translate} attribute for this element. */
  public SafeHtmlBuilder setTranslate(String value) {
    return setAttribute("translate", value);
  }

  /** These elements are whitelisted to use type with a String value. */
  private static final Set<String> TYPE_STRING_ELEMENT_WHITELIST =
      createUnmodifiableSet("button", "command", "input", "li", "link", "ol");

  /**
   * Sets the {@code type} attribute for this element.
   *
   *
The attribute {@code type} with a {@code String} value is allowed on these elements:
   *
   *

   *
{@code button}
   *
{@code command}
   *
{@code input}
   *
{@code li}
   *
{@code link}
   *
{@code ol}
   *

   *
   * @throws IllegalArgumentException if the {@code type} attribute with a {@code String} value is
   *     not allowed on this element
   */
  public SafeHtmlBuilder setType(String value) {
    if (!TYPE_STRING_ELEMENT_WHITELIST.contains(elementName)) {
      throw new IllegalArgumentException(
          "Attribute \"type\" with a String value can only be used "
              + "by one of the following elements: "
              + TYPE_STRING_ELEMENT_WHITELIST);
    }
    return setAttribute("type", value);
  }

  /** Sets the {@code valign} attribute for this element. */
  public SafeHtmlBuilder setValign(String value) {
    return setAttribute("valign", value);
  }

  /** Sets the {@code value} attribute for this element. */
  public SafeHtmlBuilder setValue(String value) {
    return setAttribute("value", value);
  }

  /** Sets the {@code width} attribute for this element. */
  public SafeHtmlBuilder setWidth(String value) {
    return setAttribute("width", value);
  }

  /** Sets the {@code wrap} attribute for this element. */
  public SafeHtmlBuilder setWrap(String value) {
    return setAttribute("wrap", value);
  }

  /**
   * Sets a custom data attribute, {@code name}, to {@code value} for this element. {@code value}
   * must consist only of letters and {@code -}.
   *
   * @param name including the "data-" prefix, e.g. "data-tooltip"
   * @throws IllegalArgumentException if the attribute name isn't valid
   * @see
   *     http://www.w3.org/TR/html5/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes
   */
  public SafeHtmlBuilder setDataAttribute(@CompileTimeConstant final String name, String value) {
    if (!name.matches(VALID_DATA_ATTRIBUTES_REGEXP)) {
      throw new IllegalArgumentException(
          "Invalid data attribute name \""
              + name
              + "\"."
              + "Name must start with \"data-\" and be followed by letters and '-'.");
    }
    return setAttribute(name, value);
  }

  /**
   * Appends the given {@code htmls} as this element's content, in sequence.
   *
   * @throws IllegalStateException if this builder represents a void element
   */
  public SafeHtmlBuilder appendContent(SafeHtml... htmls) {
    checkNotVoidElement();
    Collections.addAll(contents, htmls);
    return this;
  }

  /**
   * Appends the given {@code htmls} as this element's content, in the sequence the Iterable returns
   * them.
   *
   * @throws IllegalStateException if this builder represents a void element
   */
  public SafeHtmlBuilder appendContent(Iterable<SafeHtml> htmls) {
    checkNotVoidElement();
    for (SafeHtml html : htmls) {
      contents.add(html);
    }
    return this;
  }

  /**
   * Appends the given {@code htmls} as this element's content, in the sequence the Iterator returns
   * them.
   *
   * @throws IllegalStateException if this builder represents a void element
   */
  public SafeHtmlBuilder appendContent(Iterator<SafeHtml> htmls) {
    checkNotVoidElement();
    while (htmls.hasNext()) {
      contents.add(htmls.next());
    }
    return this;
  }

  /**
   * Checks that this combination of rel value and href contract is safe.
   *
   * @param relValue is the value of rel or null if rel isn't present.
   * @throws IllegalArgumentException if this value and contract combination is not allowed.
   */
  private static void checkLinkDependentAttributes(
      @Nullable String relValue, AttributeContract hrefValueContract) {

    if (hrefValueContract.equals(AttributeContract.SAFE_URL)
        && relValue != null
        && !LINK_HREF_SAFE_URL_REL_WHITELIST.contains(relValue.toLowerCase(Locale.ENGLISH))) {
      throw new IllegalArgumentException(
          "SafeUrl values for the href attribute are not allowed on <link rel="
              + relValue
              + ">. Did you intend to use a TrustedResourceUrl?");
    }
  }

  private void checkNotVoidElement() {
    if (VOID_ELEMENTS.contains(elementName)) {
      throw new IllegalStateException(
          "Element \"" + elementName + "\" is a void element and so cannot have content.");
    }
  }

  /**
   * HTML-escapes and appends {@code text} to this element's content.
   *
   * @throws IllegalStateException if this builder represents a void element
   */
  public SafeHtmlBuilder escapeAndAppendContent(String text) {
    // htmlEscape() unicode coerces in non-portable version.
    return appendContent(SafeHtmls.htmlEscape(text));
  }

  @CheckReturnValue
  public SafeHtml build() {
    StringBuilder sb = new StringBuilder("<" + elementName);
    for (Map.Entry<String, String> entry : attributes.entrySet()) {
      sb.append(" " + entry.getKey() + "=\"" + escapeHtmlInternal(entry.getValue()) + "\"");
    }

    boolean isVoid = VOID_ELEMENTS.contains(elementName);
    if (isVoid && useSlashOnVoid) {
      sb.append("/");
    }
    sb.append(">");
    if (!isVoid) {
      for (SafeHtml content : contents) {
        sb.append(content.getSafeHtmlString());
      }
      sb.append("</" + elementName + ">");
    }
    return SafeHtmls.create(sb.toString());
  }

  private static final Set<String> createUnmodifiableSet(String... strings) {
    HashSet<String> set = new HashSet<>();
    Collections.addAll(set, strings);
    return Collections.unmodifiableSet(set);
  }

  private SafeHtmlBuilder setAttribute(@CompileTimeConstant final String name, String value) {
    if (value == null) {
      throw new NullPointerException("setAttribute requires a non-null value.");
    }
    attributes.put(name, coerceToInterchangeValid(value));
    return this;
  }
}


/**
 * Licensed to Apereo under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Apereo 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 the following location:
 *

 * 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.apereo.cas.security;

import java.util.logging.ConsoleHandler;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;

/**
 * Utility classes.
 *
 * Stateful: configure with whether logException should (re-)throw the logged
 * exception, wrapping it in a new RuntimeException, via
 * setThrowOnErrors(boolean). Defaults to false.
 *
 * @author Misagh Moayyed
 * @since 2.1
 */
public class FilterUtils {
    private static final Logger LOGGER = Logger.getLogger(FilterUtils.class.getName());

    public static boolean throwOnErrors; // Java boolean defaults to false.

    private FilterUtils() {
    }

    public static void setThrowOnErrors(final boolean throwOnErrors) {
        FilterUtils.throwOnErrors = throwOnErrors;
    }

    /**
     * Parse a String to a boolean.
     *

     * "true" : true
     * "false" : false
     * null : false
     * Anything else : logException(IllegalArgumentException).
     *

     * This is a stateless static method.
     *

     * This method is an implementation detail and is not exposed API.
     * This method is only non-private to allow JUnit testing.
     *
     * @param stringToParse a String to parse to a boolean as specified
     * @return true or false
     * @throws IllegalArgumentException if the String is not true, false, or null.
     */
    public static boolean parseStringToBooleanDefaultingToFalse(final String stringToParse) {

        if ("true".equals(stringToParse)) {
            return true;
        } else if ("false".equals(stringToParse)) {
            return false;
        } else if (null == stringToParse) {
            return false;
        }

        logException(LOGGER, new IllegalArgumentException("String [" + stringToParse +
                "] could not parse to a boolean because it was not precisely 'true' or 'false'."));
        return false;
    }

    private static Handler loadLoggerHandlerByClassName(final String loggerHandlerClassName) {
        try {
            if (loggerHandlerClassName == null) {
                return null;
            }

            final ClassLoader classLoader = RequestParameterPolicyEnforcementFilter.class.getClassLoader();
            final Class loggerHandlerClass = classLoader.loadClass(loggerHandlerClassName);
            if (loggerHandlerClass != null) {
                return (Handler) loggerHandlerClass.newInstance();
            }
        } catch (Exception e) {
            LOGGER.log(Level.FINE, e.getMessage(), e);
        }
        return null;
    }

    public static void configureLogging(final Handler handler, final Logger logger) {
        for (final Handler h : logger.getHandlers()) {
            logger.removeHandler(h);
        }
        logger.setUseParentHandlers(false);

        if (handler == null) {
            final ConsoleHandler consoleHandler = new ConsoleHandler();
            consoleHandler.setFormatter(new Formatter() {
                @Override
                public String format(final LogRecord record) {
                    final StringBuffer sb = new StringBuffer();

                    sb.append("[");
                    sb.append(record.getLevel().getName());
                    sb.append("]\t");

                    sb.append(formatMessage(record));
                    sb.append("\n");

                    return sb.toString();
                }
            });
            logger.addHandler(consoleHandler);
        } else {
            logger.addHandler(handler);
        }

    }

    public static void configureLogging(final String loggerHandlerClassName, final Logger logger) {
        final Handler handler = loadLoggerHandlerByClassName(loggerHandlerClassName);
        configureLogging(handler, logger);
    }

    public static void logException(final Logger logger, final Exception ex) {
        logger.log(Level.SEVERE, ex.getMessage(), ex);
        if (throwOnErrors) {
            throw new RuntimeException(ex);
        }
    }
}


/**
 * Licensed to Apereo under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Apereo 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 the following location:
 *

 * 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.apereo.cas.security;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Logger;

/**
 * Allows users to easily inject the default security headers to assist in protecting the application.
 * The default for is to include the following headers:
 *

 * Cache-Control: no-cache, no-store, max-age=0, must-revalidate
 * Pragma: no-cache
 * Expires: 0
 * X-Content-Type-Options: nosniff
 * Strict-Transport-Security: max-age=15768000 ; includeSubDomains
 * X-Frame-Options: DENY
 * X-XSS-Protection: 1; mode=block
 *

 *
 * @author Misagh Moayyed
 * @since 2.0.4
 */
public class ResponseHeadersEnforcementFilter extends AbstractSecurityFilter implements Filter {
    private static final Logger LOGGER = Logger.getLogger(ResponseHeadersEnforcementFilter.class.getName());

    private static final String INIT_PARAM_ENABLE_CACHE_CONTROL = "enableCacheControl";
    private static final String INIT_PARAM_ENABLE_XCONTENT_OPTIONS = "enableXContentTypeOptions";
    private static final String INIT_PARAM_ENABLE_STRICT_TRANSPORT_SECURITY = "enableStrictTransportSecurity";

    private static final String INIT_PARAM_ENABLE_STRICT_XFRAME_OPTIONS = "enableXFrameOptions";
    private static final String INIT_PARAM_STRICT_XFRAME_OPTIONS = "XFrameOptions";

    private static final String INIT_PARAM_ENABLE_XSS_PROTECTION = "enableXSSProtection";
    private static final String INIT_PARAM_XSS_PROTECTION = "XSSProtection";

    private static final String INIT_PARAM_CONTENT_SECURITY_POLICY = "contentSecurityPolicy";

    private boolean enableCacheControl;
    private String cacheControlHeader = "no-cache, no-store, max-age=0, must-revalidate";

    private boolean enableXContentTypeOptions;
    private String xContentTypeOptionsHeader = "nosniff";

    // allow for 6 months; value is in seconds
    private boolean enableStrictTransportSecurity;
    private String strictTransportSecurityHeader = "max-age=15768000 ; includeSubDomains";

    private boolean enableXFrameOptions;
    private String XFrameOptions = "DENY";

    private boolean enableXSSProtection;
    private String XSSProtection = "1; mode=block";

    private String contentSecurityPolicy;

    public void setXSSProtection(final String XSSProtection) {
        this.XSSProtection = XSSProtection;
    }

    public void setXFrameOptions(final String XFrameOptions) {
        this.XFrameOptions = XFrameOptions;
    }

    public void setEnableStrictTransportSecurity(final boolean enableStrictTransportSecurity) {
        this.enableStrictTransportSecurity = enableStrictTransportSecurity;
    }

    public void setEnableCacheControl(final boolean enableCacheControl) {
        this.enableCacheControl = enableCacheControl;
    }

    public void setEnableXContentTypeOptions(final boolean enableXContentTypeOptions) {
        this.enableXContentTypeOptions = enableXContentTypeOptions;
    }

    public void setEnableXFrameOptions(final boolean enableXFrameOptions) {
        this.enableXFrameOptions = enableXFrameOptions;
    }

    public void setEnableXSSProtection(final boolean enableXSSProtection) {
        this.enableXSSProtection = enableXSSProtection;
    }

    public void setContentSecurityPolicy(final String contentSecurityPolicy) {
        this.contentSecurityPolicy = contentSecurityPolicy;
    }

    public ResponseHeadersEnforcementFilter() {
        FilterUtils.configureLogging(getLoggerHandlerClassName(), LOGGER);
    }

    @Override
    public void setLoggerHandlerClassName(final String loggerHandlerClassName) {
        super.setLoggerHandlerClassName(loggerHandlerClassName);
        FilterUtils.configureLogging(getLoggerHandlerClassName(), LOGGER);
    }

    @Override
    public void init(final FilterConfig filterConfig) throws ServletException {
        FilterUtils.configureLogging(getLoggerHandlerClassName(), LOGGER);

        final Enumeration initParamNames = filterConfig.getInitParameterNames();
        throwIfUnrecognizedParamName(initParamNames);

        final String enableCacheControl = filterConfig.getInitParameter(INIT_PARAM_ENABLE_CACHE_CONTROL);
        final String enableXContentTypeOptions = filterConfig.getInitParameter(INIT_PARAM_ENABLE_XCONTENT_OPTIONS);
        final String enableStrictTransportSecurity = filterConfig.getInitParameter(INIT_PARAM_ENABLE_STRICT_TRANSPORT_SECURITY);
        final String enableXFrameOptions = filterConfig.getInitParameter(INIT_PARAM_ENABLE_STRICT_XFRAME_OPTIONS);
        final String enableXSSProtection = filterConfig.getInitParameter(INIT_PARAM_ENABLE_XSS_PROTECTION);

        try {
            this.enableCacheControl = FilterUtils.parseStringToBooleanDefaultingToFalse(enableCacheControl);
        } catch (final Exception e) {
            FilterUtils.logException(LOGGER, new ServletException("Error parsing parameter [" + INIT_PARAM_ENABLE_CACHE_CONTROL
                + "] with value [" + enableCacheControl + "]", e));
        }

        try {
            this.enableXContentTypeOptions = FilterUtils.parseStringToBooleanDefaultingToFalse(enableXContentTypeOptions);
        } catch (final Exception e) {
            FilterUtils.logException(LOGGER, new ServletException("Error parsing parameter [" + INIT_PARAM_ENABLE_XCONTENT_OPTIONS
                + "] with value [" + enableXContentTypeOptions + "]", e));
        }

        try {
            this.enableStrictTransportSecurity = FilterUtils.parseStringToBooleanDefaultingToFalse(enableStrictTransportSecurity);
        } catch (final Exception e) {
            FilterUtils.logException(LOGGER, new ServletException("Error parsing parameter [" + INIT_PARAM_ENABLE_STRICT_TRANSPORT_SECURITY
                + "] with value [" + enableStrictTransportSecurity + "]", e));
        }

        try {
            this.enableXFrameOptions = FilterUtils.parseStringToBooleanDefaultingToFalse(enableXFrameOptions);
            this.XFrameOptions = filterConfig.getInitParameter(INIT_PARAM_STRICT_XFRAME_OPTIONS);
            if (this.XFrameOptions == null || this.XFrameOptions.isEmpty()) {
                this.XFrameOptions = "DENY";
            }
        } catch (final Exception e) {
            FilterUtils.logException(LOGGER, new ServletException("Error parsing parameter [" + INIT_PARAM_ENABLE_STRICT_XFRAME_OPTIONS
                + "] with value [" + enableXFrameOptions + "]", e));
        }

        try {
            this.enableXSSProtection = FilterUtils.parseStringToBooleanDefaultingToFalse(enableXSSProtection);
            this.XSSProtection = filterConfig.getInitParameter(INIT_PARAM_XSS_PROTECTION);
            if (this.XSSProtection == null || this.XSSProtection.isEmpty()) {
                this.XSSProtection = "1; mode=block";
            }
        } catch (final Exception e) {
            FilterUtils.logException(LOGGER, new ServletException("Error parsing parameter [" + INIT_PARAM_ENABLE_XSS_PROTECTION
                + "] with value [" + enableXSSProtection + "]", e));
        }

        this.contentSecurityPolicy = filterConfig.getInitParameter(INIT_PARAM_CONTENT_SECURITY_POLICY);
    }

    /**
     * Examines the Filter init parameter names and throws ServletException if they contain an unrecognized
     * init parameter name.
     *

     * This is a stateless static method.
     *

     * This method is an implementation detail and is not exposed API.
     * This method is only non-private to allow JUnit testing.
     *
     * @param initParamNames init param names, in practice as read from the FilterConfig.
     */
    static void throwIfUnrecognizedParamName(final Enumeration initParamNames) {
        final Set<String> recognizedParameterNames = new HashSet<String>();
        recognizedParameterNames.add(INIT_PARAM_ENABLE_CACHE_CONTROL);
        recognizedParameterNames.add(INIT_PARAM_ENABLE_XCONTENT_OPTIONS);
        recognizedParameterNames.add(INIT_PARAM_ENABLE_STRICT_TRANSPORT_SECURITY);
        recognizedParameterNames.add(INIT_PARAM_ENABLE_STRICT_XFRAME_OPTIONS);
        recognizedParameterNames.add(INIT_PARAM_STRICT_XFRAME_OPTIONS);
        recognizedParameterNames.add(INIT_PARAM_CONTENT_SECURITY_POLICY);
        recognizedParameterNames.add(LOGGER_HANDLER_CLASS_NAME);
        recognizedParameterNames.add(INIT_PARAM_ENABLE_XSS_PROTECTION);
        recognizedParameterNames.add(INIT_PARAM_XSS_PROTECTION);

        while (initParamNames.hasMoreElements()) {
            final String initParamName = (String) initParamNames.nextElement();
            if (!recognizedParameterNames.contains(initParamName)) {
                FilterUtils.logException(LOGGER, new ServletException("Unrecognized init parameter [" + initParamName + "].  Failing safe.  Typo" +
                    " in the web.xml configuration? " +
                    " Misunderstanding about the configuration "
                    + RequestParameterPolicyEnforcementFilter.class.getSimpleName() + " expects?"));
            }
        }
    }

    @Override
    public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
                         final FilterChain filterChain) throws IOException, ServletException {
        try {
            if (servletResponse instanceof HttpServletResponse) {
                final HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
                final HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;

                decideInsertCacheControlHeader(httpServletResponse, httpServletRequest);
                decideInsertStrictTransportSecurityHeader(httpServletResponse, httpServletRequest);
                decideInsertXContentTypeOptionsHeader(httpServletResponse, httpServletRequest);
                decideInsertXFrameOptionsHeader(httpServletResponse, httpServletRequest);
                decideInsertXSSProtectionHeader(httpServletResponse, httpServletRequest);
                decideInsertContentSecurityPolicyHeader(httpServletResponse, httpServletRequest);
            }
        } catch (final Exception e) {
            FilterUtils.logException(LOGGER, new ServletException(getClass().getSimpleName()
                + " is blocking this request. Examine the cause in this stack trace to understand why.", e));
        }

        filterChain.doFilter(servletRequest, servletResponse);
    }

    protected void decideInsertContentSecurityPolicyHeader(final HttpServletResponse httpServletResponse, final HttpServletRequest httpServletRequest) {
        if (this.contentSecurityPolicy == null) {
            return;
        }
        insertContentSecurityPolicyHeader(httpServletResponse, httpServletRequest);
    }

    protected void insertContentSecurityPolicyHeader(final HttpServletResponse httpServletResponse,
                                                     final HttpServletRequest httpServletRequest) {
        this.insertContentSecurityPolicyHeader(httpServletResponse, httpServletRequest, this.contentSecurityPolicy);
    }

    protected void insertContentSecurityPolicyHeader(final HttpServletResponse httpServletResponse,
                                                     final HttpServletRequest httpServletRequest,
                                                     final String contentSecurityPolicy) {
        final String uri = httpServletRequest.getRequestURI();
        httpServletResponse.addHeader("Content-Security-Policy", contentSecurityPolicy);
        LOGGER.fine("Adding Content-Security-Policy response header " + contentSecurityPolicy + " for " + uri);
    }

    protected void decideInsertXSSProtectionHeader(final HttpServletResponse httpServletResponse, final HttpServletRequest httpServletRequest) {
        if (!this.enableXSSProtection) {
            return;
        }
        insertXSSProtectionHeader(httpServletResponse, httpServletRequest);
    }

    protected void insertXSSProtectionHeader(final HttpServletResponse httpServletResponse, final HttpServletRequest httpServletRequest) {
        insertXSSProtectionHeader(httpServletResponse, httpServletRequest, this.XSSProtection);
    }

    protected void insertXSSProtectionHeader(final HttpServletResponse httpServletResponse, final HttpServletRequest httpServletRequest,
                                             final String XSSProtection) {
        final String uri = httpServletRequest.getRequestURI();
        httpServletResponse.addHeader("X-XSS-Protection", XSSProtection);
        LOGGER.fine("Adding X-XSS Protection " + XSSProtection + " response headers for " + uri);
    }

    protected void decideInsertXFrameOptionsHeader(final HttpServletResponse httpServletResponse, final HttpServletRequest httpServletRequest) {
        if (!this.enableXFrameOptions) {
            return;
        }
        insertXFrameOptionsHeader(httpServletResponse, httpServletRequest);
    }

    protected void insertXFrameOptionsHeader(final HttpServletResponse httpServletResponse,
                                             final HttpServletRequest httpServletRequest) {
        insertXFrameOptionsHeader(httpServletResponse, httpServletRequest, this.XFrameOptions);
    }

    protected void insertXFrameOptionsHeader(final HttpServletResponse httpServletResponse,
                                             final HttpServletRequest httpServletRequest,
                                             final String xFrameOptions) {
        final String uri = httpServletRequest.getRequestURI();
        httpServletResponse.addHeader("X-Frame-Options", xFrameOptions);
        LOGGER.fine("Adding X-Frame Options " + xFrameOptions + " response headers for [{}]" + uri);
    }

    protected void decideInsertXContentTypeOptionsHeader(final HttpServletResponse httpServletResponse, final HttpServletRequest httpServletRequest) {
        if (!this.enableXContentTypeOptions) {
            return;
        }
        insertXContentTypeOptionsHeader(httpServletResponse, httpServletRequest);
    }

    protected void insertXContentTypeOptionsHeader(final HttpServletResponse httpServletResponse,
                                                   final HttpServletRequest httpServletRequest) {
        insertXContentTypeOptionsHeader(httpServletResponse, httpServletRequest, this.xContentTypeOptionsHeader);
    }

    protected void insertXContentTypeOptionsHeader(final HttpServletResponse httpServletResponse,
                                                   final HttpServletRequest httpServletRequest,
                                                   final String xContentTypeOptionsHeader) {
        final String uri = httpServletRequest.getRequestURI();
        httpServletResponse.addHeader("X-Content-Type-Options", xContentTypeOptionsHeader);
        LOGGER.fine("Adding X-Content Type response headers " + xContentTypeOptionsHeader + " for " + uri);
    }

    protected void decideInsertCacheControlHeader(final HttpServletResponse httpServletResponse, final HttpServletRequest httpServletRequest) {
        if (!this.enableCacheControl) {
            return;
        }
        insertCacheControlHeader(httpServletResponse, httpServletRequest);
    }

    protected void insertCacheControlHeader(final HttpServletResponse httpServletResponse, final HttpServletRequest httpServletRequest) {
        insertCacheControlHeader(httpServletResponse, httpServletRequest, this.cacheControlHeader);
    }

    protected void insertCacheControlHeader(final HttpServletResponse httpServletResponse, final HttpServletRequest httpServletRequest,
                                            final String cacheControlHeader) {

        final String uri = httpServletRequest.getRequestURI();
        if (!uri.endsWith(".css")
            && !uri.endsWith(".js")
            && !uri.endsWith(".png")
            && !uri.endsWith(".txt")
            && !uri.endsWith(".jpg")
            && !uri.endsWith(".ico")
            && !uri.endsWith(".jpeg")
            && !uri.endsWith(".bmp")
            && !uri.endsWith(".gif")) {
            httpServletResponse.addHeader("Cache-Control", cacheControlHeader);
            httpServletResponse.addHeader("Pragma", "no-cache");
            httpServletResponse.addIntHeader("Expires", 0);
            LOGGER.fine("Adding Cache Control response headers for " + uri);
        }
    }

    protected void decideInsertStrictTransportSecurityHeader(final HttpServletResponse httpServletResponse, final HttpServletRequest httpServletRequest) {
        if (!this.enableStrictTransportSecurity) {
            return;
        }
        insertStrictTransportSecurityHeader(httpServletResponse, httpServletRequest);
    }

    protected void insertStrictTransportSecurityHeader(final HttpServletResponse httpServletResponse,
                                                       final HttpServletRequest httpServletRequest) {
        insertStrictTransportSecurityHeader(httpServletResponse, httpServletRequest, this.strictTransportSecurityHeader);
    }

    protected void insertStrictTransportSecurityHeader(final HttpServletResponse httpServletResponse,
                                                       final HttpServletRequest httpServletRequest,
                                                       final String strictTransportSecurityHeader) {
        if (httpServletRequest.isSecure()) {
            final String uri = httpServletRequest.getRequestURI();

            httpServletResponse.addHeader("Strict-Transport-Security", strictTransportSecurityHeader);
            LOGGER.fine("Adding HSTS response headers for " + uri);
        }
    }

    @Override
    public void destroy() {
    }
}


/**
 * Licensed to Apereo under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Apereo 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 the following location:
 *

 * 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.apereo.cas.security;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;

/**
 * This is a Java Servlet Filter that examines specified Request Parameters as to whether they contain specified
 * characters and as to whether they are multivalued throws an Exception if they do not meet configured rules.
 *

 * Configuration:
 *

 * The filter defaults to checking all request parameters for the hash, percent, question mark,
 * and ampersand characters, and enforcing no-multi-valued-ness.
 *

 * You can turn off multi-value checking by setting the init-param "allowMultiValuedParameters" to "true".  Setting it
 * to "false" is a no-op retaining the default configuration.  Setting this parameter to any other value may fail filter
 * initialization.
 *

 * You can change the set of request parameters being examined by setting the init-param "parametersToCheck" to a
 * whitespace delimited list of parameters to check.  Setting it to the special value "*" retains the default
 * behavior of checking all.  Setting it to a blank value may fail filter initialization.  Setting it to a String
 * containing the asterisk token and any additional token may fail filter initialization.
 *

 * You can change the set of characters looked for by setting the init-param "charactersToForbid" to a whitespace
 * delimited list of characters to forbid.  Setting it to the special value "none" disables the illicit character
 * blocking feature of this Filter (for the case where you only want to use the mutli-valued-ness blocking).
 * Setting it to a blank value may fail filter initialization.
 * Setting it to a value that fails to parse perfectly
 * (e.g., a value with multi-character Strings between the whitespace delimiters)
 * may fail filter initialization.  The default set of characters disallowed is percent, hash, question mark,
 * and ampersand.
 *

 * You can limit a set of request parameters to only be allowed on requests of type POST by
 * setting the init-param "onlyPostParameters" to a whitespace-delimited list of parameters.
 * Unlike "parametersToCheck", this does not support the special value "*".
 * Setting  "onlyPostParameters" to a blank value fails filter initialization.
 * By default (when "onlyPostParameters" is not set), the filter does not limit request parameters
 * to only POST requests.
 *

 * Setting any other init parameter other than these recognized by this Filter will fail Filter initialization.  This
 * is to protect the adopter from typos or misunderstandings in web.xml configuration such that an intended
 * configuration might not have taken effect, since that might have security implications.
 *

 * Setting the Filter to both allow multi-valued parameters and to disallow no characters would make the Filter a
 * no-op, and so may fail filter initialization since you probably meant the Filter to be doing something.
 *

 * The intent of this filter is rough, brute force blocking of unexpected characters in specific CAS protocol related
 * request parameters.  This is one option as a workaround for patching in place certain Java CAS Client versions that
 * may be vulnerable to certain attacks involving crafted request parameter values that may be mishandled.  This is
 * also suitable for patching certain CAS Server versions to make more of an effort to detect and block out-of-spec
 * CAS protocol requests.  Aside from the intent to be useful for those cases, there is nothing CAS-specific about
 * this Filter itself.  This is a generic Filter for doing some pretty basic generic sanity checking on request
 * parameters.  It might come in handy the next time this kind of issue arises.
 *

 * This Filter is written to have no external .jar dependencies aside from the Servlet API necessary to be a Filter.
 *
WARNING: Be sure that either FilterUtils throwOnErrors is set to true (not
 * its default value) or that you are reliably monitoring logs for SEVERE level
 * messages, so that you reliably notice if this filter is misconfigured. If
 * FilterUtils throwOnErrors is false (its default value), this Filter can init
 * in inconsistent configuration states. This is what is meant by
 * "may fail filter initialization" above -- it'll fail only if FilterUtils
 * throwOnError is true (not its default configuration), otherwise it will just
 * log an exception.
 *
 * @since cas-security-filter 1.1
 */
public class RequestParameterPolicyEnforcementFilter extends AbstractSecurityFilter implements Filter {

    private static final Logger LOGGER = Logger.getLogger(RequestParameterPolicyEnforcementFilter.class.getName());

    /**
     * The set of Characters blocked by default on checked parameters.
     * Expressed as a whitespace delimited set of characters.
     */
    public static final String DEFAULT_CHARACTERS_BLOCKED = "? & # %";

    /**
     * The name of the optional Filter init-param specifying what request parameters ought to be checked.
     * The value is a whitespace delimited set of parameters.
     * The exact value '*' has the special meaning of matching all parameters, and is the default behavior.
     */
    public static final String PARAMETERS_TO_CHECK = "parametersToCheck";

    /**
     * The name of the optional Filter init-param specifying what characters are forbidden in the checked request
     * parameters.  The value is a whitespace delimited set of such characters.
     */
    public static final String CHARACTERS_TO_FORBID = "charactersToForbid";

    /**
     * The name of the optional Filter init-param specifying whether the checked request parameters are allowed
     * to have multiple values.  Allowable values for this init parameter are `true` and `false`.
     */
    public static final String ALLOW_MULTI_VALUED_PARAMETERS = "allowMultiValuedParameters";

    /**
     * The name of the optional Filter init-param specifying what request parameters ought to be send via POST requests only.
     */
    public static final String ONLY_POST_PARAMETERS = "onlyPostParameters";

    /**
     * Set of parameter names to check.
     * Empty set represents special behavior of checking all parameters.
     */
    private Set<String> parametersToCheck;

    /**
     * Set of characters to forbid in the checked request parameters.
     * Empty set represents not forbidding any characters.
     */
    private Set<Character> charactersToForbid;

    /**
     * Should checked parameters be permitted to have multiple values.
     */
    private boolean allowMultiValueParameters = false;

    /**
     * Set of parameters which should be only received via POST requests.
     */
    private Set<String> onlyPostParameters;

    /* ========================================================================================================== */
    /* Filter methods */

    public void setParametersToCheck(Set<String> parametersToCheck) {
        this.parametersToCheck = parametersToCheck;
    }

    public void setCharactersToForbid(Set<Character> charactersToForbid) {
        this.charactersToForbid = charactersToForbid;
    }

    public void setAllowMultiValueParameters(boolean allowMultiValueParameters) {
        this.allowMultiValueParameters = allowMultiValueParameters;
    }

    public void setOnlyPostParameters(Set<String> onlyPostParameters) {
        this.onlyPostParameters = onlyPostParameters;
    }

    public RequestParameterPolicyEnforcementFilter() {
        FilterUtils.configureLogging(getLoggerHandlerClassName(), LOGGER);
    }

    @Override
    public void setLoggerHandlerClassName(final String loggerHandlerClassName) {
        super.setLoggerHandlerClassName(loggerHandlerClassName);
        FilterUtils.configureLogging(getLoggerHandlerClassName(), LOGGER);
    }

    @Override
    public void init(final FilterConfig filterConfig) throws ServletException {

            FilterUtils.configureLogging(getLoggerHandlerClassName(), LOGGER);

        // verify there are no init parameters configured that are not recognized
        // since an unrecognized init param might be the adopter trying to configure this filter in an important way
        // and accidentally ignoring that intent might have security implications.
        final Enumeration initParamNames = filterConfig.getInitParameterNames();
        throwIfUnrecognizedParamName(initParamNames);

        final String initParamAllowMultiValuedParameters = filterConfig.getInitParameter(ALLOW_MULTI_VALUED_PARAMETERS);
        final String initParamParametersToCheck = filterConfig.getInitParameter(PARAMETERS_TO_CHECK);
        final String initParamOnlyPostParameters = filterConfig.getInitParameter(ONLY_POST_PARAMETERS);
        final String initParamCharactersToForbid = filterConfig.getInitParameter(CHARACTERS_TO_FORBID);

        try {
            this.allowMultiValueParameters = FilterUtils.parseStringToBooleanDefaultingToFalse(initParamAllowMultiValuedParameters);
        } catch (final Exception e) {
            FilterUtils.logException(LOGGER, new ServletException("Error parsing request parameter [" + ALLOW_MULTI_VALUED_PARAMETERS
                    + "] with value [" + initParamAllowMultiValuedParameters + "]", e));
        }

        try {
            this.parametersToCheck = parseParametersList(initParamParametersToCheck, true);
        } catch (final Exception e) {
            FilterUtils.logException(LOGGER, new ServletException("Error parsing request parameter " + PARAMETERS_TO_CHECK + " with value ["
                    + initParamParametersToCheck + "]", e));
        }
        try {
            this.onlyPostParameters = parseParametersList(initParamOnlyPostParameters, false);
        } catch (final Exception e) {
            FilterUtils.logException(LOGGER, new ServletException("Error parsing request parameter " + ONLY_POST_PARAMETERS + " with value ["
                    + initParamOnlyPostParameters + "]", e));
        }

        try {
            this.charactersToForbid = parseCharactersToForbid(initParamCharactersToForbid);
        } catch (final Exception e) {
            FilterUtils.logException(LOGGER, new ServletException("Error parsing request parameter " + CHARACTERS_TO_FORBID + " with value [" +
                    initParamCharactersToForbid + "]", e));
        }

        if (this.allowMultiValueParameters && this.charactersToForbid.isEmpty()) {
            FilterUtils.logException(LOGGER, new ServletException("Configuration to allow multi-value parameters and forbid no characters makes "
                    + getClass().getSimpleName() + " a no-op, which is probably not what you want, " +
                    "so failing Filter init."));
        }

    }

    @Override
    public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
            throws IOException, ServletException {

        try {

            if (request instanceof HttpServletRequest) {
                final HttpServletRequest httpServletRequest = (HttpServletRequest) request;

                // immutable map from String param name --> String[] parameter values
                final Map parameterMap = httpServletRequest.getParameterMap();

                // which parameters *on this request* ought to be checked.
                final Set<String> parametersToCheckHere;

                if (this.parametersToCheck.isEmpty()) {
                    // the special meaning of empty is to check *all* the parameters, so
                    parametersToCheckHere = parameterMap.keySet();
                } else {
                    parametersToCheckHere = this.parametersToCheck;
                }

                if (!this.allowMultiValueParameters) {
                    requireNotMultiValued(parametersToCheckHere, parameterMap);
                }

                enforceParameterContentCharacterRestrictions(parametersToCheckHere,
                        this.charactersToForbid, parameterMap);

                checkOnlyPostParameters(httpServletRequest.getMethod(), parameterMap, this.onlyPostParameters);
            }
        } catch (final Exception e) {
            // translate to a ServletException to meet the typed expectations of the Filter API.
            FilterUtils.logException(LOGGER, new ServletException(getClass().getSimpleName() + " is blocking this request.  Examine the cause in" +
                    " this stack trace to understand why.", e));
        }

        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
        // do nothing
    }

    /* ========================================================================================================== */
    /* Init parameter parsing */

    /**
     * Examines the Filter init parameter names and throws ServletException if they contain an unrecognized
     * init parameter name.
     *

     * This is a stateless static method.
     *

     * This method is an implementation detail and is not exposed API.
     * This method is only non-private to allow JUnit testing.
     *
     * @param initParamNames init param names, in practice as read from the FilterConfig.
     * @throws ServletException if unrecognized parameter name is present
     */
    static void throwIfUnrecognizedParamName(Enumeration initParamNames) throws ServletException {
        final Set<String> recognizedParameterNames = new HashSet<String>();
        recognizedParameterNames.add(ALLOW_MULTI_VALUED_PARAMETERS);
        recognizedParameterNames.add(PARAMETERS_TO_CHECK);
        recognizedParameterNames.add(ONLY_POST_PARAMETERS);
        recognizedParameterNames.add(CHARACTERS_TO_FORBID);
        recognizedParameterNames.add(LOGGER_HANDLER_CLASS_NAME);

        while (initParamNames.hasMoreElements()) {
            final String initParamName = (String) initParamNames.nextElement();
            if (!recognizedParameterNames.contains(initParamName)) {
                FilterUtils.logException(LOGGER, new ServletException("Unrecognized init parameter [" + initParamName + "].  Failing safe.  Typo" +
                        " in the web.xml configuration? " +
                        " Misunderstanding about the configuration "
                        + RequestParameterPolicyEnforcementFilter.class.getSimpleName() + " expects?"));
            }
        }
    }

    /**
     * Returns the logger instance.
     *
     * @return Configured logger handler, or SLF4j handler, or JUL's default.
     */
    Logger getLogger() {
        return LOGGER;
    }

    /**
     * Parse the whitespace delimited String of parameters to check.
     *

     * If the String is null, return the empty set.
     * If the whitespace delimited String contains no tokens, throw IllegalArgumentException.
     * If the sole token is an asterisk, return the empty set.
     * If the asterisk token is encountered among other tokens, throw IllegalArgumentException.
     *

     * This method returning an empty Set has the special meaning of "check all parameters".
     *

     * This is a stateless static method.
     *

     * This method is an implementation detail and is not exposed API.
     * This method is only non-private to allow JUnit testing.
     *
     * @param initParamValue null, or a non-blank whitespace delimited list of parameters to check
     * @param allowWildcard  whether a wildcard is allowed instead of the parameters list
     * @return a Set of String names of parameters to check, or an empty set representing check-them-all.
     * @throws IllegalArgumentException when the init param value is out of spec
     */
    static Set<String> parseParametersList(final String initParamValue, final boolean allowWildcard) {

        final Set<String> parameterNames = new HashSet<String>();

        if (null == initParamValue) {
            return parameterNames;
        }

        final String[] tokens = initParamValue.split("\\s+");

        if (0 == tokens.length) {
            FilterUtils.logException(LOGGER, new IllegalArgumentException("[" + initParamValue +
                    "] had no tokens but should have had at least one token."));
        }

        if (1 == tokens.length && "*".equals(tokens[0]) && allowWildcard) {
            return parameterNames;
        }

        for (final String parameterName : tokens) {

            if ("*".equals(parameterName)) {
                FilterUtils.logException(LOGGER, new IllegalArgumentException("Star token encountered among other tokens in parsing [" + initParamValue + "]"));
            }

            parameterNames.add(parameterName);
        }

        return parameterNames;
    }

    /**
     * Parse a whitespace delimited set of Characters from a String.
     *

     * If the String is null (init param was not set) default to DEFAULT_CHARACTERS_BLOCKED.
     * If the String is "none" parse to empty set meaning block no characters.
     * If the String is empty throw, to avoid configurer accidentally configuring not to block any characters.
     *
     * @param paramValue value of the init param to parse
     * @return non-null Set of zero or more Characters to block
     */
    static Set<Character> parseCharactersToForbid(String paramValue) {

        final Set<Character> charactersToForbid = new HashSet<Character>();

        if (null == paramValue) {
            paramValue = DEFAULT_CHARACTERS_BLOCKED;
        }

        if ("none".equals(paramValue)) {
            return charactersToForbid;
        }

        final String[] tokens = paramValue.split("\\s+");

        if (0 == tokens.length) {
            FilterUtils.logException(LOGGER, new IllegalArgumentException("Expected tokens when parsing [" + paramValue + "] but found no tokens."
                    + " If you really want to configure no characters, use the magic value 'none'."));
        }

        for (final String token : tokens) {

            if (token.length() > 1) {
                FilterUtils.logException(LOGGER, new IllegalArgumentException("Expected tokens of length 1 but found [" + token + "] when " +
                        "parsing [" + paramValue + "]"));
            }

            final Character character = token.charAt(0);
            charactersToForbid.add(character);
        }

        return charactersToForbid;
    }

    /* ========================================================================================================== */
    /* Filtering requests */

    /**
     * For each parameter to check, verify that it has zero or one value.
     *

     * The Set of parameters to check MAY be empty.
     * The parameter map MAY NOT contain any given parameter to check.
     *

     * This method is an implementation detail and is not exposed API.
     * This method is only non-private to allow JUnit testing.
     *

     *

     * Static, stateless method.
     *
     * @param parametersToCheck non-null potentially empty Set of String names of parameters
     * @param parameterMap      non-null Map from String name of parameter to String[] values
     * @throws IllegalStateException if a parameterToCheck is present in the parameterMap with multiple values.
     */
    static void requireNotMultiValued(final Set<String> parametersToCheck, final Map parameterMap) {

        for (final String parameterName : parametersToCheck) {
            if (parameterMap.containsKey(parameterName)) {
                final String[] values = (String[]) parameterMap.get(parameterName);
                if (values.length > 1) {
                    FilterUtils.logException(LOGGER, new IllegalStateException("Parameter [" + parameterName + "] had multiple values [" +
                            Arrays.toString(values) + "] but at most one value is allowable."));
                }
            }
        }

    }

    /**
     * For each parameter to check, for each value of that parameter, check that the value does not contain
     * forbidden characters.
     *

     * This is a stateless static method.
     *

     * This method is an implementation detail and is not exposed API.
     * This method is only non-private to allow JUnit testing.
     *
     * @param parametersToCheck  Set of String request parameter names to look for
     * @param charactersToForbid Set of Character characters to forbid
     * @param parameterMap       String --> String[] Map, in practice as read from ServletRequest
     */
    static void enforceParameterContentCharacterRestrictions(
            final Set<String> parametersToCheck, final Set<Character> charactersToForbid, final Map parameterMap) {

        if (charactersToForbid.isEmpty()) {
            // short circuit
            return;
        }

        for (final String parameterToCheck : parametersToCheck) {

            final String[] parameterValues = (String[]) parameterMap.get(parameterToCheck);

            if (null != parameterValues) {

                for (final String parameterValue : parameterValues) {

                    for (final Character forbiddenCharacter : charactersToForbid) {

                        final StringBuilder characterAsStringBuilder = new StringBuilder();
                        characterAsStringBuilder.append(forbiddenCharacter);

                        if (parameterValue.contains(characterAsStringBuilder)) {
                            FilterUtils.logException(LOGGER, new IllegalArgumentException("Disallowed character [" + forbiddenCharacter
                                    + "] found in value [" + parameterValue + "] of parameter named ["
                                    + parameterToCheck + "]"));
                        }

                        // that forbiddenCharacter was not in this parameterValue
                    }

                    // none of the charactersToForbid were in this parameterValue
                }

                // none of the values of this parameterToCheck had a forbidden character
            } // or this parameterToCheck had null value

        }

        // none of the values of any of the parametersToCheck had a forbidden character
        // hurray! allow flow to continue without throwing an Exception.
    }

    /**
     * Check that some parameters should only be in POST requests (according to the configuration).
     *
     * @param method             the method of the request
     * @param parameterMap       all the request parameters
     * @param onlyPostParameters parameters that should only be in POST requests
     */
    static void checkOnlyPostParameters(final String method, final Map parameterMap, final Set<String> onlyPostParameters) {
        if (!"POST".equals(method)) {
            Set<String> names = parameterMap.keySet();
            for (String onlyPostParameter : onlyPostParameters) {
                if (names.contains(onlyPostParameter)) {
                    FilterUtils.logException(LOGGER, new IllegalArgumentException(onlyPostParameter + " parameter should only be used in POST requests"));
                }
            }
        }
    }

}