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