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, 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.ade.configuration.CmsDetailNameCache;
032import org.opencms.ade.detailpage.I_CmsDetailPageHandler;
033import org.opencms.file.CmsObject;
034import org.opencms.file.CmsResource;
035import org.opencms.file.CmsResourceFilter;
036import org.opencms.file.CmsVfsException;
037import org.opencms.file.CmsVfsResourceNotFoundException;
038import org.opencms.file.types.CmsResourceTypeImage;
039import org.opencms.file.types.I_CmsResourceType;
040import org.opencms.gwt.shared.CmsGwtConstants;
041import org.opencms.loader.CmsLoaderException;
042import org.opencms.main.CmsException;
043import org.opencms.main.CmsLog;
044import org.opencms.main.CmsStaticResourceHandler;
045import org.opencms.main.OpenCms;
046import org.opencms.site.CmsSite;
047import org.opencms.site.CmsSiteMatcher;
048import org.opencms.util.CmsFileUtil;
049import org.opencms.util.CmsPair;
050import org.opencms.util.CmsStringUtil;
051import org.opencms.util.CmsUUID;
052import org.opencms.util.CmsUriSplitter;
053import org.opencms.workplace.CmsWorkplace;
054import org.opencms.xml.CmsLinkFinisher;
055
056import java.net.URI;
057import java.util.List;
058import java.util.Locale;
059
060import org.apache.commons.logging.Log;
061
062/**
063 * Default link substitution behavior.<p>
064 *
065 * @since 7.0.2
066 *
067 * @see CmsLinkManager#substituteLink(org.opencms.file.CmsObject, String, String, boolean)
068 *      for the method where this handler is used.
069 */
070public class CmsDefaultLinkSubstitutionHandler implements I_CmsLinkSubstitutionHandler {
071
072    /**
073     * Request context attribute name to make the link substitution handler treat the link like an image link.<p>
074     */
075    public static final String ATTR_IS_IMAGE_LINK = "IS_IMAGE_LINK";
076
077    /** Key for a request context attribute to control whether the getRootPath method uses the current site root for workplace requests.
078     *  The getRootPath method clears this attribute when called.
079     */
080    public static final String DONT_USE_CURRENT_SITE_FOR_WORKPLACE_REQUESTS = "DONT_USE_CURRENT_SITE_FOR_WORKPLACE_REQUESTS";
081
082    /** The log object for this class. */
083    private static final Log LOG = CmsLog.getLog(CmsDefaultLinkSubstitutionHandler.class);
084
085    /** Prefix used for request context attributes to control whether a different site root should be used in appendServerPrefix. */
086    public static final String OVERRIDE_SITEROOT_PREFIX = "OVERRIDE_SITEROOT:";
087
088    /**
089     * Returns the resource root path in the OpenCms VFS for the given link, or <code>null</code> in
090     * case the link points to an external site.<p>
091     *
092     * If the target URI contains no site information, but starts with the opencms context, the context is removed:<pre>
093     * /opencms/opencms/system/further_path -> /system/further_path</pre>
094     *
095     * If the target URI contains no site information, the path will be prefixed with the current site
096     * from the provided OpenCms user context:<pre>
097     * /folder/page.html -> /sites/mysite/folder/page.html</pre>
098     *
099     * If the path of the target URI is relative, i.e. does not start with "/",
100     * the path will be prefixed with the current site and the given relative path,
101     * then normalized.
102     * If no relative path is given, <code>null</code> is returned.
103     * If the normalized path is outsite a site, null is returned.<pre>
104     * page.html -> /sites/mysite/page.html
105     * ../page.html -> /sites/mysite/page.html
106     * ../../page.html -> null</pre>
107     *
108     * If the target URI contains a scheme/server name that denotes an opencms site,
109     * it is replaced by the appropriate site path:<pre>
110     * http://www.mysite.de/folder/page.html -> /sites/mysite/folder/page.html</pre><p>
111     *
112     * If the target URI contains a scheme/server name that does not match with any site,
113     * or if the URI is opaque or invalid,
114     * <code>null</code> is returned:<pre>
115     * http://www.elsewhere.com/page.html -> null
116     * mailto:someone@elsewhere.com -> null</pre>
117     *
118     * @see org.opencms.staticexport.I_CmsLinkSubstitutionHandler#getLink(org.opencms.file.CmsObject, java.lang.String, java.lang.String, boolean)
119     */
120    public String getLink(CmsObject cms, String link, String siteRoot, boolean forceSecure) {
121
122        return getLink(cms, link, siteRoot, null, forceSecure);
123    }
124
125    /**
126     * @see org.opencms.staticexport.I_CmsLinkSubstitutionHandler#getLink(org.opencms.file.CmsObject, java.lang.String, java.lang.String, java.lang.String, boolean)
127     */
128    public String getLink(CmsObject cms, String link, String siteRoot, String targetDetailPage, boolean forceSecure) {
129
130        if (CmsStringUtil.isEmpty(link)) {
131            // not a valid link parameter, return an empty String
132            return "";
133        }
134
135        if (CmsStaticResourceHandler.isStaticResourceUri(link)) {
136            return CmsWorkplace.getStaticResourceUri(link);
137        }
138
139        CmsADEConfigData config = OpenCms.getADEManager().lookupConfigurationWithCache(
140            cms,
141            cms.getRequestContext().getRootUri());
142        CmsLinkFinisher linkFinisher;
143        boolean fullLinkFinish = true;
144
145        // make sure we have an absolute link
146        String absoluteLink = CmsLinkManager.getAbsoluteUri(link, cms.getRequestContext().getUri());
147        String overrideSiteRoot = null;
148
149        String vfsName;
150
151        CmsUriSplitter splitter = new CmsUriSplitter(absoluteLink, true);
152        String parameters = null;
153        if (splitter.getQuery() != null) {
154            parameters = "?" + splitter.getQuery();
155        }
156        String anchor = null;
157        if (splitter.getAnchor() != null) {
158            anchor = "#" + splitter.getAnchor();
159        }
160        vfsName = splitter.getPrefix();
161
162        String resultLink = null;
163        String uriBaseName = null;
164        boolean useRelativeLinks = false;
165
166        // determine the target site of the link
167        CmsSite currentSite = OpenCms.getSiteManager().getCurrentSite(cms);
168        CmsSite targetSite = null;
169        if (CmsStringUtil.isNotEmpty(siteRoot)) {
170            targetSite = OpenCms.getSiteManager().getSiteForSiteRoot(siteRoot);
171        }
172        if (targetSite == null) {
173            targetSite = currentSite;
174        }
175
176        String targetSiteRoot = targetSite.getSiteRoot();
177        String originalVfsName = vfsName;
178        String detailPage = null;
179        CmsResource detailContent = null;
180        try {
181            String rootVfsName;
182            if (!vfsName.startsWith(targetSiteRoot)
183                && !vfsName.startsWith(CmsResource.VFS_FOLDER_SYSTEM + "/")
184                && !OpenCms.getSiteManager().startsWithShared(vfsName)) {
185                rootVfsName = CmsStringUtil.joinPaths(targetSiteRoot, vfsName);
186            } else {
187                rootVfsName = vfsName;
188            }
189            if (!rootVfsName.startsWith(CmsWorkplace.VFS_PATH_WORKPLACE)) {
190                // never use the ADE manager for workplace links, to be sure the workplace stays usable in case of configuration errors
191                I_CmsDetailPageHandler finder = OpenCms.getADEManager().getDetailPageHandler();
192                detailPage = finder.getDetailPage(cms, rootVfsName, cms.getRequestContext().getUri(), targetDetailPage);
193            }
194            if (detailPage != null) {
195                CmsSite detailPageSite = OpenCms.getSiteManager().getSiteForRootPath(detailPage);
196                if (detailPageSite != null) {
197                    targetSite = detailPageSite;
198                    overrideSiteRoot = targetSiteRoot = targetSite.getSiteRoot();
199                    detailPage = detailPage.substring(targetSiteRoot.length());
200                    if (!detailPage.startsWith("/")) {
201                        detailPage = "/" + detailPage;
202                    }
203                }
204                String originalSiteRoot = cms.getRequestContext().getSiteRoot();
205                try {
206                    cms.getRequestContext().setSiteRoot("");
207                    CmsResource element = cms.readResource(rootVfsName, CmsResourceFilter.IGNORE_EXPIRATION);
208                    detailContent = element;
209                    Locale locale = cms.getRequestContext().getLocale();
210                    List<Locale> defaultLocales = OpenCms.getLocaleManager().getDefaultLocales();
211                    vfsName = CmsStringUtil.joinPaths(
212                        detailPage,
213                        cms.getDetailName(element, locale, defaultLocales),
214                        "/");
215                    // technically, we could have an URL name of 'index.html' (or whatever the configured names in the link finisher are),
216                    // and in that case the link finisher would break the link
217                    fullLinkFinish = false;
218                } catch (CmsVfsException e) {
219                    if (LOG.isWarnEnabled()) {
220                        LOG.warn(e.getLocalizedMessage(), e);
221                    }
222                } finally {
223                    cms.getRequestContext().setSiteRoot(originalSiteRoot);
224
225                }
226            }
227        } catch (CmsVfsResourceNotFoundException e) {
228            LOG.info(e.getLocalizedMessage(), e);
229        } catch (CmsException e) {
230            LOG.error(e.getLocalizedMessage(), e);
231        }
232
233        // if the link points to another site, there needs to be a server prefix
234        String serverPrefix;
235        if ((targetSite != currentSite) || cms.getRequestContext().isForceAbsoluteLinks()) {
236            serverPrefix = targetSite.getUrl();
237        } else {
238            serverPrefix = "";
239        }
240
241        // in the online project, check static export and secure settings
242        if (cms.getRequestContext().getCurrentProject().isOnlineProject()) {
243            // first check if this link needs static export
244            CmsStaticExportManager exportManager = OpenCms.getStaticExportManager();
245            String oriUri = cms.getRequestContext().getUri();
246            // check if we need relative links in the exported pages
247            if (exportManager.relativeLinksInExport(cms.getRequestContext().getSiteRoot() + oriUri)) {
248                // try to get base URI from cache
249                String cacheKey = exportManager.getCacheKey(targetSiteRoot, oriUri);
250                uriBaseName = exportManager.getCachedOnlineLink(cacheKey);
251                if (uriBaseName == null) {
252                    // base not cached, check if we must export it
253                    if (exportManager.isExportLink(cms, oriUri)) {
254                        // base URI must also be exported
255                        uriBaseName = exportManager.getRfsName(cms, oriUri);
256                    } else {
257                        // base URI dosn't need to be exported
258                        CmsPair<String, String> uriParamPair = addVfsPrefix(cms, oriUri, targetSite, parameters);
259                        uriBaseName = uriParamPair.getFirst();
260                        parameters = uriParamPair.getSecond();
261                    }
262                    // cache export base URI
263                    exportManager.cacheOnlineLink(cacheKey, uriBaseName);
264                }
265                // use relative links only on pages that get exported
266                useRelativeLinks = uriBaseName.startsWith(
267                    exportManager.getRfsPrefix(cms.getRequestContext().getSiteRoot() + oriUri));
268            }
269
270            String detailPagePart = detailPage == null ? "" : detailPage + ":";
271            // check if we have the absolute VFS name for the link target cached
272            // (We really need the target site root in the cache key, because different resources with the same site paths
273            // but in different sites may have different export settings. It seems we don't really need the site root
274            // from the request context as part of the key, but we'll leave it in to make sure we don't break anything.)
275            String cacheKey = generateCacheKey(cms, siteRoot, targetSiteRoot, detailPagePart, absoluteLink);
276            resultLink = exportManager.getCachedOnlineLink(cacheKey);
277            if (resultLink == null) {
278                String storedSiteRoot = cms.getRequestContext().getSiteRoot();
279                try {
280                    cms.getRequestContext().setSiteRoot(targetSite.getSiteRoot());
281                    // didn't find the link in the cache
282                    if (exportManager.isExportLink(cms, vfsName)) {
283                        parameters = prepareExportParameters(cms, vfsName, parameters);
284                        // export required, get export name for target link
285                        resultLink = exportManager.getRfsName(cms, vfsName, parameters, targetDetailPage);
286                        // link finisher may give wrong results for export links
287                        fullLinkFinish = false;
288                        // now set the parameters to null, we do not need them anymore
289                        parameters = null;
290                    } else {
291                        // no export required for the target link
292                        CmsPair<String, String> uriParamPair = addVfsPrefix(cms, vfsName, targetSite, parameters);
293                        resultLink = uriParamPair.getFirst();
294                        parameters = uriParamPair.getSecond();
295                        // add cut off parameters if required
296                        if (parameters != null) {
297                            resultLink = resultLink.concat(parameters);
298                        }
299                    }
300                } finally {
301                    cms.getRequestContext().setSiteRoot(storedSiteRoot);
302                }
303                // cache the result
304                exportManager.cacheOnlineLink(cacheKey, resultLink);
305            }
306
307            // now check for the secure settings
308
309            // check if either the current site or the target site does have a secure server configured
310            if (targetSite.hasSecureServer() || currentSite.hasSecureServer()) {
311
312                if (!vfsName.startsWith(CmsWorkplace.VFS_PATH_SYSTEM)) {
313                    // don't make a secure connection to the "/system" folder (why ?)
314                    int linkType = -1;
315                    try {
316                        // read the linked resource
317                        linkType = cms.readResource(originalVfsName).getTypeId();
318                    } catch (CmsException e) {
319                        // the resource could not be read
320                        if (LOG.isInfoEnabled()) {
321                            String message = Messages.get().getBundle().key(
322                                Messages.LOG_RESOURCE_ACESS_ERROR_3,
323                                vfsName,
324                                cms.getRequestContext().getCurrentUser().getName(),
325                                cms.getRequestContext().getSiteRoot());
326                            if (LOG.isDebugEnabled()) {
327                                LOG.debug(message, e);
328                            } else {
329                                LOG.info(message);
330                            }
331                        }
332                    }
333
334                    // images are always referenced without a server prefix
335                    int imageId;
336                    try {
337                        imageId = OpenCms.getResourceManager().getResourceType(
338                            CmsResourceTypeImage.getStaticTypeName()).getTypeId();
339                    } catch (CmsLoaderException e1) {
340                        // should really never happen
341                        LOG.warn(e1.getLocalizedMessage(), e1);
342                        imageId = CmsResourceTypeImage.getStaticTypeId();
343                    }
344                    boolean hasIsImageLinkAttr = Boolean.parseBoolean(
345                        "" + cms.getRequestContext().getAttribute(ATTR_IS_IMAGE_LINK));
346                    if ((linkType != imageId) && !hasIsImageLinkAttr) {
347                        // check the secure property of the link
348                        boolean secureRequest = cms.getRequestContext().isSecureRequest()
349                            || exportManager.isSecureLink(cms, oriUri);
350
351                        boolean secureLink;
352                        if (detailContent == null) {
353                            secureLink = isSecureLink(cms, vfsName, targetSite, secureRequest);
354                        } else {
355                            secureLink = isDetailPageLinkSecure(
356                                cms,
357                                detailPage,
358                                detailContent,
359                                targetSite,
360                                secureRequest);
361
362                        }
363                        // if we are on a normal server, and the requested resource is secure,
364                        // the server name has to be prepended
365                        if (secureLink && (forceSecure || !secureRequest)) {
366                            serverPrefix = targetSite.getSecureUrl();
367                        } else if (!secureLink && secureRequest) {
368                            serverPrefix = targetSite.getUrl();
369                        }
370                    }
371                }
372            }
373            // make absolute link relative, if relative links in export are required
374            // and if the link does not point to another server
375            if (useRelativeLinks && CmsStringUtil.isEmpty(serverPrefix)) {
376                // in case the current page is a detailpage, append another path level
377                if (cms.getRequestContext().getDetailContentId() != null) {
378                    uriBaseName = CmsStringUtil.joinPaths(
379                        CmsResource.getFolderPath(uriBaseName),
380                        cms.getRequestContext().getDetailContentId().toString() + "/index.html");
381                }
382                resultLink = CmsLinkManager.getRelativeUri(uriBaseName, resultLink);
383            }
384            // for exported resources, the 'force absolute links' mode shall override any RFS rule
385            if (exportManager.isExportLink(cms, vfsName)) {
386                String linkForceAbsoluteExportPrefix = config.getLinkForceAbsoluteExportPrefix(cms);
387                if (linkForceAbsoluteExportPrefix != null) {
388                    // prepend or replace the export prefix
389                    resultLink = CmsLinkManager.ensureServerPrefix(resultLink, linkForceAbsoluteExportPrefix);
390                }
391            }
392        } else {
393            // offline project, no export or secure handling required
394            if (OpenCms.getRunLevel() >= OpenCms.RUNLEVEL_3_SHELL_ACCESS) {
395                // in unit test this code would fail otherwise
396                CmsPair<String, String> uriParamPair = addVfsPrefix(cms, vfsName, targetSite, parameters);
397                resultLink = uriParamPair.getFirst();
398                parameters = uriParamPair.getSecond();
399            }
400
401            // add cut off parameters and return the result
402            if ((parameters != null) && (resultLink != null)) {
403                resultLink = resultLink.concat(parameters);
404            }
405        }
406
407        if ((anchor != null) && (resultLink != null)) {
408            resultLink = resultLink.concat(anchor);
409        }
410        if (overrideSiteRoot != null) {
411            cms.getRequestContext().setAttribute(OVERRIDE_SITEROOT_PREFIX + resultLink, overrideSiteRoot);
412        }
413
414        String result = null;
415        if (CmsLinkManager.hasScheme(resultLink)) {
416            // in the case there is a rfs-rule that already includes a server host or we are in 'force absolute link'
417            // mode with a 'template.link.forceabsolute.exportprefix' attribute configured
418            result = resultLink;
419        } else {
420            result = serverPrefix.concat(resultLink);
421        }
422        boolean isEditMode = !cms.getRequestContext().getCurrentProject().isOnlineProject()
423            && (cms.getRequestContext().getAttribute(CmsGwtConstants.PARAM_DISABLE_DIRECT_EDIT) == null);
424
425        if (isEditMode
426            && (cms.getRequestContext().getAttribute(CmsLinkProcessor.ATTR_IS_PROCESSING_LINKS) == Boolean.TRUE)) {
427            // in the Offline project, the link engine is also used for rendering links in the WYSIWYG editor, and the resulting HTML
428            // is sent to the server later for saving, so we want to preserve the actual resources linked to - so we can't cut off index.html or similar suffixes.
429            fullLinkFinish = false;
430        }
431        linkFinisher = config.getLinkFinisher();
432        result = linkFinisher.transformLink(result, fullLinkFinish);
433        return result;
434    }
435
436    /**
437     * @see org.opencms.staticexport.I_CmsLinkSubstitutionHandler#getRootPath(org.opencms.file.CmsObject, java.lang.String, java.lang.String)
438     */
439    public String getRootPath(CmsObject cms, String targetUri, String basePath) {
440
441        String result = getSimpleRootPath(cms, targetUri, basePath);
442        String detailRootPath = getDetailRootPath(cms, result);
443        if (detailRootPath != null) {
444            result = detailRootPath;
445        }
446        return result;
447
448    }
449
450    /**
451     * Adds the VFS prefix to the VFS name and potentially adjusts request parameters<p>
452     * This method is required as a hook used in {@link CmsLocalePrefixLinkSubstitutionHandler}.<p>
453     *
454     * @param cms the cms context
455     * @param vfsName the VFS name
456     * @param targetSite the target site
457     * @param parameters the request parameters
458     *
459     * @return the path and the (adjusted) request parameters.
460     */
461    protected CmsPair<String, String> addVfsPrefix(
462        CmsObject cms,
463        String vfsName,
464        CmsSite targetSite,
465        String parameters) {
466
467        return new CmsPair<String, String>(OpenCms.getStaticExportManager().getVfsPrefix().concat(vfsName), parameters);
468    }
469
470    /**
471     * Generates the cache key for Online links.
472     * @param cms the current CmsObject
473     * @param sourceSiteRoot the source site root (where the content linked to is located)
474     * @param targetSiteRoot the target site root
475     * @param detailPagePart the detail page part
476     * @param absoluteLink the absolute (site-relative) link to the resource
477     * @return the cache key
478     */
479    protected String generateCacheKey(
480        CmsObject cms,
481        String sourceSiteRoot,
482        String targetSiteRoot,
483        String detailPagePart,
484        String absoluteLink) {
485
486        return ""
487            + cms.getRequestContext().getCurrentUser().getId()
488            + ":"
489            + cms.getRequestContext().getSiteRoot()
490            + ":"
491            + sourceSiteRoot
492            + ":"
493            + targetSiteRoot
494            + ":"
495            + detailPagePart
496            + absoluteLink;
497    }
498
499    /**
500     * Returns the root path for given site.<p>
501     * This method is required as a hook used in {@link CmsLocalePrefixLinkSubstitutionHandler}.<p>
502     * @param cms the cms context
503     * @param path the path
504     * @param siteRoot the site root, will be null in case of the root site
505     * @param isRootPath in case the path is already a root path
506     *
507     * @return the root path
508     */
509    protected String getRootPathForSite(CmsObject cms, String path, String siteRoot, boolean isRootPath) {
510
511        if (isRootPath || (siteRoot == null)) {
512            return CmsStringUtil.joinPaths("/", path);
513        } else {
514            CmsSite site = OpenCms.getSiteManager().getSiteForRootPath(siteRoot);
515            if (site != null) {
516                if (site.matchAlternativeSiteRoot(path)) {
517                    siteRoot = site.getAlternativeSiteRootMapping().get().getSiteRoot().asString();
518                }
519            }
520            return cms.getRequestContext().addSiteRoot(siteRoot, path);
521        }
522    }
523
524    /**
525     * Gets the root path without taking into account detail page links.<p>
526     *
527     * @param cms - see the getRootPath() method
528     * @param targetUri - see the getRootPath() method
529     * @param basePath - see the getRootPath() method
530     * @return - see the getRootPath() method
531     */
532    protected String getSimpleRootPath(CmsObject cms, String targetUri, String basePath) {
533
534        if (cms == null) {
535            // required by unit test cases
536            return targetUri;
537        }
538
539        URI uri;
540        String path;
541        String suffix = "";
542
543        // malformed uri
544        try {
545            uri = new URI(targetUri);
546            path = uri.getPath();
547            suffix = getSuffix(uri);
548        } catch (Exception e) {
549            if (LOG.isWarnEnabled()) {
550                LOG.warn(Messages.get().getBundle().key(Messages.LOG_MALFORMED_URI_1, targetUri), e);
551            }
552            return null;
553        }
554        // opaque URI
555        if (uri.isOpaque()) {
556            return null;
557        }
558
559        // in case the target is the workplace UI
560        if (CmsLinkManager.isWorkplaceUri(uri)) {
561            return null;
562        }
563
564        // in case the target is a static resource served from the class path
565        if (CmsStaticResourceHandler.isStaticResourceUri(uri)) {
566            return CmsStringUtil.joinPaths(
567                CmsStaticResourceHandler.STATIC_RESOURCE_PREFIX,
568                CmsStaticResourceHandler.removeStaticResourcePrefix(path));
569        }
570
571        CmsStaticExportManager exportManager = OpenCms.getStaticExportManager();
572        if (exportManager.isValidRfsName(path)) {
573            String originalSiteRoot = cms.getRequestContext().getSiteRoot();
574            String vfsName = null;
575            try {
576                cms.getRequestContext().setSiteRoot("");
577                vfsName = exportManager.getVfsName(cms, path);
578                if (vfsName != null) {
579                    return vfsName;
580                }
581            } finally {
582                cms.getRequestContext().setSiteRoot(originalSiteRoot);
583            }
584        }
585
586        // absolute URI (i.e. URI has a scheme component like http:// ...)
587        if (uri.isAbsolute()) {
588            CmsSiteMatcher targetMatcher = new CmsSiteMatcher(targetUri);
589            if (OpenCms.getSiteManager().isMatching(targetMatcher)
590                || targetMatcher.equalsIgnoreScheme(cms.getRequestContext().getRequestMatcher())) {
591
592                path = CmsLinkManager.removeOpenCmsContext(path);
593                boolean isWorkplaceServer = OpenCms.getSiteManager().isWorkplaceRequest(targetMatcher)
594                    || targetMatcher.equalsIgnoreScheme(cms.getRequestContext().getRequestMatcher());
595                if (isWorkplaceServer) {
596                    String selectedPath;
597                    String targetSiteRoot = OpenCms.getSiteManager().getSiteRoot(path);
598                    if (targetSiteRoot != null) {
599                        selectedPath = getRootPathForSite(cms, path, targetSiteRoot, true);
600                    } else {
601                        // set selectedPath with the path for the current site
602                        selectedPath = getRootPathForSite(cms, path, cms.getRequestContext().getSiteRoot(), false);
603                        String pathForMatchedSite = getRootPathForSite(
604                            cms,
605                            path,
606                            OpenCms.getSiteManager().matchSite(targetMatcher).getSiteRoot(),
607                            false);
608                        String originalSiteRoot = cms.getRequestContext().getSiteRoot();
609                        try {
610                            cms.getRequestContext().setSiteRoot("");
611                            // the path for the current site normally is preferred, but if it doesn't exist and the path for the matched site
612                            // does exist, then use the path for the matched site
613                            if (!cms.existsResource(selectedPath, CmsResourceFilter.ALL)
614                                && cms.existsResource(pathForMatchedSite, CmsResourceFilter.ALL)) {
615                                selectedPath = pathForMatchedSite;
616                            }
617                        } finally {
618                            cms.getRequestContext().setSiteRoot(originalSiteRoot);
619                        }
620                    }
621                    return selectedPath + suffix;
622                } else {
623                    // add the site root of the matching site
624                    return getRootPathForSite(
625                        cms,
626                        path + suffix,
627                        OpenCms.getSiteManager().matchSite(targetMatcher).getSiteRoot(),
628                        false);
629                }
630            } else {
631                return null;
632            }
633        }
634
635        // relative URI (i.e. no scheme component, but filename can still start with "/")
636        String context = OpenCms.getSystemInfo().getOpenCmsContext();
637        String vfsPrefix = OpenCms.getStaticExportManager().getVfsPrefix();
638        if ((context != null) && (path.startsWith(context + "/") || (path.startsWith(vfsPrefix + "/")))) {
639            // URI is starting with opencms context
640
641            // cut context from path
642            path = CmsLinkManager.removeOpenCmsContext(path);
643
644            String targetSiteRoot = getTargetSiteRoot(cms, path, basePath);
645
646            return getRootPathForSite(
647                cms,
648                path + suffix,
649                targetSiteRoot,
650                (targetSiteRoot != null) && path.startsWith(targetSiteRoot));
651        }
652
653        // URI with relative path is relative to the given relativePath if available and in a site,
654        // otherwise invalid
655        if (CmsStringUtil.isNotEmpty(path) && (path.charAt(0) != '/')) {
656            if (basePath != null) {
657                String absolutePath;
658                int pos = path.indexOf("../../galleries/pics/");
659                if (pos >= 0) {
660                    // HACK: mixed up editor path to system gallery image folder
661                    return CmsWorkplace.VFS_PATH_SYSTEM + path.substring(pos + 6) + suffix;
662                }
663                absolutePath = CmsLinkManager.getAbsoluteUri(path, cms.getRequestContext().addSiteRoot(basePath));
664                if (OpenCms.getSiteManager().getSiteRoot(absolutePath) != null) {
665                    return absolutePath + suffix;
666                }
667                // HACK: some editor components (e.g. HtmlArea) mix up the editor URL with the current request URL
668                absolutePath = CmsLinkManager.getAbsoluteUri(
669                    path,
670                    cms.getRequestContext().getSiteRoot() + CmsWorkplace.VFS_PATH_EDITORS);
671                if (OpenCms.getSiteManager().getSiteRoot(absolutePath) != null) {
672                    return absolutePath + suffix;
673                }
674                // HACK: same as above, but XmlContent editor has one path element more
675                absolutePath = CmsLinkManager.getAbsoluteUri(
676                    path,
677                    cms.getRequestContext().getSiteRoot() + CmsWorkplace.VFS_PATH_EDITORS + "xmlcontent/");
678                if (OpenCms.getSiteManager().getSiteRoot(absolutePath) != null) {
679                    return absolutePath + suffix;
680                }
681            }
682
683            return null;
684        }
685
686        if (CmsStringUtil.isNotEmpty(path)) {
687            String targetSiteRoot = getTargetSiteRoot(cms, path, basePath);
688
689            return getRootPathForSite(
690                cms,
691                path + suffix,
692                targetSiteRoot,
693                (targetSiteRoot != null) && path.startsWith(targetSiteRoot));
694        }
695
696        // URI without path (typically local link)
697        return suffix;
698    }
699
700    /**
701     * Checks whether a link to a detail page should be secure.<p>
702     *
703     * @param cms the current CMS context
704     * @param detailPage the detail page path
705     * @param detailContent the detail content resource
706     * @param targetSite the target site containing the detail page
707     * @param secureRequest true if the currently running request is secure
708     *
709     * @return true if the link should be a secure link
710     */
711    protected boolean isDetailPageLinkSecure(
712        CmsObject cms,
713        String detailPage,
714        CmsResource detailContent,
715        CmsSite targetSite,
716        boolean secureRequest) {
717
718        boolean result = false;
719        CmsStaticExportManager exportManager = OpenCms.getStaticExportManager();
720        try {
721            cms = OpenCms.initCmsObject(cms);
722            if (targetSite.getSiteRoot() != null) {
723                cms.getRequestContext().setSiteRoot(targetSite.getSiteRoot());
724            }
725            CmsResource defaultFile = cms.readDefaultFile(detailPage);
726            if (defaultFile != null) {
727                result = exportManager.isSecureLink(cms, defaultFile.getRootPath(), "", secureRequest);
728            }
729        } catch (Exception e) {
730            LOG.error("Error while checking whether detail page link should be secure: " + e.getLocalizedMessage(), e);
731        }
732        return result;
733    }
734
735    /**
736     * Checks if the link target is a secure link.<p
737     *
738     * @param cms the current CMS context
739     * @param vfsName the path of the link target
740     * @param targetSite the target site containing the detail page
741     * @param secureRequest true if the currently running request is secure
742     *
743     * @return true if the link should be a secure link
744     */
745    protected boolean isSecureLink(CmsObject cms, String vfsName, CmsSite targetSite, boolean secureRequest) {
746
747        return OpenCms.getStaticExportManager().isSecureLink(cms, vfsName, targetSite.getSiteRoot(), secureRequest);
748    }
749
750    /**
751     * Prepares the request parameters for the given resource.<p>
752     * This method is required as a hook used in {@link CmsLocalePrefixLinkSubstitutionHandler}.<p>
753     *
754     * @param cms the cms context
755     * @param vfsName the vfs name
756     * @param parameters the parameters to prepare
757     *
758     * @return the root path
759     */
760    protected String prepareExportParameters(CmsObject cms, String vfsName, String parameters) {
761
762        return parameters;
763    }
764
765    /**
766     * Gets the suffix (query + fragment) of the URI.<p>
767     *
768     * @param uri the URI
769     * @return the suffix of the URI
770     */
771    String getSuffix(URI uri) {
772
773        String fragment = uri.getFragment();
774        if (fragment != null) {
775            fragment = "#" + fragment;
776        } else {
777            fragment = "";
778        }
779
780        String query = uri.getRawQuery();
781        if (query != null) {
782            query = "?" + query;
783        } else {
784            query = "";
785        }
786        return query.concat(fragment);
787    }
788
789    /**
790     * Tries to interpret the given URI as a detail page URI and returns the detail content's root path if possible.<p>
791     *
792     * If the given URI is not a detail URI, null will be returned.<p>
793     *
794     * @param cms the CMS context to use
795     * @param result the detail root path, or null if the given uri is not a detail page URI
796     *
797     * @return the detail content root path
798     */
799    private String getDetailRootPath(CmsObject cms, String result) {
800
801        if (result == null) {
802            return null;
803        }
804        try {
805            URI uri = new URI(result);
806            String path = uri.getPath();
807            if (CmsStringUtil.isEmptyOrWhitespaceOnly(path) || !OpenCms.getADEManager().isInitialized()) {
808                return null;
809            }
810            String name = CmsFileUtil.removeTrailingSeparator(CmsResource.getName(path));
811            CmsUUID detailId = null;
812            if (cms.getRequestContext().getAttribute(CmsDetailNameCache.ATTR_BYPASS) != null) {
813                detailId = cms.readIdForUrlName(name);
814            } else {
815                if (CmsUUID.isValidUUID(name)) {
816                    detailId = new CmsUUID(name);
817                } else {
818                    detailId = OpenCms.getADEManager().getDetailIdCache(
819                        cms.getRequestContext().getCurrentProject().isOnlineProject()).getDetailId(name);
820                }
821            }
822            if (detailId == null) {
823                return null;
824            }
825            String origSiteRoot = cms.getRequestContext().getSiteRoot();
826            try {
827                cms.getRequestContext().setSiteRoot("");
828                // real root paths have priority over detail contents
829                if (cms.existsResource(path)) {
830                    return null;
831                }
832            } finally {
833                cms.getRequestContext().setSiteRoot(origSiteRoot);
834            }
835            CmsResource detailResource = cms.readResource(detailId, CmsResourceFilter.ALL);
836            I_CmsResourceType type = OpenCms.getResourceManager().getResourceType(detailResource);
837            if (!OpenCms.getADEManager().getDetailPageTypes(cms).contains(type.getTypeName())) {
838                return null;
839            }
840            return detailResource.getRootPath() + getSuffix(uri);
841        } catch (Exception e) {
842            LOG.error(e.getLocalizedMessage(), e);
843            return null;
844        }
845    }
846
847    /**
848     * Returns the target site for the given path.<p>
849     *
850     * @param cms the cms context
851     * @param path the path
852     * @param basePath the base path
853     *
854     * @return the target site
855     */
856    private String getTargetSiteRoot(CmsObject cms, String path, String basePath) {
857
858        if (OpenCms.getSiteManager().startsWithShared(path) || path.startsWith(CmsWorkplace.VFS_PATH_SYSTEM)) {
859            return null;
860        }
861        String targetSiteRoot = OpenCms.getSiteManager().getSiteRoot(path);
862        if ((targetSiteRoot == null) && (basePath != null)) {
863            targetSiteRoot = OpenCms.getSiteManager().getSiteRoot(basePath);
864        }
865        if (targetSiteRoot == null) {
866            targetSiteRoot = cms.getRequestContext().getSiteRoot();
867        }
868        return targetSiteRoot;
869    }
870
871}