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>&amp;</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 &amps;
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("&amp;");
249                        }
250                    } else {
251                        result.append("&amp;");
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;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, "&amp;", "&");
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}