001package org.jsoup.safety;
002
003/*
004    Thank you to Ryan Grove (wonko.com) for the Ruby HTML cleaner http://github.com/rgrove/sanitize/, which inspired
005    this safe-list configuration, and the initial defaults.
006 */
007
008import org.jsoup.helper.Validate;
009import org.jsoup.internal.Normalizer;
010import org.jsoup.internal.StringUtil;
011import org.jsoup.nodes.Attribute;
012import org.jsoup.nodes.Attributes;
013import org.jsoup.nodes.Element;
014
015import java.util.HashMap;
016import java.util.HashSet;
017import java.util.Iterator;
018import java.util.Map;
019import java.util.Objects;
020import java.util.Set;
021
022import static org.jsoup.internal.Normalizer.lowerCase;
023
024
025/**
026 Safelists define what HTML (elements and attributes) to allow through a {@link Cleaner}. Everything else is removed.
027 <p>
028 Start with one of the defaults:
029 </p>
030 <ul>
031 <li>{@link #none}
032 <li>{@link #simpleText}
033 <li>{@link #basic}
034 <li>{@link #basicWithImages}
035 <li>{@link #relaxed}
036 </ul>
037 <p>
038 If you need to allow more through (please be careful!), tweak a base safelist with:
039 </p>
040 <ul>
041 <li>{@link #addTags(String... tagNames)}
042 <li>{@link #addAttributes(String tagName, String... attributes)}
043 <li>{@link #addEnforcedAttribute(String tagName, String attribute, String value)}
044 <li>{@link #addProtocols(String tagName, String attribute, String... protocols)}
045 </ul>
046 <p>
047 You can remove any setting from an existing safelist with:
048 </p>
049 <ul>
050 <li>{@link #removeTags(String... tagNames)}
051 <li>{@link #removeAttributes(String tagName, String... attributes)}
052 <li>{@link #removeEnforcedAttribute(String tagName, String attribute)}
053 <li>{@link #removeProtocols(String tagName, String attribute, String... removeProtocols)}
054 </ul>
055
056 <p>
057 The {@link Cleaner} and these safelists assume that you want to clean a <code>body</code> fragment of HTML (to add user
058 supplied HTML into a templated page), and not to clean a full HTML document. If the latter is the case, you could wrap
059 the templated document HTML around the cleaned body HTML.
060 </p>
061 <p>
062 Safelists are mutable. A {@link Cleaner} uses the supplied safelist directly, so later changes affect later cleaning
063 calls. If you want to share a safelist across threads, finish configuring it first and do not mutate it while it is in
064 use. To build a variant from an existing configuration, use {@link #Safelist(Safelist)} to make a copy.
065 </p>
066 <p>
067 If you are going to extend a safelist, please be very careful. Make sure you understand what attributes may lead to
068 XSS attack vectors. URL attributes are particularly vulnerable and require careful validation. See 
069 the <a href="https://owasp.org/www-community/xss-filter-evasion-cheatsheet">XSS Filter Evasion Cheat Sheet</a> for some
070 XSS attack examples (that jsoup will safeguard against with the default Cleaner and Safelist configuration).
071 </p>
072 */
073public class Safelist {
074    private static final String All = ":all";
075    private static final TagName AllTag = TagName.valueOf(All);
076    private final Set<TagName> tagNames; // tags allowed, lower case. e.g. [p, br, span]
077    private final Map<TagName, Set<AttributeKey>> attributes; // tag -> attribute[]. allowed attributes [href] for a tag.
078    private final Map<TagName, Map<AttributeKey, AttributeValue>> enforcedAttributes; // always set these attribute values
079    private final Map<TagName, Map<AttributeKey, Set<Protocol>>> protocols; // allowed URL protocols for attributes
080    private boolean preserveRelativeLinks; // option to preserve relative links
081
082    /**
083     This safelist allows only text nodes: any HTML Element or any Node other than a TextNode will be removed.
084     <p>
085     Note that the output of {@link org.jsoup.Jsoup#clean(String, Safelist)} is still <b>HTML</b> even when using
086     this Safelist, and so any HTML entities in the output will be appropriately escaped. If you want plain text, not
087     HTML, you should use a text method such as {@link Element#text()} instead, after cleaning the document.
088     </p>
089     <p>Example:</p>
090     <pre>{@code
091     String sourceBodyHtml = "<p>5 is &lt; 6.</p>";
092     String html = Jsoup.clean(sourceBodyHtml, Safelist.none());
093
094     Cleaner cleaner = new Cleaner(Safelist.none());
095     String text = cleaner.clean(Jsoup.parse(sourceBodyHtml)).text();
096
097     // html is: 5 is &lt; 6.
098     // text is: 5 is < 6.
099     }</pre>
100
101     @return safelist
102     */
103    public static Safelist none() {
104        return new Safelist();
105    }
106
107    /**
108     This safelist allows only simple text formatting: <code>b, em, i, strong, u</code>. All other HTML (tags and
109     attributes) will be removed.
110
111     @return safelist
112     */
113    public static Safelist simpleText() {
114        return new Safelist()
115                .addTags("b", "em", "i", "strong", "u")
116                ;
117    }
118
119    /**
120     <p>
121     This safelist allows a fuller range of text nodes: <code>a, b, blockquote, br, cite, code, dd, dl, dt, em, i, li,
122     ol, p, pre, q, small, span, strike, strong, sub, sup, u, ul</code>, and appropriate attributes.
123     </p>
124     <p>
125     Links (<code>a</code> elements) can point to <code>http, https, ftp, mailto</code>, and have an enforced
126     <code>rel=nofollow</code> attribute if they link offsite (as indicated by the specified base URI).
127     </p>
128     <p>
129     Does not allow images.
130     </p>
131
132     @return safelist
133     */
134    public static Safelist basic() {
135        return new Safelist()
136                .addTags(
137                        "a", "b", "blockquote", "br", "cite", "code", "dd", "dl", "dt", "em",
138                        "i", "li", "ol", "p", "pre", "q", "small", "span", "strike", "strong", "sub",
139                        "sup", "u", "ul")
140
141                .addAttributes("a", "href")
142                .addAttributes("blockquote", "cite")
143                .addAttributes("q", "cite")
144
145                .addProtocols("a", "href", "ftp", "http", "https", "mailto")
146                .addProtocols("blockquote", "cite", "http", "https")
147                .addProtocols("cite", "cite", "http", "https")
148
149                .addEnforcedAttribute("a", "rel", "nofollow") // has special handling for external links, in Cleaner
150                ;
151
152    }
153
154    /**
155     This safelist allows the same text tags as {@link #basic}, and also allows <code>img</code> tags, with appropriate
156     attributes, with <code>src</code> pointing to <code>http</code> or <code>https</code>.
157
158     @return safelist
159     */
160    public static Safelist basicWithImages() {
161        return basic()
162                .addTags("img")
163                .addAttributes("img", "align", "alt", "height", "src", "title", "width")
164                .addProtocols("img", "src", "http", "https")
165                ;
166    }
167
168    /**
169     This safelist allows a full range of text and structural body HTML: <code>a, b, blockquote, br, caption, cite,
170     code, col, colgroup, dd, div, dl, dt, em, h1, h2, h3, h4, h5, h6, i, img, li, ol, p, pre, q, small, span, strike, strong, sub,
171     sup, table, tbody, td, tfoot, th, thead, tr, u, ul</code>
172     <p>
173     Links do not have an enforced <code>rel=nofollow</code> attribute, but you can add that if desired.
174     </p>
175
176     @return safelist
177     */
178    public static Safelist relaxed() {
179        return new Safelist()
180                .addTags(
181                        "a", "b", "blockquote", "br", "caption", "cite", "code", "col",
182                        "colgroup", "dd", "div", "dl", "dt", "em", "h1", "h2", "h3", "h4", "h5", "h6",
183                        "i", "img", "li", "ol", "p", "pre", "q", "small", "span", "strike", "strong",
184                        "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "u",
185                        "ul")
186
187                .addAttributes("a", "href", "title")
188                .addAttributes("blockquote", "cite")
189                .addAttributes("col", "span", "width")
190                .addAttributes("colgroup", "span", "width")
191                .addAttributes("img", "align", "alt", "height", "src", "title", "width")
192                .addAttributes("ol", "start", "type")
193                .addAttributes("q", "cite")
194                .addAttributes("table", "summary", "width")
195                .addAttributes("td", "abbr", "axis", "colspan", "rowspan", "width")
196                .addAttributes(
197                        "th", "abbr", "axis", "colspan", "rowspan", "scope",
198                        "width")
199                .addAttributes("ul", "type")
200
201                .addProtocols("a", "href", "ftp", "http", "https", "mailto")
202                .addProtocols("blockquote", "cite", "http", "https")
203                .addProtocols("cite", "cite", "http", "https")
204                .addProtocols("img", "src", "http", "https")
205                .addProtocols("q", "cite", "http", "https")
206                ;
207    }
208
209    /**
210     Create a new, empty safelist. Generally it will be better to start with a default prepared safelist instead.
211
212     @see #basic()
213     @see #basicWithImages()
214     @see #simpleText()
215     @see #relaxed()
216     */
217    public Safelist() {
218        tagNames = new HashSet<>();
219        attributes = new HashMap<>();
220        enforcedAttributes = new HashMap<>();
221        protocols = new HashMap<>();
222        preserveRelativeLinks = false;
223    }
224
225    /**
226     Deep copy an existing Safelist to a new Safelist.
227     @param copy the Safelist to copy
228     */
229    public Safelist(Safelist copy) {
230        this();
231        tagNames.addAll(copy.tagNames);
232        for (Map.Entry<TagName, Set<AttributeKey>> copyTagAttributes : copy.attributes.entrySet()) {
233            attributes.put(copyTagAttributes.getKey(), new HashSet<>(copyTagAttributes.getValue()));
234        }
235        for (Map.Entry<TagName, Map<AttributeKey, AttributeValue>> enforcedEntry : copy.enforcedAttributes.entrySet()) {
236            enforcedAttributes.put(enforcedEntry.getKey(), new HashMap<>(enforcedEntry.getValue()));
237        }
238        for (Map.Entry<TagName, Map<AttributeKey, Set<Protocol>>> protocolsEntry : copy.protocols.entrySet()) {
239            Map<AttributeKey, Set<Protocol>> attributeProtocolsCopy = new HashMap<>();
240            for (Map.Entry<AttributeKey, Set<Protocol>> attributeProtocols : protocolsEntry.getValue().entrySet()) {
241                attributeProtocolsCopy.put(attributeProtocols.getKey(), new HashSet<>(attributeProtocols.getValue()));
242            }
243            protocols.put(protocolsEntry.getKey(), attributeProtocolsCopy);
244        }
245        preserveRelativeLinks = copy.preserveRelativeLinks;
246    }
247
248    /**
249     Add a list of allowed elements to a safelist. (If a tag is not allowed, it will be removed from the HTML.)
250
251     @param tags tag names to allow
252     @return this (for chaining)
253     */
254    public Safelist addTags(String... tags) {
255        Validate.notNull(tags);
256
257        for (String tagName : tags) {
258            Validate.notEmpty(tagName);
259            Validate.isFalse(tagName.equalsIgnoreCase("noscript"),
260                "noscript is unsupported in Safelists, due to incompatibilities between parsers with and without script-mode enabled");
261            tagNames.add(TagName.valueOf(tagName));
262        }
263        return this;
264    }
265
266    /**
267     Remove a list of allowed elements from a safelist. (If a tag is not allowed, it will be removed from the HTML.)
268
269     @param tags tag names to disallow
270     @return this (for chaining)
271     */
272    public Safelist removeTags(String... tags) {
273        Validate.notNull(tags);
274
275        for(String tag: tags) {
276            Validate.notEmpty(tag);
277            TagName tagName = TagName.valueOf(tag);
278
279            if(tagNames.remove(tagName)) { // Only look in sub-maps if tag was allowed
280                attributes.remove(tagName);
281                enforcedAttributes.remove(tagName);
282                protocols.remove(tagName);
283            }
284        }
285        return this;
286    }
287
288    /**
289     Add a list of allowed attributes to a tag. (If an attribute is not allowed on an element, it will be removed.)
290     <p>
291     E.g.: <code>addAttributes("a", "href", "class")</code> allows <code>href</code> and <code>class</code> attributes
292     on <code>a</code> tags.
293     </p>
294     <p>
295     To make an attribute valid for <b>all tags</b>, use the pseudo tag <code>:all</code>, e.g.
296     <code>addAttributes(":all", "class")</code>.
297     </p>
298
299     @param tag  The tag the attributes are for. The tag will be added to the allowed tag list if necessary.
300     @param attributes List of valid attributes for the tag
301     @return this (for chaining)
302     */
303    public Safelist addAttributes(String tag, String... attributes) {
304        Validate.notEmpty(tag);
305        Validate.notNull(attributes);
306        Validate.isTrue(attributes.length > 0, "No attribute names supplied.");
307
308        addTags(tag);
309        TagName tagName = TagName.valueOf(tag);
310        Set<AttributeKey> attributeSet = new HashSet<>();
311        for (String key : attributes) {
312            Validate.notEmpty(key);
313            attributeSet.add(AttributeKey.valueOf(key));
314        }
315        Set<AttributeKey> currentSet = this.attributes.computeIfAbsent(tagName, k -> new HashSet<>());
316        currentSet.addAll(attributeSet);
317        return this;
318    }
319
320    /**
321     Remove a list of allowed attributes from a tag. (If an attribute is not allowed on an element, it will be removed.)
322     <p>
323     E.g.: <code>removeAttributes("a", "href", "class")</code> disallows <code>href</code> and <code>class</code>
324     attributes on <code>a</code> tags.
325     </p>
326     <p>
327     To make an attribute invalid for <b>all tags</b>, use the pseudo tag <code>:all</code>, e.g.
328     <code>removeAttributes(":all", "class")</code>.
329     </p>
330
331     @param tag  The tag the attributes are for.
332     @param attributes List of invalid attributes for the tag
333     @return this (for chaining)
334     */
335    public Safelist removeAttributes(String tag, String... attributes) {
336        Validate.notEmpty(tag);
337        Validate.notNull(attributes);
338        Validate.isTrue(attributes.length > 0, "No attribute names supplied.");
339
340        TagName tagName = TagName.valueOf(tag);
341        Set<AttributeKey> attributeSet = new HashSet<>();
342        for (String key : attributes) {
343            Validate.notEmpty(key);
344            attributeSet.add(AttributeKey.valueOf(key));
345        }
346        if(tagNames.contains(tagName) && this.attributes.containsKey(tagName)) { // Only look in sub-maps if tag was allowed
347            Set<AttributeKey> currentSet = this.attributes.get(tagName);
348            currentSet.removeAll(attributeSet);
349
350            if(currentSet.isEmpty()) // Remove tag from attribute map if no attributes are allowed for tag
351                this.attributes.remove(tagName);
352        }
353        if(tag.equals(All)) { // Attribute needs to be removed from all individually set tags
354            Iterator<Map.Entry<TagName, Set<AttributeKey>>> it = this.attributes.entrySet().iterator();
355            while (it.hasNext()) {
356                Map.Entry<TagName, Set<AttributeKey>> entry = it.next();
357                Set<AttributeKey> currentSet = entry.getValue();
358                currentSet.removeAll(attributeSet);
359                if(currentSet.isEmpty()) // Remove tag from attribute map if no attributes are allowed for tag
360                    it.remove();
361            }
362        }
363        return this;
364    }
365
366    /**
367     Add an enforced attribute to a tag. An enforced attribute will always be added to the element. If the element
368     already has the attribute set, it will be overridden with this value.
369     <p>
370     E.g.: <code>addEnforcedAttribute("a", "rel", "nofollow")</code> will make all <code>a</code> tags output as
371     <code>&lt;a href="..." rel="nofollow"&gt;</code>
372     </p>
373
374     @param tag   The tag the enforced attribute is for. The tag will be added to the allowed tag list if necessary.
375     @param attribute   The attribute name
376     @param value The enforced attribute value
377     @return this (for chaining)
378     */
379    public Safelist addEnforcedAttribute(String tag, String attribute, String value) {
380        Validate.notEmpty(tag);
381        Validate.notEmpty(attribute);
382        Validate.notEmpty(value);
383
384        TagName tagName = TagName.valueOf(tag);
385        tagNames.add(tagName);
386        AttributeKey attrKey = AttributeKey.valueOf(attribute);
387        AttributeValue attrVal = AttributeValue.valueOf(value);
388
389        Map<AttributeKey, AttributeValue> attrMap = enforcedAttributes.computeIfAbsent(tagName, k -> new HashMap<>());
390        attrMap.put(attrKey, attrVal);
391        return this;
392    }
393
394    /**
395     Remove a previously configured enforced attribute from a tag.
396
397     @param tag   The tag the enforced attribute is for.
398     @param attribute   The attribute name
399     @return this (for chaining)
400     */
401    public Safelist removeEnforcedAttribute(String tag, String attribute) {
402        Validate.notEmpty(tag);
403        Validate.notEmpty(attribute);
404
405        TagName tagName = TagName.valueOf(tag);
406        if(tagNames.contains(tagName) && enforcedAttributes.containsKey(tagName)) {
407            AttributeKey attrKey = AttributeKey.valueOf(attribute);
408            Map<AttributeKey, AttributeValue> attrMap = enforcedAttributes.get(tagName);
409            attrMap.remove(attrKey);
410
411            if(attrMap.isEmpty()) // Remove tag from enforced attribute map if no enforced attributes are present
412                enforcedAttributes.remove(tagName);
413        }
414        return this;
415    }
416
417    /**
418     * Configure this Safelist to preserve relative links in an element's URL attribute, or convert them to absolute
419     * links. By default, this is <b>false</b>: URLs will be  made absolute (e.g. start with an allowed protocol, like
420     * e.g. {@code http://}.
421     *
422     * @param preserve {@code true} to allow relative links, {@code false} (default) to deny
423     * @return this Safelist, for chaining.
424     * @see #addProtocols
425     */
426    public Safelist preserveRelativeLinks(boolean preserve) {
427        preserveRelativeLinks = preserve;
428        return this;
429    }
430
431    /**
432     * Get the current setting for preserving relative links.
433     * @return {@code true} if relative links are preserved, {@code false} if they are converted to absolute.
434     */
435    public boolean preserveRelativeLinks() {
436        return preserveRelativeLinks;
437    }
438
439    /**
440     Add allowed URL protocols for an element's URL attribute. This restricts the possible values of the attribute to
441     URLs with the defined protocol.
442     <p>
443     E.g.: <code>addProtocols("a", "href", "ftp", "http", "https")</code>
444     </p>
445     <p>
446     To allow a link to an in-page URL anchor (i.e. <code>&lt;a href="#anchor"&gt;</code>, add a <code>#</code>:<br>
447     E.g.: <code>addProtocols("a", "href", "#")</code>
448     </p>
449
450     @param tag       Tag the URL protocol is for
451     @param attribute       Attribute name
452     @param protocols List of valid protocols
453     @return this, for chaining
454     */
455    public Safelist addProtocols(String tag, String attribute, String... protocols) {
456        Validate.notEmpty(tag);
457        Validate.notEmpty(attribute);
458        Validate.notNull(protocols);
459
460        TagName tagName = TagName.valueOf(tag);
461        AttributeKey attrKey = AttributeKey.valueOf(attribute);
462        Map<AttributeKey, Set<Protocol>> attrMap = this.protocols.computeIfAbsent(tagName, k -> new HashMap<>());
463        Set<Protocol> protSet = attrMap.computeIfAbsent(attrKey, k -> new HashSet<>());
464
465        for (String protocol : protocols) {
466            Validate.notEmpty(protocol);
467            Protocol prot = Protocol.valueOf(protocol);
468            protSet.add(prot);
469        }
470        return this;
471    }
472
473    /**
474     Remove allowed URL protocols for an element's URL attribute. If you remove all protocols for an attribute, that
475     attribute will allow any protocol.
476     <p>
477     E.g.: <code>removeProtocols("a", "href", "ftp")</code>
478     </p>
479
480     @param tag Tag the URL protocol is for
481     @param attribute Attribute name
482     @param removeProtocols List of invalid protocols
483     @return this, for chaining
484     */
485    public Safelist removeProtocols(String tag, String attribute, String... removeProtocols) {
486        Validate.notEmpty(tag);
487        Validate.notEmpty(attribute);
488        Validate.notNull(removeProtocols);
489
490        TagName tagName = TagName.valueOf(tag);
491        AttributeKey attr = AttributeKey.valueOf(attribute);
492
493        // make sure that what we're removing actually exists; otherwise can open the tag to any data and that can
494        // be surprising
495        Validate.isTrue(protocols.containsKey(tagName), "Cannot remove a protocol that is not set.");
496        Map<AttributeKey, Set<Protocol>> tagProtocols = protocols.get(tagName);
497        Validate.isTrue(tagProtocols.containsKey(attr), "Cannot remove a protocol that is not set.");
498
499        Set<Protocol> attrProtocols = tagProtocols.get(attr);
500        for (String protocol : removeProtocols) {
501            Validate.notEmpty(protocol);
502            attrProtocols.remove(Protocol.valueOf(protocol));
503        }
504
505        if (attrProtocols.isEmpty()) { // Remove protocol set if empty
506            tagProtocols.remove(attr);
507            if (tagProtocols.isEmpty()) // Remove entry for tag if empty
508                protocols.remove(tagName);
509        }
510        return this;
511    }
512
513    /**
514     * Test if the supplied tag is allowed by this safelist.
515     * @param tag test tag
516     * @return true if allowed
517     */
518    public boolean isSafeTag(String tag) {
519        return tagNames.contains(TagName.valueOf(tag));
520    }
521
522    /**
523     * Test if the supplied attribute is allowed by this safelist for this tag.
524     * <p>This method does not modify the input element or attribute.</p>
525     * @param tagName tag to consider allowing the attribute in
526     * @param el element under test, to confirm protocol
527     * @param attr attribute under test
528     * @return true if allowed
529     */
530    public boolean isSafeAttribute(String tagName, Element el, Attribute attr) {
531        TagName tag = TagName.valueOf(tagName);
532        AttributeKey key = AttributeKey.valueOf(attr.getKey());
533
534        Set<AttributeKey> okSet = attributes.get(tag);
535        if (okSet != null && okSet.contains(key)) {
536            if (protocols.containsKey(tag)) {
537                Map<AttributeKey, Set<Protocol>> attrProts = protocols.get(tag);
538                // ok if not defined protocol; otherwise test
539                return !attrProts.containsKey(key) || isSafeProtocol(getProtocolValue(el, attr), attrProts.get(key));
540            } else { // attribute found, no protocols defined, so OK
541                return true;
542            }
543        }
544        Map<AttributeKey, AttributeValue> enforcedSet = enforcedAttributes.get(tag);
545        if (enforcedSet != null && enforcedSet.containsKey(key)) {
546            // enforced attr key was LCed via AttributeKey.valueOf(attr.getKey()),
547            // if the input already has that exact value, treat it as safe
548            return enforcedSet.get(key).equals(AttributeValue.valueOf(attr.getValue()));
549        }
550        // no attributes defined for tag, try :all tag
551        return !tagName.equals(All) && isSafeAttribute(All, el, attr);
552    }
553
554    private String getProtocolValue(Element el, Attribute attr) {
555        String value = el.absUrl(attr.getKey());
556        if (value.isEmpty() && !StringUtil.hasHttpScheme(attr.getValue()))
557            value = attr.getValue(); // if it could not be made abs, run as-is to allow custom unknown protocols
558        return value;
559    }
560
561    private boolean isSafeProtocol(String value, Set<Protocol> protocols) {
562        for (Protocol protocol : protocols) {
563            String prot = protocol.toString();
564
565            if (prot.equals("#")) { // allows anchor links
566                if (isValidAnchor(value)) {
567                    return true;
568                } else {
569                    continue;
570                }
571            }
572
573            String lc = lowerCase(value);
574            if (lc.startsWith(prot)
575                && lc.length() > prot.length()
576                && lc.charAt(prot.length()) == ':') {
577                return true;
578            }
579        }
580        return false;
581    }
582
583    /**
584     Check if a URL attribute should be normalized to an absolute URL in the cleaned output. Uses the configured
585     protocols for that tag+attribute pair, falling back to {@code :all} only if the tag does not define the
586     attribute.
587     */
588    boolean shouldAbsUrl(String tagName, String attrKey) {
589        if (preserveRelativeLinks) return false;
590        return shouldAbsUrl(TagName.valueOf(tagName), AttributeKey.valueOf(attrKey));
591    }
592
593    private boolean shouldAbsUrl(TagName tag, AttributeKey key) {
594        Set<AttributeKey> allowedAttrs = attributes.get(tag);
595        if (allowedAttrs != null && allowedAttrs.contains(key)) {
596            Map<AttributeKey, Set<Protocol>> protocolsByAttr = protocols.get(tag);
597            return protocolsByAttr != null && protocolsByAttr.containsKey(key);
598        }
599
600        Map<AttributeKey, AttributeValue> enforcedAttrs = enforcedAttributes.get(tag);
601        if (enforcedAttrs != null && enforcedAttrs.containsKey(key)) return false;
602
603        return !tag.equals(AllTag) && shouldAbsUrl(AllTag, key);
604    }
605
606    private static boolean isValidAnchor(String value) {
607        return value.startsWith("#") && !value.matches(".*\\s.*");
608    }
609
610    /**
611     Gets the Attributes that should be enforced for a given tag
612     * @param tagName the tag
613     * @return the attributes that will be enforced; empty if none are set for the given tag
614     */
615    public Attributes getEnforcedAttributes(String tagName) {
616        Attributes attrs = new Attributes();
617        TagName tag = TagName.valueOf(tagName);
618        if (enforcedAttributes.containsKey(tag)) {
619            Map<AttributeKey, AttributeValue> keyVals = enforcedAttributes.get(tag);
620            for (Map.Entry<AttributeKey, AttributeValue> entry : keyVals.entrySet()) {
621                attrs.put(entry.getKey().toString(), entry.getValue().toString());
622            }
623        }
624        return attrs;
625    }
626    
627    // named types for config. All just hold strings, but here for my sanity.
628
629    static class TagName extends TypedValue {
630        TagName(String value) {
631            super(value);
632        }
633
634        static TagName valueOf(String value) {
635            return new TagName(Normalizer.lowerCase(value));
636        }
637    }
638
639    static class AttributeKey extends TypedValue {
640        AttributeKey(String value) {
641            super(value);
642        }
643
644        static AttributeKey valueOf(String value) {
645            return new AttributeKey(Normalizer.lowerCase(value));
646        }
647    }
648
649    static class AttributeValue extends TypedValue {
650        AttributeValue(String value) {
651            super(value);
652        }
653
654        static AttributeValue valueOf(String value) {
655            return new AttributeValue(value);
656        }
657    }
658
659    static class Protocol extends TypedValue {
660        Protocol(String value) {
661            super(value);
662        }
663
664        static Protocol valueOf(String value) {
665            return new Protocol(value);
666        }
667    }
668
669    abstract static class TypedValue {
670        private final String value;
671
672        TypedValue(String value) {
673            Validate.notNull(value);
674            this.value = value;
675        }
676
677        @Override
678        public int hashCode() {
679            return value.hashCode();
680        }
681
682        @Override
683        public boolean equals(Object obj) {
684            if (this == obj) return true;
685            if (obj == null || getClass() != obj.getClass()) return false;
686            TypedValue other = (TypedValue) obj;
687            return Objects.equals(value, other.value);
688        }
689
690        @Override
691        public String toString() {
692            return value;
693        }
694    }
695}