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 < 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 < 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><a href="..." rel="nofollow"></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><a href="#anchor"></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}