001    // Copyright (c) 2011, Mike Samuel
002    // All rights reserved.
003    //
004    // Redistribution and use in source and binary forms, with or without
005    // modification, are permitted provided that the following conditions
006    // are met:
007    //
008    // Redistributions of source code must retain the above copyright
009    // notice, this list of conditions and the following disclaimer.
010    // Redistributions in binary form must reproduce the above copyright
011    // notice, this list of conditions and the following disclaimer in the
012    // documentation and/or other materials provided with the distribution.
013    // Neither the name of the OWASP nor the names of its contributors may
014    // be used to endorse or promote products derived from this software
015    // without specific prior written permission.
016    // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
017    // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
018    // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
019    // FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
020    // COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
021    // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
022    // BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
023    // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
024    // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
025    // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
026    // ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
027    // POSSIBILITY OF SUCH DAMAGE.
028    
029    package org.owasp.html;
030    
031    import java.util.List;
032    import java.util.Map;
033    import java.util.Set;
034    import java.util.regex.Pattern;
035    
036    import javax.annotation.Nullable;
037    import javax.annotation.concurrent.NotThreadSafe;
038    
039    import com.google.common.base.Function;
040    import com.google.common.base.Predicate;
041    import com.google.common.collect.ImmutableList;
042    import com.google.common.collect.ImmutableMap;
043    import com.google.common.collect.ImmutableSet;
044    import com.google.common.collect.Maps;
045    import com.google.common.collect.Sets;
046    
047    
048    /**
049     * Conveniences for configuring policies for the {@link HtmlSanitizer}.
050     *
051     * <h3>Usage</h3>
052     * <p>
053     * To create a policy, first construct an instance of this class; then call
054     * <code>allow&hellip;</code> methods to turn on tags, attributes, and other
055     * processing modes; and finally call <code>build()</code> or
056     * <code>toFactory()</code>.
057     * </p>
058     * <pre class="prettyprint lang-java">
059     * // Define the policy.
060     * Function<HtmlStreamEventReceiver, HtmlSanitizer.Policy> policyDefinition
061     *     = new HtmlPolicyBuilder()
062     *         .allowElements("a", "p")
063     *         .allowAttributes("href").onElements("a")
064     *         .toFactory();
065     *
066     * // Sanitize your output.
067     * HtmlSanitizer.sanitize(myHtml. policyDefinition.apply(myHtmlStreamRenderer));
068     * </pre>
069     *
070     * <h3>Embedded Content</h3>
071     * <p>
072     * Embedded URLs are filtered by
073     * {@link HtmlPolicyBuilder#allowUrlProtocols protocol}.
074     * There is a {@link HtmlPolicyBuilder#allowStandardUrlProtocols canned policy}
075     * so you can easily white-list widely used policies that don't violate the
076     * current pages origin.  See "Customization" below for ways to do further
077     * filtering.  If you allow links it might be worthwhile to
078     * {@link HtmlPolicyBuilder#requireRelNofollowOnLinks() require}
079     * {@code rel=nofollow}.
080     * </p>
081     * <p>
082     * This class simply throws out all embedded JS.
083     * Use a custom element or attribute policy to allow through
084     * signed or otherwise known-safe code.
085     * Check out the Caja project if you need a way to contain third-party JS.
086     * </p>
087     * <p>
088     * This class does not attempt to faithfully parse and sanitize CSS.
089     * It does provide {@link HtmlPolicyBuilder#allowStyling() one} styling option
090     * that allows through a few CSS properties that allow textual styling, but that
091     * disallow image loading, history stealing, layout breaking, code execution,
092     * etc.
093     * </p>
094     *
095     * <h3>Customization</h3>
096     * <p>
097     * You can easily do custom processing on tags and attributes by supplying your
098     * own {@link ElementPolicy element policy} or
099     * {@link AttributePolicy attribute policy} when calling
100     * <code>allow&hellip;</code>.
101     * E.g. to convert headers into {@code <div>}s, you could use an element policy
102     * </p>
103     * <pre class="prettyprint lang-java">
104     *     new HtmlPolicyBuilder
105     *         .allowElement(
106     *         new ElementPolicy() {
107     *           public String apply(String elementName, List<String> attributes) {
108     *             attributes.add("class");
109     *             attributes.add("header-" + elementName);
110     *             return "div";
111     *           }
112     *         },
113     *         "h1", "h2", "h3", "h4", "h5", "h6")
114     *         .build(outputChannel)
115     * </pre>
116     *
117     * <h3>Rules of Thumb</h3>
118     * <p>
119     * Throughout this class, several rules hold:
120     * <ul>
121     *   <li>Everything is denied by default.  There are
122     *     <code>disallow&hellip;</code> methods, but those reverse
123     *     allows instead of rolling back overly permissive defaults.
124     *   <li>The order of allows and disallows does not matter.
125     *     Disallows trump allows whether they occur before or after them.
126     *     The only method that needs to be called in a particular place is
127     *     {@link HtmlPolicyBuilder#build}.
128     *     Allows or disallows after {@code build} is called have no
129     *     effect on the already built policy.
130     *   <li>Element and attribute policies are applied in the following order:
131     *     element specific attribute policy, global attribute policy, element
132     *     policy.
133     *     Element policies come last so they can observe all the post-processed
134     *     attributes, and so they can add attributes that are exempt from
135     *     attribute policies.
136     *     Element specific policies go first, so they can normalize content to
137     *     a form that might be acceptable to a more simplistic global policy.
138     * </ul>
139     *
140     * <h3>Thread safety and efficiency</h3>
141     * <p>
142     * This class is not thread-safe.  The resulting policy will not violate its
143     * security guarantees as a result of race conditions, but is not thread safe
144     * because it maintains state to track whether text inside disallowed elements
145     * should be suppressed.
146     * <p>
147     * The resulting policy can be reused, but if you use the
148     * {@link HtmlPolicyBuilder#toFactory()} method instead of {@link #build}, then
149     * binding policies to output channels is cheap so there's no need.
150     * </p>
151     *
152     * @author Mike Samuel <mikesamuel@gmail.com>
153     */
154    @TCB
155    @NotThreadSafe
156    public class HtmlPolicyBuilder {
157      /**
158       * The default set of elements that are removed if they have no attributes.
159       * Since {@code <img>} is in this set, by default, a policy will remove
160       * {@code <img src=javascript:alert(1337)>} because its URL is not allowed
161       * and it has no other attributes that would warrant it appearing in the
162       * output.
163       */
164      public static final ImmutableSet<String> DEFAULT_SKIP_IF_EMPTY
165          = ImmutableSet.of("a", "font", "img", "input", "span");
166    
167      private final Map<String, ElementPolicy> elPolicies = Maps.newLinkedHashMap();
168      private final Map<String, Map<String, AttributePolicy>> attrPolicies
169          = Maps.newLinkedHashMap();
170      private final Map<String, AttributePolicy> globalAttrPolicies
171          = Maps.newLinkedHashMap();
172      private final Set<String> allowedProtocols = Sets.newLinkedHashSet();
173      private final Set<String> skipIfEmpty = Sets.newLinkedHashSet(
174          DEFAULT_SKIP_IF_EMPTY);
175      private boolean requireRelNofollowOnLinks, allowStyling;
176    
177      /**
178       * Allows the named elements.
179       */
180      public HtmlPolicyBuilder allowElements(String... elementNames) {
181        return allowElements(ElementPolicy.IDENTITY_ELEMENT_POLICY, elementNames);
182      }
183    
184      /**
185       * Disallows the named elements.  Elements are disallowed by default, so
186       * there is no need to disallow elements, unless you are making an exception
187       * based on an earlier allow.
188       */
189      public HtmlPolicyBuilder disallowElements(String... elementNames) {
190        return allowElements(ElementPolicy.REJECT_ALL_ELEMENT_POLICY, elementNames);
191      }
192    
193      /**
194       * Allow the given elements with the given policy.
195       *
196       * @param policy May remove or add attributes, change the element name, or
197       *    deny the element.
198       */
199      public HtmlPolicyBuilder allowElements(
200          ElementPolicy policy, String... elementNames) {
201        invalidateCompiledState();
202        for (String elementName : elementNames) {
203          elementName = HtmlLexer.canonicalName(elementName);
204          ElementPolicy newPolicy = ElementPolicy.Util.join(
205              elPolicies.get(elementName), policy);
206          // Don't remove if newPolicy is the always reject policy since we want
207          // that to infect later allowElement calls for this particular element
208          // name.  rejects should have higher priority than allows.
209          elPolicies.put(elementName, newPolicy);
210        }
211        return this;
212      }
213    
214      /**
215       * A canned policy that allows a number of common formatting elements.
216       */
217      public HtmlPolicyBuilder allowCommonInlineFormattingElements() {
218        return allowElements(
219            "b", "i", "font", "s", "u", "o", "sup", "sub", "ins", "del", "strong",
220            "strike", "tt", "code", "big", "small", "br", "span");
221      }
222    
223      /**
224       * A canned policy that allows a number of common block elements.
225       */
226      public HtmlPolicyBuilder allowCommonBlockElements() {
227        return allowElements(
228            "p", "div", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "li",
229            "blockquote");
230      }
231    
232      /**
233       * Assuming the given elements are allowed, allows them to appear without
234       * attributes.
235       *
236       * @see #DEFAULT_SKIP_IF_EMPTY
237       * @see #disallowWithoutAttributes
238       */
239      public HtmlPolicyBuilder allowWithoutAttributes(String... elementNames) {
240        invalidateCompiledState();
241        for (String elementName : elementNames) {
242          elementName = HtmlLexer.canonicalName(elementName);
243          skipIfEmpty.remove(elementName);
244        }
245        return this;
246      }
247    
248      /**
249       * Disallows the given elements from appearing without attributes.
250       *
251       * @see #DEFAULT_SKIP_IF_EMPTY
252       * @see #allowWithoutAttributes
253       */
254      public HtmlPolicyBuilder disallowWithoutAttributes(String... elementNames) {
255        invalidateCompiledState();
256        for (String elementName : elementNames) {
257          elementName = HtmlLexer.canonicalName(elementName);
258          skipIfEmpty.add(elementName);
259        }
260        return this;
261      }
262    
263      /**
264       * Returns an object that lets you associate policies with the given
265       * attributes, and allow them globally or on specific elements.
266       */
267      public AttributeBuilder allowAttributes(String... attributeNames) {
268        ImmutableList.Builder<String> b = ImmutableList.builder();
269        for (String attributeName : attributeNames) {
270          b.add(HtmlLexer.canonicalName(attributeName));
271        }
272        return new AttributeBuilder(b.build());
273      }
274    
275      /**
276       * Reverse an earlier attribute {@link #allowAttributes allow}.
277       * <p>
278       * For this to have an effect you must call at least one of
279       * {@link AttributeBuilder#globally} and {@link AttributeBuilder#onElements}.
280       * <p>
281       * Attributes are disallowed by default, so there is no need to call this
282       * with a laundry list of attribute/element pairs.
283       */
284      public AttributeBuilder disallowAttributes(String... attributeNames) {
285        return this.allowAttributes(attributeNames)
286            .matching(AttributePolicy.REJECT_ALL_ATTRIBUTE_POLICY);
287      }
288    
289    
290      private HtmlPolicyBuilder allowAttributesGlobally(
291          AttributePolicy policy, List<String> attributeNames) {
292        invalidateCompiledState();
293        for (String attributeName : attributeNames) {
294          // We reinterpret the identity policy later via policy joining since its
295          // the default passed from the policy-less method, but we don't do
296          // anything here since we don't know until build() is called whether the
297          // policy author wants to allow certain URL protocols or wants to deal
298          // with styles.
299          AttributePolicy oldPolicy = globalAttrPolicies.get(attributeName);
300          globalAttrPolicies.put(
301              attributeName, AttributePolicy.Util.join(oldPolicy, policy));
302        }
303        return this;
304      }
305    
306      private HtmlPolicyBuilder allowAttributesOnElements(
307          AttributePolicy policy, List<String> attributeNames,
308          List<String> elementNames) {
309        invalidateCompiledState();
310        for (String elementName : elementNames) {
311          Map<String, AttributePolicy> policies = attrPolicies.get(elementName);
312          if (policies == null) {
313            policies = Maps.newLinkedHashMap();
314            attrPolicies.put(elementName, policies);
315          }
316          for (String attributeName : attributeNames) {
317            AttributePolicy oldPolicy = policies.get(attributeName);
318            policies.put(
319                attributeName,
320                AttributePolicy.Util.join(oldPolicy, policy));
321          }
322        }
323        return this;
324      }
325    
326      /**
327       * Adds <a href="http://en.wikipedia.org/wiki/Nofollow"><code>rel=nofollow</code></a>
328       * to links.
329       */
330      public HtmlPolicyBuilder requireRelNofollowOnLinks() {
331        invalidateCompiledState();
332        this.requireRelNofollowOnLinks = true;
333        return this;
334      }
335    
336      /**
337       * Adds to the set of protocols that are allowed in URL attributes.
338       * For each URL attribute that is allowed, we further constrain it by
339       * only allowing the value through if it specifies no protocol, or if it
340       * specifies one in the allowedProtocols white-list.
341       * This is done regardless of whether any protocols have been allowed, so
342       * allowing the attribute "href" globally with the identity policy but
343       * not white-listing any protocols, effectively disallows the "href"
344       * attribute globally.
345       * <p>
346       * Do not allow any <code>*script</code> such as <code>javascript</code>
347       * protocols if you might use this policy with untrusted code.
348       */
349      public HtmlPolicyBuilder allowUrlProtocols(String... protocols) {
350        invalidateCompiledState();
351        // If there is at least one allowed protocol, then allow URLs and
352        // add a filter that checks href and src values.
353    
354        // Do not allow href and srcs through otherwise, and only allow on images
355        // and links.
356        for (String protocol : protocols) {
357          protocol = Strings.toLowerCase(protocol);
358          allowedProtocols.add(protocol);
359        }
360        return this;
361      }
362    
363      /**
364       * Reverses a decision made by {@link #allowUrlProtocols}.
365       */
366      public HtmlPolicyBuilder disallowUrlProtocols(String... protocols) {
367        invalidateCompiledState();
368        for (String protocol : protocols) {
369          protocol = Strings.toLowerCase(protocol);
370          allowedProtocols.remove(protocol);
371        }
372        return this;
373      }
374    
375      /**
376       * A canned URL protocol policy that allows <code>http</code>,
377       * <code>https</code>, and <code>mailto</code>.
378       */
379      public HtmlPolicyBuilder allowStandardUrlProtocols() {
380        return allowUrlProtocols("http", "https", "mailto");
381      }
382    
383      /**
384       * Convert <code>style="&lt;CSS&gt;"</code> to simple non-JS containing
385       * <code>&lt;font&gt;</code> tags to allow color, font-size, typeface, and
386       * other styling.
387       */
388      public HtmlPolicyBuilder allowStyling() {
389        invalidateCompiledState();
390        allowStyling = true;
391        return this;
392      }
393    
394      /**
395       * Names of attributes from HTML 4 whose values are URLs.
396       * Other attributes, e.g. <code>style</code> may contain URLs even though
397       * there values are not URLs.
398       */
399      private static final Set<String> URL_ATTRIBUTE_NAMES = ImmutableSet.of(
400          "action", "archive", "background", "cite", "classid", "codebase", "data",
401          "dsync", "formaction", "href", "icon", "longdesc", "manifest", "poster",
402          "profile", "src", "usemap");
403    
404      /**
405       * Produces a policy based on the allow and disallow calls previously made.
406       *
407       * @param out receives calls to open only tags allowed by
408       *      previous calls to this object.
409       *      Typically a {@link HtmlStreamRenderer}.
410       */
411      public HtmlSanitizer.Policy build(HtmlStreamEventReceiver out) {
412        return toFactory().apply(out);
413      }
414    
415      /**
416       * Like {@link #build} but can be reused to create many different policies
417       * each backed by a different output channel.
418       */
419      public Function<HtmlStreamEventReceiver, HtmlSanitizer.Policy> toFactory() {
420        return new Factory(compilePolicies(), allowStyling);
421      }
422    
423      // Speed up subsequent builds by caching the compiled policies.
424      private transient ImmutableMap<String, ElementAndAttributePolicies>
425          compiledPolicies;
426    
427      /** Called by mutators to signal that any compiled policy is out-of-date. */
428      private void invalidateCompiledState() {
429        compiledPolicies = null;
430      }
431    
432      private ImmutableMap<String, ElementAndAttributePolicies> compilePolicies() {
433        if (compiledPolicies != null) { return compiledPolicies; }
434    
435        // Copy maps before normalizing in case builder is reused.
436        Map<String, ElementPolicy> elPolicies
437            = Maps.newLinkedHashMap(this.elPolicies);
438        Map<String, Map<String, AttributePolicy>> attrPolicies
439            = Maps.newLinkedHashMap(this.attrPolicies);
440        for (Map.Entry<String, Map<String, AttributePolicy>> e :
441             attrPolicies.entrySet()) {
442          e.setValue(Maps.newLinkedHashMap(e.getValue()));
443        }
444        Map<String, AttributePolicy> globalAttrPolicies
445            = Maps.newLinkedHashMap(this.globalAttrPolicies);
446        Set<String> allowedProtocols = ImmutableSet.copyOf(this.allowedProtocols);
447    
448        // Implement requireRelNofollowOnLinks
449        if (requireRelNofollowOnLinks) {
450          elPolicies.put(
451              "a",
452              ElementPolicy.Util.join(
453                  elPolicies.get("a"),
454                  new ElementPolicy() {
455                    public String apply(String elementName, List<String> attrs) {
456                      for (int i = 0, n = attrs.size(); i < n; i += 2) {
457                        if ("href".equals(attrs.get(i))) {
458                          attrs.add("rel");
459                          attrs.add("nofollow");
460                          break;
461                        }
462                      }
463                      return elementName;
464                    }
465                  }));
466        }
467    
468        // Implement protocol policies.
469        // For each URL attribute that is allowed, we further constrain it by
470        // only allowing the value through if it specifies no protocol, or if it
471        // specifies one in the allowedProtocols white-list.
472        // This is done regardless of whether any protocols have been allowed, so
473        // allowing the attribute "href" globally with the identity policy but
474        // not white-listing any protocols, effectively disallows the "href"
475        // attribute globally.
476        {
477          AttributePolicy urlAttributePolicy;
478          if (allowedProtocols.size() == 3
479              && allowedProtocols.contains("mailto")
480              && allowedProtocols.contains("http")
481              && allowedProtocols.contains("https")) {
482            urlAttributePolicy = StandardUrlAttributePolicy.INSTANCE;
483          } else {
484            urlAttributePolicy = new FilterUrlByProtocolAttributePolicy(
485                allowedProtocols);
486          }
487          Set<String> toGuard = Sets.newLinkedHashSet(URL_ATTRIBUTE_NAMES);
488          for (String urlAttributeName : URL_ATTRIBUTE_NAMES) {
489            if (globalAttrPolicies.containsKey(urlAttributeName)) {
490              toGuard.remove(urlAttributeName);
491              globalAttrPolicies.put(urlAttributeName, AttributePolicy.Util.join(
492                  urlAttributePolicy, globalAttrPolicies.get(urlAttributeName)));
493            }
494          }
495          // Implement guards not implemented on global policies in the per-element
496          // policy maps.
497          for (Map.Entry<String, Map<String, AttributePolicy>> e
498               : attrPolicies.entrySet()) {
499            Map<String, AttributePolicy> policies = e.getValue();
500            for (String urlAttributeName : toGuard) {
501              if (policies.containsKey(urlAttributeName)) {
502                policies.put(urlAttributeName, AttributePolicy.Util.join(
503                    urlAttributePolicy, policies.get(urlAttributeName)));
504              }
505            }
506          }
507        }
508    
509        ImmutableMap.Builder<String, ElementAndAttributePolicies> policiesBuilder
510            = ImmutableMap.builder();
511        for (Map.Entry<String, ElementPolicy> e : elPolicies.entrySet()) {
512          String elementName = e.getKey();
513          ElementPolicy elPolicy = e.getValue();
514          if (ElementPolicy.REJECT_ALL_ELEMENT_POLICY.equals(elPolicy)) {
515            continue;
516          }
517    
518          Map<String, AttributePolicy> elAttrPolicies
519              = attrPolicies.get(elementName);
520          if (elAttrPolicies == null) { elAttrPolicies = ImmutableMap.of(); }
521          ImmutableMap.Builder<String, AttributePolicy> attrs
522              = ImmutableMap.builder();
523          for (Map.Entry<String, AttributePolicy> ape : elAttrPolicies.entrySet()) {
524            String attributeName = ape.getKey();
525            if (globalAttrPolicies.containsKey(attributeName)) { continue; }
526            AttributePolicy policy = ape.getValue();
527            if (!AttributePolicy.REJECT_ALL_ATTRIBUTE_POLICY.equals(policy)) {
528              attrs.put(attributeName, policy);
529            }
530          }
531          for (Map.Entry<String, AttributePolicy> ape
532               : globalAttrPolicies.entrySet()) {
533            String attributeName = ape.getKey();
534            AttributePolicy policy = AttributePolicy.Util.join(
535                elAttrPolicies.get(attributeName), ape.getValue());
536            if (!AttributePolicy.REJECT_ALL_ATTRIBUTE_POLICY.equals(policy)) {
537              attrs.put(attributeName, policy);
538            }
539          }
540    
541          policiesBuilder.put(
542              elementName,
543              new ElementAndAttributePolicies(
544                  elementName,
545                  elPolicy, attrs.build(), skipIfEmpty.contains(elementName)));
546        }
547        return compiledPolicies = policiesBuilder.build();
548      }
549    
550      /**
551       * Builds the relationship between attributes, the values that they may have,
552       * and the elements on which they may appear.
553       *
554       * @author Mike Samuel
555       */
556      public final class AttributeBuilder {
557        private final List<String> attributeNames;
558        private AttributePolicy policy = AttributePolicy.IDENTITY_ATTRIBUTE_POLICY;
559    
560        AttributeBuilder(List<? extends String> attributeNames) {
561          this.attributeNames = ImmutableList.copyOf(attributeNames);
562        }
563    
564        /**
565         * Filters and/or transforms the attribute values
566         * allowed by later {@code allow*} calls.
567         * Multiple calls to {@code matching} are combined so that the policies
568         * receive the value in order, each seeing the value after any
569         * transformation by a previous policy.
570         */
571        public AttributeBuilder matching(AttributePolicy policy) {
572          this.policy = AttributePolicy.Util.join(this.policy, policy);
573          return this;
574        }
575    
576        /**
577         * Restrict the values allowed by later {@code allow*} calls to those
578         * matching the pattern.
579         * Multiple calls to {@code matching} are combined to restrict to the
580         * intersection of possible matched values.
581         */
582        public AttributeBuilder matching(final Pattern pattern) {
583          return matching(new AttributePolicy() {
584            public @Nullable String apply(
585                String elementName, String attributeName, String value) {
586              return pattern.matcher(value).matches() ? value : null;
587            }
588          });
589        }
590    
591        /**
592         * Restrict the values allowed by later {@code allow*} calls to those
593         * matching the given predicate.
594         * Multiple calls to {@code matching} are combined to restrict to the
595         * intersection of possible matched values.
596         */
597        public AttributeBuilder matching(
598            final Predicate<? super String> filter) {
599          return matching(new AttributePolicy() {
600            public @Nullable String apply(
601                String elementName, String attributeName, String value) {
602              return filter.apply(value) ? value : null;
603            }
604          });
605        }
606    
607        /**
608         * Restrict the values allowed by later {@code allow*} calls to those
609         * supplied.
610         * Multiple calls to {@code matching} are combined to restrict to the
611         * intersection of possible matched values.
612         */
613        public AttributeBuilder matching(
614            boolean ignoreCase, String... allowedValues) {
615          return matching(ignoreCase, ImmutableSet.copyOf(allowedValues));
616        }
617    
618        /**
619         * Restrict the values allowed by later {@code allow*} calls to those
620         * supplied.
621         * Multiple calls to {@code matching} are combined to restrict to the
622         * intersection of possible matched values.
623         */
624        public AttributeBuilder matching(
625            final boolean ignoreCase, Set<? extends String> allowedValues) {
626          final ImmutableSet<String> allowed = ImmutableSet.copyOf(allowedValues);
627          return matching(new AttributePolicy() {
628            public @Nullable String apply(
629                String elementName, String attributeName, String value) {
630              if (ignoreCase) { value = Strings.toLowerCase(value); }
631              return allowed.contains(value) ? value : null;
632            }
633          });
634        }
635    
636        /**
637         * Allows the given attributes on any elements but filters the
638         * attributes' values based on previous calls to {@code matching(...)}.
639         * Global attribute policies are applied after element specific policies.
640         * Be careful of using this with attributes like <code>type</code> which
641         * have different meanings on different attributes.
642         * Also be careful of allowing globally attributes like <code>href</code>
643         * which can have more far-reaching effects on tags like
644         * <code>&lt;base&gt;</code> and <code>&lt;link&gt;</code> than on
645         * <code>&lt;a&gt;</code> because in the former, they have an effect without
646         * user interaction and can change the behavior of the current page.
647         */
648        public HtmlPolicyBuilder globally() {
649          return HtmlPolicyBuilder.this.allowAttributesGlobally(
650              policy, attributeNames);
651        }
652    
653        /**
654         * Allows the named attributes on the given elements but filters the
655         * attributes' values based on previous calls to {@code matching(...)}.
656         */
657        public HtmlPolicyBuilder onElements(String... elementNames) {
658          ImmutableList.Builder<String> b = ImmutableList.builder();
659          for (String elementName : elementNames) {
660            b.add(HtmlLexer.canonicalName(elementName));
661          }
662          return HtmlPolicyBuilder.this.allowAttributesOnElements(
663              policy, attributeNames, b.build());
664        }
665      }
666    
667    }
668    
669    final class Factory
670        implements Function<HtmlStreamEventReceiver, HtmlSanitizer.Policy> {
671      private final ImmutableMap<String, ElementAndAttributePolicies> policies;
672      private final boolean allowStyling;
673    
674      Factory(
675          ImmutableMap<String, ElementAndAttributePolicies> policies,
676          boolean allowStyling) {
677        this.policies = policies;
678        this.allowStyling = allowStyling;
679      }
680    
681      public HtmlSanitizer.Policy apply(HtmlStreamEventReceiver out) {
682        if (allowStyling) {
683          return new StylingPolicy(out, policies);
684        } else {
685          return new ElementAndAttributePolicyBasedSanitizerPolicy(
686              out, policies);
687        }
688      }
689    }