001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software GmbH & Co. KG, please see the 018 * company website: https://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: https://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.staticexport; 029 030import org.opencms.ade.configuration.CmsADEConfigData; 031import org.opencms.file.CmsObject; 032import org.opencms.file.CmsPropertyDefinition; 033import org.opencms.file.wrapper.CmsObjectWrapper; 034import org.opencms.gwt.shared.CmsGwtConstants; 035import org.opencms.i18n.CmsEncoder; 036import org.opencms.main.CmsException; 037import org.opencms.main.CmsLog; 038import org.opencms.main.OpenCms; 039import org.opencms.relations.CmsLink; 040import org.opencms.relations.CmsRelationType; 041import org.opencms.site.CmsSite; 042import org.opencms.site.CmsSiteMatcher; 043import org.opencms.util.CmsHtmlParser; 044import org.opencms.util.CmsMacroResolver; 045import org.opencms.util.CmsRequestUtil; 046import org.opencms.util.CmsStringUtil; 047import org.opencms.util.CmsUUID; 048 049import java.net.URI; 050import java.net.URISyntaxException; 051import java.util.Arrays; 052import java.util.Collections; 053import java.util.HashSet; 054import java.util.List; 055import java.util.Map; 056import java.util.Set; 057import java.util.Vector; 058import java.util.concurrent.ExecutionException; 059import java.util.concurrent.TimeUnit; 060import java.util.stream.Collectors; 061 062import org.apache.commons.lang3.StringUtils; 063import org.apache.commons.logging.Log; 064 065import org.htmlparser.Attribute; 066import org.htmlparser.Node; 067import org.htmlparser.Tag; 068import org.htmlparser.tags.ImageTag; 069import org.htmlparser.tags.LinkTag; 070import org.htmlparser.tags.ObjectTag; 071import org.htmlparser.util.ParserException; 072import org.htmlparser.util.SimpleNodeIterator; 073 074import com.google.common.cache.Cache; 075import com.google.common.cache.CacheBuilder; 076 077/** 078 * Implements the HTML parser node visitor pattern to 079 * exchange all links on the page.<p> 080 * 081 * @since 6.0.0 082 */ 083public class CmsLinkProcessor extends CmsHtmlParser { 084 085 /** 086 * Holds information about external link domain whitelists. 087 */ 088 public static class ExternalLinkWhitelistInfo { 089 090 /** The list of site roots matching the whitelist. */ 091 private Set<String> m_siteRoots; 092 093 /** The whitelist itself. */ 094 private Set<String> m_whitelistEntries; 095 096 /** 097 * Creates a new instance 098 * 099 * @param whitelistEntries the whitelist entries 100 * @param siteRoots the site roots matching the whitelist 101 */ 102 public ExternalLinkWhitelistInfo(Set<String> whitelistEntries, Set<String> siteRoots) { 103 104 m_siteRoots = siteRoots; 105 m_whitelistEntries = whitelistEntries; 106 } 107 108 /** 109 * Gets the site roots matching the whitelist 110 * 111 * @return the matching site roots 112 */ 113 public Set<String> getSiteRoots() { 114 115 return m_siteRoots; 116 } 117 118 /** 119 * Gets the whitelist entries 120 * @return the whitelist entries 121 */ 122 public Set<String> getWhitelistEntries() { 123 124 return m_whitelistEntries; 125 } 126 127 } 128 129 /** Context attribute used to mark when we are in the link processing stage (expanding macros into links). */ 130 public static final String ATTR_IS_PROCESSING_LINKS = "isInLinkProcessor"; 131 132 /** Constant for the attribute name. */ 133 public static final String ATTRIBUTE_HREF = "href"; 134 135 /** Constant for the attribute name. */ 136 public static final String ATTRIBUTE_SRC = "src"; 137 138 /** Constant for the attribute name. */ 139 public static final String ATTRIBUTE_VALUE = "value"; 140 141 /** HTML end. */ 142 public static final String HTML_END = "</body></html>"; 143 144 /** HTML start. */ 145 public static final String HTML_START = "<html><body>"; 146 147 /** Constant for the tag name. */ 148 public static final String TAG_AREA = "AREA"; 149 150 /** Constant for the tag name. */ 151 public static final String TAG_EMBED = "EMBED"; 152 153 /** Constant for the tag name. */ 154 public static final String TAG_IFRAME = "IFRAME"; 155 156 /** Constant for the tag name. */ 157 public static final String TAG_PARAM = "PARAM"; 158 159 /** List of attributes that may contain links for the embed tag. */ 160 private static final String[] EMBED_TAG_LINKED_ATTRIBS = new String[] {ATTRIBUTE_SRC, "pluginurl", "pluginspage"}; 161 162 /** List of attributes that may contain links for the object tag ("codebase" has to be first). */ 163 private static final String[] OBJECT_TAG_LINKED_ATTRIBS = new String[] {"codebase", "data", "datasrc"}; 164 165 /** Processing mode "process links" (macros to links). */ 166 private static final int PROCESS_LINKS = 1; 167 168 /** Processing mode "replace links" (links to macros). */ 169 private static final int REPLACE_LINKS = 0; 170 171 private static final Log LOG = CmsLog.getLog(CmsLinkProcessor.class); 172 173 /** Cache for site roots of internal sites that should not be marked as external, according to the sitemap configuration. */ 174 private static Cache<String, ExternalLinkWhitelistInfo> externalLinkWhitelistCache = CacheBuilder.newBuilder().expireAfterWrite( 175 5, 176 TimeUnit.SECONDS).concurrencyLevel(4).build(); 177 178 /** The current users OpenCms context, containing the users permission and site root context. */ 179 private CmsObject m_cms; 180 181 /** The selected encoding to use for parsing the HTML. */ 182 private String m_encoding; 183 184 /** The link table used for link macro replacements. */ 185 private CmsLinkTable m_linkTable; 186 187 /** Current processing mode. */ 188 private int m_mode; 189 190 /** The relative path for relative links, if not set, relative links are treated as external links. */ 191 private String m_relativePath; 192 193 /** Another OpenCms context based on the current users OpenCms context, but with the site root set to '/'. */ 194 private CmsObject m_rootCms; 195 196 /** 197 * Creates a new link processor.<p> 198 * 199 * @param cms the current users OpenCms context 200 * @param linkTable the link table to use 201 * @param encoding the encoding to use for parsing the HTML content 202 * @param relativePath additional path for links with relative path (only used in "replace" mode) 203 */ 204 public CmsLinkProcessor(CmsObject cms, CmsLinkTable linkTable, String encoding, String relativePath) { 205 206 // echo mode must be on for link processor 207 super(true); 208 209 m_cms = cms; 210 if (m_cms != null) { 211 try { 212 m_rootCms = OpenCms.initCmsObject(cms); 213 m_rootCms.getRequestContext().setSiteRoot("/"); 214 } catch (CmsException e) { 215 // this should not happen 216 m_rootCms = null; 217 } 218 } 219 m_linkTable = linkTable; 220 m_encoding = encoding; 221 m_relativePath = relativePath; 222 } 223 224 /** 225 * Escapes all <code>&</code>, e.g. replaces them with a <code>&</code>.<p> 226 * 227 * @param source the String to escape 228 * @return the escaped String 229 */ 230 public static String escapeLink(String source) { 231 232 if (source == null) { 233 return null; 234 } 235 StringBuffer result = new StringBuffer(source.length() * 2); 236 int terminatorIndex; 237 for (int i = 0; i < source.length(); ++i) { 238 char ch = source.charAt(i); 239 switch (ch) { 240 case '&': 241 // don't escape already escaped &s; 242 terminatorIndex = source.indexOf(';', i); 243 if (terminatorIndex > 0) { 244 String substr = source.substring(i + 1, terminatorIndex); 245 if ("amp".equals(substr)) { 246 result.append(ch); 247 } else { 248 result.append("&"); 249 } 250 } else { 251 result.append("&"); 252 } 253 break; 254 default: 255 result.append(ch); 256 } 257 } 258 return new String(result); 259 } 260 261 /** 262 * Gets the list of site roots of sites in OpenCms which should be considered as internal, in the sense of not having to mark 263 * the corresponding link tags with the external link marker. 264 * 265 * @param cms the current CMS context 266 * 267 * @return the external link whitelist site roots 268 */ 269 public static ExternalLinkWhitelistInfo getExternalLinkWhitelistInfo(CmsObject cms) { 270 271 CmsADEConfigData sitemapConfig = OpenCms.getADEManager().lookupConfigurationWithCache( 272 cms, 273 cms.getRequestContext().getRootUri()); 274 String whitelist = sitemapConfig.getAttribute("template.editor.links.externalWhitelist", ""); 275 String cacheKey = "" + cms.getRequestContext().getCurrentProject().isOnlineProject() + ":" + whitelist; 276 try { 277 return externalLinkWhitelistCache.get(cacheKey, () -> { 278 279 Set<String> whitelistEntries = Arrays.asList(whitelist.split(",")).stream().map( 280 entry -> entry.trim()).filter(entry -> !CmsStringUtil.isEmptyOrWhitespaceOnly(entry)).collect( 281 Collectors.toSet()); 282 Set<String> siteRoots = new HashSet<>(); 283 for (String whitelistEntry : whitelistEntries) { 284 for (Map.Entry<CmsSiteMatcher, CmsSite> siteEntry : OpenCms.getSiteManager().getSites().entrySet()) { 285 CmsSiteMatcher key = siteEntry.getKey(); 286 try { 287 URI uri = new URI(key.getUrl()); 288 String host = uri.getHost(); 289 if (host != null) { 290 if (host.equals(whitelistEntry) || host.endsWith("." + whitelistEntry)) { 291 siteRoots.add(siteEntry.getValue().getSiteRoot()); 292 } 293 } 294 } catch (Exception e) { 295 LOG.info(e.getLocalizedMessage(), e); 296 } 297 298 } 299 } 300 return new ExternalLinkWhitelistInfo( 301 Collections.unmodifiableSet(whitelistEntries), 302 Collections.unmodifiableSet(siteRoots)); 303 }); 304 } catch (ExecutionException e) { 305 LOG.error(e.getLocalizedMessage(), e); 306 return new ExternalLinkWhitelistInfo(new HashSet<>(), new HashSet<>()); 307 } 308 309 } 310 311 /** 312 * Checks if the link should be marked as external. 313 * 314 * @param cms the current CMS context 315 * @param sitemapConfig the current sitemap configuration 316 * @param link the link to check 317 * @return true if the link should be be marked as external 318 */ 319 public static boolean shouldMarkAsExternal(CmsObject cms, CmsADEConfigData sitemapConfig, CmsLink link) { 320 321 boolean markAsExternal = false; 322 if (link.isInternal()) { 323 String target = link.getTarget(); 324 if ((target != null) && target.startsWith("/")) { 325 CmsSite site = OpenCms.getSiteManager().getSiteForRootPath(target); 326 CmsSite currentSite = OpenCms.getSiteManager().getSiteForRootPath(cms.getRequestContext().getRootUri()); 327 if ((site != null) && (currentSite != null) && !currentSite.getSiteRoot().equals(site.getSiteRoot())) { 328 markAsExternal = true; 329 } 330 } 331 } else { 332 markAsExternal = true; 333 } 334 final String siteRoot = link.getSiteRoot(); 335 if (markAsExternal) { 336 ExternalLinkWhitelistInfo whitelistInfo = getExternalLinkWhitelistInfo(cms); 337 Set<String> whitelistSiteRoots = whitelistInfo.getSiteRoots(); 338 try { 339 URI uri = new URI(link.getUri()); 340 if (uri.getHost() != null) { 341 if (whitelistInfo.getWhitelistEntries().stream().anyMatch( 342 entry -> entry.equals(uri.getHost()) || StringUtils.endsWith(uri.getHost(), "." + entry))) { 343 markAsExternal = false; 344 } 345 } else if (siteRoot != null) { 346 if (whitelistSiteRoots.contains(siteRoot)) { 347 markAsExternal = false; 348 } 349 } 350 } catch (URISyntaxException e) { 351 LOG.debug(e.getLocalizedMessage(), e); 352 } 353 } 354 return markAsExternal; 355 } 356 357 /** 358 * Unescapes all <code>&amp;</code>, that is replaces them with a <code>&</code>.<p> 359 * 360 * @param source the String to unescape 361 * @return the unescaped String 362 */ 363 public static String unescapeLink(String source) { 364 365 if (source == null) { 366 return null; 367 } 368 return CmsStringUtil.substitute(source, "&", "&"); 369 370 } 371 372 /** 373 * Returns the link table this link processor was initialized with.<p> 374 * 375 * @return the link table this link processor was initialized with 376 */ 377 public CmsLinkTable getLinkTable() { 378 379 return m_linkTable; 380 } 381 382 /** 383 * Starts link processing for the given content in processing mode.<p> 384 * 385 * Macros are replaced by links.<p> 386 * 387 * @param content the content to process 388 * @return the processed content with replaced macros 389 * 390 * @throws ParserException if something goes wrong 391 */ 392 public String processLinks(String content) throws ParserException { 393 394 m_mode = PROCESS_LINKS; 395 396 if (m_cms != null) { 397 m_cms.getRequestContext().setAttribute(ATTR_IS_PROCESSING_LINKS, Boolean.TRUE); 398 } 399 try { 400 return process(content, m_encoding); 401 } finally { 402 if (m_cms != null) { 403 m_cms.getRequestContext().removeAttribute(ATTR_IS_PROCESSING_LINKS); 404 } 405 } 406 407 } 408 409 /** 410 * Starts link processing for the given content in replacement mode.<p> 411 * 412 * Links are replaced by macros.<p> 413 * 414 * @param content the content to process 415 * @return the processed content with replaced links 416 * 417 * @throws ParserException if something goes wrong 418 */ 419 public String replaceLinks(String content) throws ParserException { 420 421 m_mode = REPLACE_LINKS; 422 return process(content, m_encoding); 423 } 424 425 /** 426 * Visitor method to process a tag (start).<p> 427 * 428 * @param tag the tag to process 429 */ 430 @Override 431 public void visitTag(Tag tag) { 432 433 if (tag instanceof LinkTag) { 434 processLinkTag((LinkTag)tag); 435 } else if (tag instanceof ImageTag) { 436 processImageTag((ImageTag)tag); 437 } else if (tag instanceof ObjectTag) { 438 processObjectTag((ObjectTag)tag); 439 } else { 440 // there are no specialized tag classes for these tags :( 441 if (TAG_EMBED.equals(tag.getTagName())) { 442 processEmbedTag(tag); 443 } else if (TAG_AREA.equals(tag.getTagName())) { 444 processAreaTag(tag); 445 } else if (TAG_IFRAME.equals(tag.getTagName())) { 446 String src = tag.getAttribute(ATTRIBUTE_SRC); 447 if ((src != null) && !src.startsWith("//")) { 448 // link processing does not work for protocol-relative URLs, which were once used in Youtube embed 449 // codes. 450 processLink(tag, ATTRIBUTE_SRC, CmsRelationType.HYPERLINK); 451 } 452 } 453 } 454 // append text content of the tag (may have been changed by above methods) 455 super.visitTag(tag); 456 } 457 458 /** 459 * Process an area tag.<p> 460 * 461 * @param tag the tag to process 462 */ 463 protected void processAreaTag(Tag tag) { 464 465 processLink(tag, ATTRIBUTE_HREF, CmsRelationType.HYPERLINK); 466 } 467 468 /** 469 * Process an embed tag.<p> 470 * 471 * @param tag the tag to process 472 */ 473 protected void processEmbedTag(Tag tag) { 474 475 for (int i = 0; i < EMBED_TAG_LINKED_ATTRIBS.length; i++) { 476 String attr = EMBED_TAG_LINKED_ATTRIBS[i]; 477 processLink(tag, attr, CmsRelationType.EMBEDDED_OBJECT); 478 } 479 } 480 481 /** 482 * Process an image tag.<p> 483 * 484 * @param tag the tag to process 485 */ 486 protected void processImageTag(ImageTag tag) { 487 488 processLink(tag, ATTRIBUTE_SRC, CmsRelationType.valueOf(tag.getTagName())); 489 } 490 491 /** 492 * Process a tag having a link in the given attribute, considering the link as the given type.<p> 493 * 494 * @param tag the tag to process 495 * @param attr the attribute 496 * @param type the link type 497 */ 498 protected void processLink(Tag tag, String attr, CmsRelationType type) { 499 500 if (tag.getAttribute(attr) == null) { 501 return; 502 } 503 CmsLink link = null; 504 505 switch (m_mode) { 506 case PROCESS_LINKS: 507 // macros are replaced with links 508 link = m_linkTable.getLink(CmsMacroResolver.stripMacro(tag.getAttribute(attr))); 509 if (link != null) { 510 // link management check 511 String l = link.getLink(m_cms); 512 if (TAG_PARAM.equals(tag.getTagName())) { 513 // HACK: to distinguish link parameters the link itself has to end with '&' or '?' 514 // another solution should be a kind of macro... 515 if (!l.endsWith(CmsRequestUtil.URL_DELIMITER) 516 && !l.endsWith(CmsRequestUtil.PARAMETER_DELIMITER)) { 517 if (l.indexOf(CmsRequestUtil.URL_DELIMITER) > 0) { 518 l += CmsRequestUtil.PARAMETER_DELIMITER; 519 } else { 520 l += CmsRequestUtil.URL_DELIMITER; 521 } 522 } 523 } 524 // set the real target 525 tag.setAttribute(attr, CmsEncoder.escapeXml(l)); 526 527 // In the Online project, remove href attributes with broken links from A tags. 528 // Exception: We don't do this if the target is empty, because fragment links ('#anchor') 529 // in the WYSIWYG editor are stored as internal links with empty targets 530 if (tag.getTagName().equalsIgnoreCase("A") 531 && m_cms.getRequestContext().isOnlineOrEditDisabled() 532 && link.isInternal() 533 && !CmsStringUtil.isEmpty(link.getTarget()) 534 && (link.getResource() == null)) { 535 536 // getResource() == null could either mean checkConsistency has not been called, or that the link is broken. 537 // so we have to call checkConsistency to eliminate the first possibility. 538 link.checkConsistency(m_cms); 539 // The consistency check tries to read the resource by id first, and then by path if this fails. If at some point in this process 540 // we get a security exception, then there must be some resource there, either for the given id or for the path, although we don't 541 // know at this point in the code which one it is. But it doesn't matter; because a potential link target exists, we don't remove the link. 542 if ((link.getResource() == null) 543 && !CmsUUID.getNullUUID().equals( 544 link.getStructureId()) /* 00000000-0000-0000-0000-000000000000 corresponds to static resource served from Jar file. We probably don't need that in the Online project, but we don't need to actively remove that, either. */ 545 && !link.hadSecurityErrorDuringLastConsistencyCheck()) { 546 tag.removeAttribute(ATTRIBUTE_HREF); 547 tag.setAttribute(CmsGwtConstants.ATTR_DEAD_LINK_MARKER, "true"); 548 } 549 } 550 if (m_cms != null) { 551 CmsADEConfigData sitemapConfig = OpenCms.getADEManager().lookupConfigurationWithCache( 552 m_cms, 553 m_cms.getRequestContext().getRootUri()); 554 String externalMarker = sitemapConfig.getAttribute( 555 "template.editor.links.externalMarker", 556 "none").trim(); 557 if (!"none".equals(externalMarker)) { 558 if (tag.getTagName().equalsIgnoreCase("A")) { 559 boolean markAsExternal = shouldMarkAsExternal(m_cms, sitemapConfig, link); 560 final String attrClass = "class"; 561 String classesValue = tag.getAttribute(attrClass); 562 if (markAsExternal) { 563 // Add marker class 564 565 if (CmsStringUtil.isEmptyOrWhitespaceOnly(classesValue)) { 566 tag.setAttribute(attrClass, externalMarker); 567 } else { 568 List<String> classes = Arrays.asList(classesValue.trim().split("\\s+")); 569 if (!classes.contains(externalMarker)) { 570 tag.setAttribute(attrClass, classesValue + " " + externalMarker); 571 } 572 } 573 } else { 574 if (!CmsStringUtil.isEmptyOrWhitespaceOnly(classesValue)) { 575 // Remove marker class 576 String newValue = Arrays.asList(classesValue.split("\\s+")).stream().filter( 577 cls -> !cls.equals(externalMarker)).collect(Collectors.joining(" ")); 578 tag.setAttribute(attrClass, newValue); 579 } 580 } 581 } 582 } 583 } 584 } 585 break; 586 case REPLACE_LINKS: 587 // links are replaced with macros 588 String targetUri = tag.getAttribute(attr); 589 if (CmsStringUtil.isNotEmpty(targetUri)) { 590 String internalUri = null; 591 if (!CmsMacroResolver.isMacro(targetUri)) { 592 m_cms.getRequestContext().setAttribute( 593 CmsDefaultLinkSubstitutionHandler.DONT_USE_CURRENT_SITE_FOR_WORKPLACE_REQUESTS, 594 "true"); 595 internalUri = OpenCms.getLinkManager().getRootPath(m_cms, targetUri, m_relativePath); 596 } 597 // HACK: to distinguish link parameters the link itself has to end with '&' or '?' 598 // another solution should be a kind of macro... 599 if (!TAG_PARAM.equals(tag.getTagName()) 600 || targetUri.endsWith(CmsRequestUtil.URL_DELIMITER) 601 || targetUri.endsWith(CmsRequestUtil.PARAMETER_DELIMITER)) { 602 if (internalUri != null) { 603 internalUri = rewriteUri(internalUri); 604 // this is an internal link 605 link = m_linkTable.addLink(type, internalUri, true); 606 // link management check 607 link.checkConsistency(m_cms); 608 609 if ("IMG".equals(tag.getTagName()) || TAG_AREA.equals(tag.getTagName())) { 610 // now ensure the image has the "alt" attribute set 611 setAltAttributeFromTitle(tag, internalUri); 612 } 613 } else { 614 // this is an external link 615 link = m_linkTable.addLink(type, targetUri, false); 616 } 617 } 618 if (link != null) { 619 tag.setAttribute(attr, CmsMacroResolver.formatMacro(link.getName())); 620 } 621 } 622 break; 623 default: // empty 624 } 625 626 } 627 628 /** 629 * Process a link tag.<p> 630 * 631 * @param tag the tag to process 632 */ 633 protected void processLinkTag(LinkTag tag) { 634 635 processLink(tag, ATTRIBUTE_HREF, CmsRelationType.valueOf(tag.getTagName())); 636 } 637 638 /** 639 * Process an object tag.<p> 640 * 641 * @param tag the tag to process 642 */ 643 protected void processObjectTag(ObjectTag tag) { 644 645 CmsRelationType type = CmsRelationType.valueOf(tag.getTagName()); 646 for (int i = 0; i < OBJECT_TAG_LINKED_ATTRIBS.length; i++) { 647 String attr = OBJECT_TAG_LINKED_ATTRIBS[i]; 648 processLink(tag, attr, type); 649 if ((i == 0) && (tag.getAttribute(attr) != null)) { 650 // if code base is available, the other attributes are relative to it, so do not process them 651 break; 652 } 653 } 654 SimpleNodeIterator itChildren = tag.children(); 655 while (itChildren.hasMoreNodes()) { 656 Node node = itChildren.nextNode(); 657 if (node instanceof Tag) { 658 Tag childTag = (Tag)node; 659 if (TAG_PARAM.equals(childTag.getTagName())) { 660 processLink(childTag, ATTRIBUTE_VALUE, type); 661 } 662 } 663 } 664 } 665 666 /** 667 * Ensures that the given tag has the "alt" attribute set.<p> 668 * 669 * if not set, it will be set from the title of the given resource.<p> 670 * 671 * @param tag the tag to set the alt attribute for 672 * @param internalUri the internal URI to get the title from 673 */ 674 protected void setAltAttributeFromTitle(Tag tag, String internalUri) { 675 676 boolean hasAltAttrib = (tag.getAttribute("alt") != null); 677 if (!hasAltAttrib) { 678 String value = null; 679 if ((internalUri != null) && (m_rootCms != null)) { 680 // internal image: try to read the "alt" text from the "Title" property 681 try { 682 value = m_rootCms.readPropertyObject( 683 internalUri, 684 CmsPropertyDefinition.PROPERTY_TITLE, 685 false).getValue(); 686 } catch (CmsException e) { 687 // property can't be read, ignore 688 } 689 } 690 // some editors add a "/" at the end of the tag, we must make sure to insert before that 691 @SuppressWarnings("unchecked") 692 Vector<Attribute> attrs = tag.getAttributesEx(); 693 // first element is always the tag name 694 attrs.add(1, new Attribute(" ")); 695 attrs.add(2, new Attribute("alt", value == null ? "" : value, '"')); 696 } 697 } 698 699 /** 700 * Use the {@link org.opencms.file.wrapper.CmsObjectWrapper} to restore the link in the VFS.<p> 701 * 702 * @param internalUri the internal URI to restore 703 * 704 * @return the restored URI 705 */ 706 private String rewriteUri(String internalUri) { 707 708 // if an object wrapper is used, rewrite the uri 709 if (m_cms != null) { 710 Object obj = m_cms.getRequestContext().getAttribute(CmsObjectWrapper.ATTRIBUTE_NAME); 711 if (obj != null) { 712 CmsObjectWrapper wrapper = (CmsObjectWrapper)obj; 713 return wrapper.restoreLink(internalUri); 714 } 715 } 716 717 return internalUri; 718 } 719}