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