001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH & Co. KG, please see the
018 * company website: https://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: https://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.staticexport;
029
030import org.opencms.file.CmsObject;
031import org.opencms.file.CmsProject;
032import org.opencms.file.CmsResource;
033import org.opencms.file.CmsResourceFilter;
034import org.opencms.main.CmsException;
035import org.opencms.main.CmsLog;
036import org.opencms.main.CmsPermalinkResourceHandler;
037import org.opencms.main.OpenCms;
038import org.opencms.relations.CmsExternalLinksValidationResult;
039import org.opencms.security.CmsRole;
040import org.opencms.security.CmsRoleViolationException;
041import org.opencms.site.CmsSite;
042import org.opencms.util.CmsFileUtil;
043import org.opencms.util.CmsStringUtil;
044import org.opencms.util.CmsUUID;
045
046import java.net.MalformedURLException;
047import java.net.URI;
048import java.net.URISyntaxException;
049import java.net.URL;
050import java.util.regex.Pattern;
051
052import org.apache.commons.logging.Log;
053
054import com.google.common.base.Optional;
055
056/**
057 * Does the link replacement for the &lg;link&gt; tags.<p>
058 *
059 * Since this functionality is closely related to the static export,
060 * this class resides in the static export package.<p>
061 *
062 * @since 6.0.0
063 */
064public class CmsLinkManager {
065
066    /** The log object for this class. */
067    private static final Log LOG = CmsLog.getLog(CmsLinkManager.class);
068
069    /** Base URL to calculate absolute links. */
070    private static URL m_baseUrl;
071
072    /**
073     * Static initializer for the base URL.<p>
074     */
075    static {
076        m_baseUrl = null;
077        try {
078            m_baseUrl = new URL("http://127.0.0.1");
079        } catch (MalformedURLException e) {
080            // this won't happen
081            LOG.error(e.getLocalizedMessage(), e);
082        }
083    }
084
085    /** Pattern for detecting whether an URI starts with a scheme. */
086    public static final Pattern HAS_SCHEME_PATTERN = Pattern.compile("^[A-Za-z][A-Za-z0-9+.-]*:");
087
088    /** The configured link substitution handler. */
089    private I_CmsLinkSubstitutionHandler m_linkSubstitutionHandler;
090
091    /** Stores the results of a external link validation. */
092    private CmsExternalLinksValidationResult m_pointerLinkValidationResult;
093
094    /**
095     * Public constructor.<p>
096     *
097     * @param linkSubstitutionHandler the link substitution handler to use
098     */
099    public CmsLinkManager(I_CmsLinkSubstitutionHandler linkSubstitutionHandler) {
100
101        m_linkSubstitutionHandler = linkSubstitutionHandler;
102        if (m_linkSubstitutionHandler == null) {
103            // just make very sure that this is not null
104            m_linkSubstitutionHandler = new CmsDefaultLinkSubstitutionHandler();
105        }
106    }
107
108    /**
109     * For a given link, ensures that it starts with the given server prefix.
110     * @param link the link
111     * @param serverPrefix the server prefix
112     * @return the link starting with the given server prefix
113     */
114    public static String ensureServerPrefix(String link, String serverPrefix) {
115
116        if (CmsStringUtil.isEmptyOrWhitespaceOnly(link) || CmsStringUtil.isEmptyOrWhitespaceOnly(serverPrefix)) {
117            return link;
118        }
119        if (link.startsWith(serverPrefix)) {
120            return link;
121        }
122        try {
123            URI uri = new URI(link);
124            StringBuffer result = new StringBuffer(serverPrefix);
125            result.append(uri.getRawPath());
126            if (uri.getRawQuery() != null) {
127                result.append('?');
128                result.append(uri.getRawQuery());
129            }
130            if (uri.getRawFragment() != null) {
131                result.append('#');
132                result.append(uri.getRawFragment());
133            }
134            return result.toString();
135        } catch (Exception e) {
136            LOG.debug(e.getLocalizedMessage(), e);
137            return link;
138        }
139    }
140
141    /**
142     * Calculates the absolute URI for the "relativeUri" with the given absolute "baseUri" as start. <p>
143     *
144     * If "relativeUri" is already absolute, it is returned unchanged.
145     * This method also returns "relativeUri" unchanged if it is not well-formed.<p>
146     *
147     * @param relativeUri the relative URI to calculate an absolute URI for
148     * @param baseUri the base URI, this must be an absolute URI
149     *
150     * @return an absolute URI calculated from "relativeUri" and "baseUri"
151     */
152    public static String getAbsoluteUri(String relativeUri, String baseUri) {
153
154        if (isAbsoluteUri(relativeUri)) {
155            // URI is null or already absolute
156            return relativeUri;
157        }
158        try {
159            URL url = new URL(new URL(m_baseUrl, baseUri), relativeUri);
160            StringBuffer result = new StringBuffer(100);
161            result.append(url.getPath());
162            if (url.getQuery() != null) {
163                result.append('?');
164                result.append(url.getQuery());
165            }
166            if (url.getRef() != null) {
167                result.append('#');
168                result.append(url.getRef());
169            }
170            return result.toString();
171        } catch (MalformedURLException e) {
172            LOG.debug(e.getLocalizedMessage(), e);
173            return relativeUri;
174        }
175    }
176
177    /**
178     * Gets the absolute path for the subsite a link links to.
179     *
180     * <p>For detail links, the subsite of the detail page is returned, not the subsite of the detail content
181     * <p>If the link is not internal, null will be returned.
182     *
183     * @param cms a CMS context
184     * @param link the link to check
185     * @return the subsite path for the link target, or null if not applicable
186     */
187    public static String getLinkSubsite(CmsObject cms, String link) {
188
189        try {
190
191            URI uri = new URI(link);
192            String path = uri.getPath();
193            String name = CmsResource.getName(path);
194            name = CmsFileUtil.removeTrailingSeparator(name);
195            String rootPath = OpenCms.getLinkManager().getRootPath(cms, link);
196            if (rootPath == null) {
197                return null;
198            }
199            String parentRootPath = null;
200            try {
201                CmsUUID detailId = cms.readIdForUrlName(name);
202                if (detailId != null) {
203                    CmsResource detailRes = cms.readResource(detailId, CmsResourceFilter.IGNORE_EXPIRATION);
204                    // When the last part of the path, interpreted as a detail name, resolves to the same root path returned by CmsLinkManager.getRootPath(), it is a detail page URL
205                    if (detailRes.getRootPath().equals(rootPath)) {
206                        URI parentUri = new URI(
207                            uri.getScheme(),
208                            uri.getAuthority(),
209                            CmsResource.getParentFolder(uri.getPath()),
210                            null,
211                            null);
212                        parentRootPath = OpenCms.getLinkManager().getRootPath(cms, parentUri.toASCIIString());
213                    }
214                }
215            } catch (CmsException e) {
216                LOG.info(e.getLocalizedMessage(), e);
217            }
218            if (parentRootPath != null) {
219                return OpenCms.getADEManager().getSubSiteRoot(cms, parentRootPath);
220            } else {
221                return OpenCms.getADEManager().getSubSiteRoot(cms, rootPath);
222            }
223        } catch (URISyntaxException e) {
224            LOG.warn(e.getLocalizedMessage(), e);
225            return null;
226        }
227
228    }
229
230    /**
231     * Calculates a relative URI from "fromUri" to "toUri",
232     * both URI must be absolute.<p>
233     *
234     * @param fromUri the URI to start
235     * @param toUri the URI to calculate a relative path to
236     * @return a relative URI from "fromUri" to "toUri"
237     */
238    public static String getRelativeUri(String fromUri, String toUri) {
239
240        StringBuffer result = new StringBuffer();
241        int pos = 0;
242
243        while (true) {
244            int i = fromUri.indexOf('/', pos);
245            int j = toUri.indexOf('/', pos);
246            if ((i == -1) || (i != j) || !fromUri.regionMatches(pos, toUri, pos, i - pos)) {
247                break;
248            }
249            pos = i + 1;
250        }
251
252        // count hops up from here to the common ancestor
253        for (int i = fromUri.indexOf('/', pos); i > 0; i = fromUri.indexOf('/', i + 1)) {
254            result.append("../");
255        }
256
257        // append path down from common ancestor to there
258        result.append(toUri.substring(pos));
259
260        if (result.length() == 0) {
261            // special case: relative link to the parent folder from a file in that folder
262            result.append("./");
263        }
264
265        return result.toString();
266    }
267
268    /**
269     * Returns the resource root path for the given target URI in the OpenCms VFS, or <code>null</code> in
270     * case the target URI points to an external site.<p>
271     *
272     * @param cms the current users OpenCms context
273     * @param basePath path to use as base site for the target URI (can be <code>null</code>)
274     * @param targetUri the target URI
275     *
276     * @return the resource root path for the given target URI in the OpenCms VFS, or <code>null</code> in
277     *      case the target URI points to an external site
278     *
279     * @deprecated use {@link #getRootPath(CmsObject, String, String)} instead, obtain the link manager
280     *      with {@link OpenCms#getLinkManager()}
281     */
282    @Deprecated
283    public static String getSitePath(CmsObject cms, String basePath, String targetUri) {
284
285        return OpenCms.getLinkManager().getRootPath(cms, targetUri, basePath);
286    }
287
288    /**
289     * Tests if the given URI starts with a scheme component.<p>
290     *
291     * The scheme component is something like <code>http:</code> or <code>ftp:</code>.<p>
292     *
293     * @param uri the URI to test
294     *
295     * @return <code>true</code> if the given URI starts with a scheme component
296     */
297    public static boolean hasScheme(String uri) {
298
299        return HAS_SCHEME_PATTERN.matcher(uri).find();
300    }
301
302    /**
303     * Returns <code>true</code> in case the given URI is absolute.<p>
304     *
305     * An URI is considered absolute if one of the following is true:<ul>
306     * <li>The URI starts with a <code>'/'</code> char.
307     * <li>The URI contains a <code>':'</code> in the first 10 chars.
308     * <li>The URI is <code>null</code>
309     * </ul>
310     *
311     * @param uri the URI to test
312     *
313     * @return <code>true</code> in case the given URI is absolute
314     */
315    public static boolean isAbsoluteUri(String uri) {
316
317        return (uri == null) || ((uri.length() >= 1) && ((uri.charAt(0) == '/') || hasScheme(uri)));
318    }
319
320    /**
321     * Returns if the given link points to the OpenCms workplace UI.<p>
322     *
323     * @param link the link to test
324     *
325     * @return <code>true</code> in case the given URI points to the OpenCms workplace UI
326     */
327    public static boolean isWorkplaceLink(String link) {
328
329        boolean result = false;
330        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(link)) {
331            result = link.startsWith(OpenCms.getSystemInfo().getWorkplaceContext());
332            if (!result) {
333                try {
334                    URI uri = new URI(link);
335                    result = isWorkplaceUri(uri);
336                } catch (URISyntaxException e) {
337                    LOG.debug(e.getLocalizedMessage(), e);
338                }
339            }
340        }
341
342        return result;
343    }
344
345    /**
346     * Returns if the given URI is pointing to the OpenCms workplace UI.<p>
347     *
348     * @param uri the URI
349     *
350     * @return <code>true</code> if the given URI is pointing to the OpenCms workplace UI
351     */
352    public static boolean isWorkplaceUri(URI uri) {
353
354        return (uri != null) && uri.getPath().startsWith(OpenCms.getSystemInfo().getWorkplaceContext());
355    }
356
357    /**
358     * Given a path to a VFS resource, the method removes the OpenCms context,
359     * in case the path is prefixed by that context.
360     * @param path the path where the OpenCms context should be removed
361     * @return the adjusted path
362     */
363    public static String removeOpenCmsContext(final String path) {
364
365        String context = OpenCms.getSystemInfo().getOpenCmsContext();
366        if (path.startsWith(context + "/")) {
367            return path.substring(context.length());
368        }
369        String renderPrefix = OpenCms.getStaticExportManager().getVfsPrefix();
370        if (path.startsWith(renderPrefix + "/")) {
371            return path.substring(renderPrefix.length());
372        }
373        return path;
374    }
375
376    /**
377     * Returns the online link for the given resource, with full server prefix.<p>
378     *
379     * Like <code>http://site.enterprise.com:8080/index.html</code>.<p>
380     *
381     * In case the resource name is a full root path, the site from the root path will be used.
382     * Otherwise the resource is assumed to be in the current site set be the OpenCms user context.<p>
383     *
384     * Please note that this method will always return the link as it will appear in the "Online"
385     * project, that is after the resource has been published. In case you need a method that
386     * just returns the link with the full server prefix, use {@link #getServerLink(CmsObject, String)}.<p>
387     *
388     * @param cms the current OpenCms user context
389     * @param resourceName the resource to generate the online link for
390     *
391     * @return the online link for the given resource, with full server prefix
392     *
393     * @see #getServerLink(CmsObject, String)
394     */
395    public String getOnlineLink(CmsObject cms, String resourceName) {
396
397        return getOnlineLink(cms, resourceName, false);
398    }
399
400    /**
401     * Returns the online link for the given resource, with full server prefix.<p>
402     *
403     * Like <code>http://site.enterprise.com:8080/index.html</code>.<p>
404     *
405     * In case the resource name is a full root path, the site from the root path will be used.
406     * Otherwise the resource is assumed to be in the current site set be the OpenCms user context.<p>
407     *
408     * Please note that this method will always return the link as it will appear in the "Online"
409     * project, that is after the resource has been published. In case you need a method that
410     * just returns the link with the full server prefix, use {@link #getServerLink(CmsObject, String)}.<p>
411     *
412     * @param cms the current OpenCms user context
413     * @param resourceName the resource to generate the online link for
414     * @param forceSecure forces the secure server prefix if the target is secure
415     *
416     * @return the online link for the given resource, with full server prefix
417     *
418     * @see #getServerLink(CmsObject, String)
419     */
420    public String getOnlineLink(CmsObject cms, String resourceName, boolean forceSecure) {
421
422        String result = "";
423        try {
424            CmsProject currentProject = cms.getRequestContext().getCurrentProject();
425            try {
426                cms.getRequestContext().setCurrentProject(cms.readProject(CmsProject.ONLINE_PROJECT_ID));
427                result = substituteLinkForUnknownTarget(cms, resourceName, forceSecure);
428                result = appendServerPrefix(cms, result, resourceName, false);
429            } finally {
430                cms.getRequestContext().setCurrentProject(currentProject);
431            }
432        } catch (CmsException e) {
433            // should never happen
434            result = e.getLocalizedMessage();
435            if (LOG.isErrorEnabled()) {
436                LOG.error(e.getLocalizedMessage(), e);
437            }
438        }
439        return result;
440    }
441
442    /**
443     * Returns the online link for the given resource, with full server prefix.<p>
444     *
445     * Like <code>http://site.enterprise.com:8080/index.html</code>.<p>
446     *
447     * In case the resource name is a full root path, the site from the root path will be used.
448     * Otherwise the resource is assumed to be in the current site set be the OpenCms user context.<p>
449     *
450     * Please note that this method will always return the link as it will appear in the "Online"
451     * project, that is after the resource has been published. In case you need a method that
452     * just returns the link with the full server prefix, use {@link #getServerLink(CmsObject, String)}.<p>
453     *
454     * @param cms the current OpenCms user context
455     * @param resourceName the resource to generate the online link for
456     * @param targetDetailPage the target detail page, in case of linking to a specific detail page
457     * @param forceSecure forces the secure server prefix if the target is secure
458     *
459     * @return the online link for the given resource, with full server prefix
460     *
461     * @see #getServerLink(CmsObject, String)
462     */
463    public String getOnlineLink(CmsObject cms, String resourceName, String targetDetailPage, boolean forceSecure) {
464
465        String result = "";
466        try {
467            CmsProject currentProject = cms.getRequestContext().getCurrentProject();
468            try {
469                cms.getRequestContext().setCurrentProject(cms.readProject(CmsProject.ONLINE_PROJECT_ID));
470                result = substituteLinkForUnknownTarget(cms, resourceName, targetDetailPage, forceSecure);
471                result = appendServerPrefix(cms, result, resourceName, false);
472            } finally {
473                cms.getRequestContext().setCurrentProject(currentProject);
474            }
475        } catch (CmsException e) {
476            // should never happen
477            result = e.getLocalizedMessage();
478            if (LOG.isErrorEnabled()) {
479                LOG.error(e.getLocalizedMessage(), e);
480            }
481        }
482        return result;
483    }
484
485    /**
486     * Returns the perma link for the given resource.<p>
487     *
488     * Like
489     * <code>http://site.enterprise.com:8080/permalink/4b65369f-1266-11db-8360-bf0f6fbae1f8.html</code>.<p>
490     *
491     * @param cms the cms context
492     * @param resourceName the resource to generate the perma link for
493     *
494     * @return the perma link
495     */
496    public String getPermalink(CmsObject cms, String resourceName) {
497
498        return getPermalink(cms, resourceName, null);
499    }
500
501    /**
502     * Returns the perma link for the given resource and optional detail content.<p<
503     *
504     * @param cms the CMS context to use
505     * @param resourceName the page to generate the perma link for
506     * @param detailContentId the structure id of the detail content (may be null)
507     *
508     * @return the perma link
509     */
510    public String getPermalink(CmsObject cms, String resourceName, CmsUUID detailContentId) {
511
512        String permalink = "";
513        try {
514            permalink = substituteLink(cms, CmsPermalinkResourceHandler.PERMALINK_HANDLER);
515            String id = cms.readResource(resourceName, CmsResourceFilter.ALL).getStructureId().toString();
516            permalink += id;
517            if (detailContentId != null) {
518                permalink += ":" + detailContentId;
519            }
520            String ext = CmsFileUtil.getExtension(resourceName);
521            if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(ext)) {
522                permalink += ext;
523            }
524            CmsSite currentSite = OpenCms.getSiteManager().getCurrentSite(cms);
525            String serverPrefix = null;
526            if (currentSite == OpenCms.getSiteManager().getDefaultSite()) {
527                Optional<CmsSite> siteForDefaultUri = OpenCms.getSiteManager().getSiteForDefaultUri();
528                if (siteForDefaultUri.isPresent()) {
529                    serverPrefix = siteForDefaultUri.get().getServerPrefix(cms, resourceName);
530                } else {
531                    serverPrefix = OpenCms.getSiteManager().getWorkplaceServer();
532                }
533            } else {
534                serverPrefix = currentSite.getServerPrefix(cms, resourceName);
535            }
536
537            if (!permalink.startsWith(serverPrefix)) {
538                permalink = serverPrefix + permalink;
539            }
540        } catch (CmsException e) {
541            // if something wrong
542            permalink = e.getLocalizedMessage();
543            if (LOG.isErrorEnabled()) {
544                LOG.error(e.getLocalizedMessage(), e);
545            }
546        }
547        return permalink;
548    }
549
550    /**
551     * Returns the perma link for the current page based on the URI and detail content id stored in the CmsObject passed as a parameter.<p<
552     *
553     * @param cms the CMS context to use to generate the permalink
554     *
555     * @return the permalink
556     */
557    public String getPermalinkForCurrentPage(CmsObject cms) {
558
559        return getPermalink(cms, cms.getRequestContext().getUri(), cms.getRequestContext().getDetailContentId());
560    }
561
562    /**
563     * Returns the result of the last extern link validation.<p>
564     *
565     * @return the result of the last extern link validation
566     */
567    public CmsExternalLinksValidationResult getPointerLinkValidationResult() {
568
569        return m_pointerLinkValidationResult;
570    }
571
572    /**
573     * Returns the resource root path in the OpenCms VFS for the given target URI link, or <code>null</code> in
574     * case the link points to an external site.<p>
575     *
576     * This methods does not support relative target URI links, so the given URI must be an absolute link.<p>
577     *
578     * See {@link #getRootPath(CmsObject, String)} for a full explanation of this method.<p>
579     *
580     * @param cms the current users OpenCms context
581     * @param targetUri the target URI link
582     *
583     * @return the resource root path in the OpenCms VFS for the given target URI link, or <code>null</code> in
584     *      case the link points to an external site
585     *
586     * @see #getRootPath(CmsObject, String, String)
587     *
588     * @since 7.0.2
589     */
590    public String getRootPath(CmsObject cms, String targetUri) {
591
592        return getRootPath(cms, targetUri, null);
593    }
594
595    /**
596     * Returns the resource root path in the OpenCms VFS for the given target URI link, or <code>null</code> in
597     * case the link points to an external site.<p>
598     *
599     * The default implementation applies the following transformations to the link:<ul>
600     * <li>In case the link starts with a VFS prefix (for example <code>/opencms/opencms</code>,
601     *      this prefix is removed from the result
602     * <li>In case the link is not a root path, the current site root is appended to the result.<p>
603     * <li>In case the link is relative, it will be made absolute using the given absolute <code>basePath</code>
604     *      as starting point.<p>
605     * <li>In case the link contains a server schema (for example <code>http://www.mysite.de/</code>),
606     *      which points to a configured site in OpenCms, the server schema is replaced with
607     *      the root path of the site.<p>
608     * <li>In case the link points to an external site, or in case it is not a valid URI,
609     *      then <code>null</code> is returned.<p>
610     * </ul>
611     *
612     * Please note the above text describes the default behavior as implemented by
613     * {@link CmsDefaultLinkSubstitutionHandler}, which can be fully customized using
614     * the {@link I_CmsLinkSubstitutionHandler} interface.<p>
615     *
616     * @param cms the current users OpenCms context
617     * @param targetUri the target URI link
618     * @param basePath path to use as base in case the target URI is relative (can be <code>null</code>)
619     *
620     * @return the resource root path in the OpenCms VFS for the given target URI link, or <code>null</code> in
621     *      case the link points to an external site
622     *
623     * @see I_CmsLinkSubstitutionHandler for the interface that can be used to fully customize the link substitution
624     * @see CmsDefaultLinkSubstitutionHandler for the default link substitution handler
625     *
626     * @since 7.0.2
627     */
628    public String getRootPath(CmsObject cms, String targetUri, String basePath) {
629
630        return m_linkSubstitutionHandler.getRootPath(cms, targetUri, basePath);
631    }
632
633    /**
634     * Returns the link for the given resource in the current project, with full server prefix.<p>
635     *
636     * Like <code>http://site.enterprise.com:8080/index.html</code>.<p>
637     *
638     * In case the resource name is a full root path, the site from the root path will be used.
639     * Otherwise the resource is assumed to be in the current site set be the OpenCms user context.<p>
640     *
641     * @param cms the current OpenCms user context
642     * @param resourceName the resource to generate the online link for
643     *
644     * @return the link for the given resource in the current project, with full server prefix
645     *
646     * @see #getOnlineLink(CmsObject, String)
647     */
648    public String getServerLink(CmsObject cms, String resourceName) {
649
650        return getServerLink(cms, resourceName, false);
651    }
652
653    /**
654     * Returns the link for the given resource in the current project, with full server prefix.<p>
655     *
656     * Like <code>http://site.enterprise.com:8080/index.html</code>.<p>
657     *
658     * In case the resource name is a full root path, the site from the root path will be used.
659     * Otherwise the resource is assumed to be in the current site set be the OpenCms user context.<p>
660     *
661     * @param cms the current OpenCms user context
662     * @param resourceName the resource to generate the online link for
663     * @param forceSecure forces the secure server prefix
664     *
665     * @return the link for the given resource in the current project, with full server prefix
666     *
667     * @see #getOnlineLink(CmsObject, String)
668     */
669    public String getServerLink(CmsObject cms, String resourceName, boolean forceSecure) {
670
671        String result = substituteLinkForUnknownTarget(cms, resourceName, forceSecure);
672        return appendServerPrefix(cms, result, resourceName, false);
673    }
674
675    /**
676     * Returns the link for the given workplace resource.
677     *
678     * This should only be used for resources under /system or /shared.<p<
679     *
680     * @param cms the current OpenCms user context
681     * @param resourceName the resource to generate the online link for
682     * @param forceSecure forces the secure server prefix
683     *
684     * @return the link for the given resource
685     */
686    public String getWorkplaceLink(CmsObject cms, String resourceName, boolean forceSecure) {
687
688        String result = substituteLinkForUnknownTarget(cms, resourceName, forceSecure);
689        return appendServerPrefix(cms, result, resourceName, true);
690
691    }
692
693    /**
694     * Sets the internal link substitution handler.<p>
695     *
696     * @param cms an OpenCms user context that must have the permissions for role {@link CmsRole#ROOT_ADMIN}.<p>
697     * @param linkSubstitutionHandler the handler to set
698     *
699     * @throws CmsRoleViolationException in case the provided OpenCms user context does not have the required permissions
700     */
701    public void setLinkSubstitutionHandler(CmsObject cms, I_CmsLinkSubstitutionHandler linkSubstitutionHandler)
702    throws CmsRoleViolationException {
703
704        OpenCms.getRoleManager().checkRole(cms, CmsRole.ROOT_ADMIN);
705        m_linkSubstitutionHandler = linkSubstitutionHandler;
706    }
707
708    /**
709     * Sets the result of an external link validation.<p>
710     *
711     * @param externLinkValidationResult the result an external link validation
712     */
713    public void setPointerLinkValidationResult(CmsExternalLinksValidationResult externLinkValidationResult) {
714
715        m_pointerLinkValidationResult = externLinkValidationResult;
716    }
717
718    /**
719     * Returns a link <i>from</i> the URI stored in the provided OpenCms user context
720     * <i>to</i> the given VFS resource, for use on web pages.<p>
721     *
722     * The result will contain the configured context path and
723     * servlet name, and in the case of the "online" project it will also be rewritten according to
724     * to the configured static export settings.<p>
725     *
726     * Should the current site of the given OpenCms user context <code>cms</code> be different from the
727     * site root of the given resource, the result will contain the full server URL to the target resource.<p>
728     *
729     * Please note the above text describes the default behavior as implemented by
730     * {@link CmsDefaultLinkSubstitutionHandler}, which can be fully customized using the
731     * {@link I_CmsLinkSubstitutionHandler} interface.<p>
732     *
733     * @param cms the current OpenCms user context
734     * @param resource the VFS resource the link should point to
735     *
736     * @return a link <i>from</i> the URI stored in the provided OpenCms user context
737     *      <i>to</i> the given VFS resource, for use on web pages
738     */
739    public String substituteLink(CmsObject cms, CmsResource resource) {
740
741        return substituteLinkForRootPath(cms, resource.getRootPath());
742    }
743
744    /**
745     * Returns a link <i>from</i> the URI stored in the provided OpenCms user context
746     * <i>to</i> the VFS resource indicated by the given <code>link</code> in the current site,
747     * for use on web pages.<p>
748     *
749     * The provided <code>link</code> is assumed to be the contained in the site currently
750     * set in the provided OpenCms user context <code>cms</code>.<p>
751     *
752     * The result will be an absolute link that contains the configured context path and
753     * servlet name, and in the case of the "online" project it will also be rewritten according to
754     * to the configured static export settings.<p>
755     *
756     * In case <code>link</code> is a relative URI, the current URI contained in the provided
757     * OpenCms user context <code>cms</code> is used to make the relative <code>link</code> absolute.<p>
758     *
759     * Please note the above text describes the default behavior as implemented by
760     * {@link CmsDefaultLinkSubstitutionHandler}, which can be fully customized using the
761     * {@link I_CmsLinkSubstitutionHandler} interface.<p>
762     *
763     * @param cms the current OpenCms user context
764     * @param link the link to process which is assumed to point to a VFS resource, with optional parameters
765
766     * @return a link <i>from</i> the URI stored in the provided OpenCms user context
767     *      <i>to</i> the VFS resource indicated by the given <code>link</code> in the current site
768     */
769    public String substituteLink(CmsObject cms, String link) {
770
771        return substituteLink(cms, link, null, false);
772    }
773
774    /**
775     * Returns a link <i>from</i> the URI stored in the provided OpenCms user context
776     * <i>to</i> the VFS resource indicated by the given <code>link</code> and <code>siteRoot</code>,
777     * for use on web pages.<p>
778     *
779     * The result will be an absolute link that contains the configured context path and
780     * servlet name, and in the case of the "online" project it will also be rewritten according to
781     * to the configured static export settings.<p>
782     *
783     * In case <code>link</code> is a relative URI, the current URI contained in the provided
784     * OpenCms user context <code>cms</code> is used to make the relative <code>link</code> absolute.<p>
785     *
786     * The provided <code>siteRoot</code> is assumed to be the "home" of the link.
787     * In case the current site of the given OpenCms user context <code>cms</code> is different from the
788     * provided <code>siteRoot</code>, the full server prefix is appended to the result link.<p>
789     *
790     * Please note the above text describes the default behavior as implemented by
791     * {@link CmsDefaultLinkSubstitutionHandler}, which can be fully customized using the
792     * {@link I_CmsLinkSubstitutionHandler} interface.<p>
793     *
794     * @param cms the current OpenCms user context
795     * @param link the link to process which is assumed to point to a VFS resource, with optional parameters
796     * @param siteRoot the site root of the <code>link</code>
797     *
798     * @return the substituted link
799     */
800    public String substituteLink(CmsObject cms, String link, String siteRoot) {
801
802        return substituteLink(cms, link, siteRoot, false);
803    }
804
805    /**
806     * Returns a link <i>from</i> the URI stored in the provided OpenCms user context
807     * <i>to</i> the VFS resource indicated by the given <code>link</code> and <code>siteRoot</code>,
808     * for use on web pages, using the configured link substitution handler.<p>
809     *
810     * The result will be an absolute link that contains the configured context path and
811     * servlet name, and in the case of the "online" project it will also be rewritten according to
812     * to the configured static export settings.<p>
813     *
814     * In case <code>link</code> is a relative URI, the current URI contained in the provided
815     * OpenCms user context <code>cms</code> is used to make the relative <code>link</code> absolute.<p>
816     *
817     * The provided <code>siteRoot</code> is assumed to be the "home" of the link.
818     * In case the current site of the given OpenCms user context <code>cms</code> is different from the
819     * provided <code>siteRoot</code>, the full server prefix is appended to the result link.<p>
820     *
821     * A server prefix is also added if
822     * <ul>
823     *   <li>the link is contained in a normal document and the link references a secure document</li>
824     *   <li>the link is contained in a secure document and the link references a normal document</li>
825     * </ul>
826     *
827     * Please note the above text describes the default behavior as implemented by
828     * {@link CmsDefaultLinkSubstitutionHandler}, which can be fully customized using the
829     * {@link I_CmsLinkSubstitutionHandler} interface.<p>
830     *
831     * @param cms the current OpenCms user context
832     * @param link the link to process which is assumed to point to a VFS resource, with optional parameters
833     * @param siteRoot the site root of the <code>link</code>
834     * @param forceSecure if <code>true</code> generates always an absolute URL (with protocol and server name) for secure links
835     *
836     * @return a link <i>from</i> the URI stored in the provided OpenCms user context
837     *      <i>to</i> the VFS resource indicated by the given <code>link</code> and <code>siteRoot</code>
838     *
839     * @see I_CmsLinkSubstitutionHandler for the interface that can be used to fully customize the link substitution
840     * @see CmsDefaultLinkSubstitutionHandler for the default link substitution handler
841     */
842    public String substituteLink(CmsObject cms, String link, String siteRoot, boolean forceSecure) {
843
844        return substituteLink(cms, link, siteRoot, null, forceSecure);
845    }
846
847    /**
848     * Returns a link <i>from</i> the URI stored in the provided OpenCms user context
849     * <i>to</i> the VFS resource indicated by the given <code>link</code> and <code>siteRoot</code>,
850     * for use on web pages, using the configured link substitution handler.<p>
851     *
852     * The result will be an absolute link that contains the configured context path and
853     * servlet name, and in the case of the "online" project it will also be rewritten according to
854     * to the configured static export settings.<p>
855     *
856     * In case <code>link</code> is a relative URI, the current URI contained in the provided
857     * OpenCms user context <code>cms</code> is used to make the relative <code>link</code> absolute.<p>
858     *
859     * The provided <code>siteRoot</code> is assumed to be the "home" of the link.
860     * In case the current site of the given OpenCms user context <code>cms</code> is different from the
861     * provided <code>siteRoot</code>, the full server prefix is appended to the result link.<p>
862     *
863     * A server prefix is also added if
864     * <ul>
865     *   <li>the link is contained in a normal document and the link references a secure document</li>
866     *   <li>the link is contained in a secure document and the link references a normal document</li>
867     * </ul>
868     *
869     * Please note the above text describes the default behavior as implemented by
870     * {@link CmsDefaultLinkSubstitutionHandler}, which can be fully customized using the
871     * {@link I_CmsLinkSubstitutionHandler} interface.<p>
872     *
873     * @param cms the current OpenCms user context
874     * @param link the link to process which is assumed to point to a VFS resource, with optional parameters
875     * @param siteRoot the site root of the <code>link</code>
876     * @param targetDetailPage the target detail page, in case of linking to a specific detail page
877     * @param forceSecure if <code>true</code> generates always an absolute URL (with protocol and server name) for secure links
878     *
879     * @return a link <i>from</i> the URI stored in the provided OpenCms user context
880     *      <i>to</i> the VFS resource indicated by the given <code>link</code> and <code>siteRoot</code>
881     *
882     * @see I_CmsLinkSubstitutionHandler for the interface that can be used to fully customize the link substitution
883     * @see CmsDefaultLinkSubstitutionHandler for the default link substitution handler
884     */
885    public String substituteLink(
886        CmsObject cms,
887        String link,
888        String siteRoot,
889        String targetDetailPage,
890        boolean forceSecure) {
891
892        if (targetDetailPage != null) {
893            return m_linkSubstitutionHandler.getLink(cms, link, siteRoot, targetDetailPage, forceSecure);
894        } else {
895            return m_linkSubstitutionHandler.getLink(cms, link, siteRoot, forceSecure);
896        }
897
898    }
899
900    /**
901     * Returns a link <i>from</i> the URI stored in the provided OpenCms user context
902     * <i>to</i> the VFS resource indicated by the given root path, for use on web pages.<p>
903     *
904     * The result will contain the configured context path and
905     * servlet name, and in the case of the "online" project it will also be rewritten according to
906     * to the configured static export settings.<p>
907     *
908     * Should the current site of the given OpenCms user context <code>cms</code> be different from the
909     * site root of the given resource root path, the result will contain the full server URL to the target resource.<p>
910     *
911     * @param cms the current OpenCms user context
912     * @param rootPath the VFS resource root path the link should point to
913     *
914     * @return a link <i>from</i> the URI stored in the provided OpenCms user context
915     *      <i>to</i> the VFS resource indicated by the given root path
916     */
917    public String substituteLinkForRootPath(CmsObject cms, String rootPath) {
918
919        String siteRoot = OpenCms.getSiteManager().getSiteRoot(rootPath);
920        if (siteRoot == null) {
921            // use current site root in case no valid site root is available
922            // this will also be the case if a "/system" link is used
923            siteRoot = cms.getRequestContext().getSiteRoot();
924        }
925        String sitePath;
926        if (rootPath.startsWith(siteRoot)) {
927            // only cut the site root if the root part really has this prefix
928            sitePath = rootPath.substring(siteRoot.length());
929        } else {
930            sitePath = rootPath;
931        }
932        return substituteLink(cms, sitePath, siteRoot, false);
933    }
934
935    /**
936     * Returns a link <i>from</i> the URI stored in the provided OpenCms user context
937     * <i>to</i> the given <code>link</code>, for use on web pages.<p>
938     *
939     * A number of tests are performed with the <code>link</code> in order to find out how to create the link:<ul>
940     * <li>If <code>link</code> is empty, an empty String is returned.
941     * <li>If <code>link</code> starts with an URI scheme component, for example <code>http://</code>,
942     * and does not point to an internal OpenCms site, it is returned unchanged.
943     * <li>If <code>link</code> is an absolute URI that starts with a configured site root,
944     * the site root is cut from the link and
945     * the same result as {@link #substituteLink(CmsObject, String, String)} is returned.
946     * <li>Otherwise the same result as {@link #substituteLink(CmsObject, String)} is returned.
947     * </ul>
948     *
949     * @param cms the current OpenCms user context
950     * @param link the link to process
951     *
952     * @return a link <i>from</i> the URI stored in the provided OpenCms user context
953     *      <i>to</i> the given <code>link</code>
954     */
955    public String substituteLinkForUnknownTarget(CmsObject cms, String link) {
956
957        return substituteLinkForUnknownTarget(cms, link, false);
958
959    }
960
961    /**
962     * Returns a link <i>from</i> the URI stored in the provided OpenCms user context
963     * <i>to</i> the given <code>link</code>, for use on web pages.<p>
964     *
965     * A number of tests are performed with the <code>link</code> in order to find out how to create the link:<ul>
966     * <li>If <code>link</code> is empty, an empty String is returned.
967     * <li>If <code>link</code> starts with an URI scheme component, for example <code>http://</code>,
968     * and does not point to an internal OpenCms site, it is returned unchanged.
969     * <li>If <code>link</code> is an absolute URI that starts with a configured site root,
970     * the site root is cut from the link and
971     * the same result as {@link #substituteLink(CmsObject, String, String)} is returned.
972     * <li>Otherwise the same result as {@link #substituteLink(CmsObject, String)} is returned.
973     * </ul>
974     *
975     * @param cms the current OpenCms user context
976     * @param link the link to process
977     * @param forceSecure forces the secure server prefix if the link target is secure
978     *
979     * @return a link <i>from</i> the URI stored in the provided OpenCms user context
980     *      <i>to</i> the given <code>link</code>
981     */
982    public String substituteLinkForUnknownTarget(CmsObject cms, String link, boolean forceSecure) {
983
984        return substituteLinkForUnknownTarget(cms, link, null, forceSecure);
985    }
986
987    /**
988     * Returns a link <i>from</i> the URI stored in the provided OpenCms user context
989     * <i>to</i> the given <code>link</code>, for use on web pages.<p>
990     *
991     * A number of tests are performed with the <code>link</code> in order to find out how to create the link:<ul>
992     * <li>If <code>link</code> is empty, an empty String is returned.
993     * <li>If <code>link</code> starts with an URI scheme component, for example <code>http://</code>,
994     * and does not point to an internal OpenCms site, it is returned unchanged.
995     * <li>If <code>link</code> is an absolute URI that starts with a configured site root,
996     * the site root is cut from the link and
997     * the same result as {@link #substituteLink(CmsObject, String, String)} is returned.
998     * <li>Otherwise the same result as {@link #substituteLink(CmsObject, String)} is returned.
999     * </ul>
1000     *
1001     * @param cms the current OpenCms user context
1002     * @param link the link to process
1003     * @param targetDetailPage the target detail page, in case of linking to a specific detail page
1004     * @param forceSecure forces the secure server prefix if the link target is secure
1005     *
1006     * @return a link <i>from</i> the URI stored in the provided OpenCms user context
1007     *      <i>to</i> the given <code>link</code>
1008     */
1009    public String substituteLinkForUnknownTarget(
1010        CmsObject cms,
1011        String link,
1012        String targetDetailPage,
1013        boolean forceSecure) {
1014
1015        if (CmsStringUtil.isEmpty(link)) {
1016            return "";
1017        }
1018        String sitePath = link;
1019        String siteRoot = null;
1020        if (hasScheme(link)) {
1021            // the link has a scheme, that is starts with something like "http://"
1022            // usually this should be a link to an external resource, but check anyway
1023            sitePath = getRootPath(cms, link);
1024            if (sitePath == null) {
1025                // probably an external link, don't touch this
1026                return link;
1027            }
1028        }
1029        // check if we can find a site from the link
1030        siteRoot = OpenCms.getSiteManager().getSiteRoot(sitePath);
1031        if (siteRoot == null) {
1032            // use current site root in case no valid site root is available
1033            // this will also be the case if a "/system" link is used
1034            String fullPath = cms.addSiteRoot(sitePath); // fullPath can contain query string, but works fine currently with site manager
1035            String siteRootForFullPath = OpenCms.getSiteManager().getSiteRoot(fullPath);
1036            if (OpenCms.getSiteManager().isNestedSite(siteRootForFullPath)) {
1037                // special case for link target in a site nested in the current site
1038                siteRoot = siteRootForFullPath;
1039                sitePath = fullPath.substring(siteRoot.length());
1040            } else {
1041                siteRoot = cms.getRequestContext().getSiteRoot();
1042            }
1043        } else {
1044            // we found a site root, cut this from the resource path
1045            sitePath = sitePath.substring(siteRoot.length());
1046        }
1047        return substituteLink(cms, sitePath, siteRoot, targetDetailPage, forceSecure);
1048    }
1049
1050    /**
1051     * Returns the link for the given resource in the current project, with full server prefix.<p>
1052     *
1053     * The input link must already have been processed according to the link substitution rules.
1054     * This method does just append the server prefix in case this is requires.<p>
1055     *
1056     * @param cms the current OpenCms user context
1057     * @param link the resource to generate the online link for
1058     * @param pathWithOptionalParameters the resource name
1059     * @param workplaceLink if this is set, use the workplace server prefix even if we are in the Online project
1060     *
1061     * @return the link for the given resource in the current project, with full server prefix
1062     */
1063    private String appendServerPrefix(
1064        CmsObject cms,
1065        String link,
1066        String pathWithOptionalParameters,
1067        boolean workplaceLink) {
1068
1069        int paramPos = pathWithOptionalParameters.indexOf("?");
1070        String resourceName = paramPos > -1
1071        ? pathWithOptionalParameters.substring(0, paramPos)
1072        : pathWithOptionalParameters;
1073
1074        if (isAbsoluteUri(link) && !hasScheme(link)) {
1075            // URI is absolute and contains no schema
1076            // this indicates source and target link are in the same site
1077            String serverPrefix;
1078            if (cms.getRequestContext().getCurrentProject().isOnlineProject() && !workplaceLink) {
1079                String overrideSiteRoot = (String)(cms.getRequestContext().getAttribute(
1080                    CmsDefaultLinkSubstitutionHandler.OVERRIDE_SITEROOT_PREFIX + link));
1081                // on online project, get the real site name from the site manager
1082                CmsSite currentSite = OpenCms.getSiteManager().getSite(
1083                    overrideSiteRoot != null ? overrideSiteRoot : resourceName,
1084                    cms.getRequestContext().getSiteRoot());
1085                serverPrefix = currentSite.getServerPrefix(cms, resourceName);
1086            } else {
1087                // in offline mode, source must be the workplace
1088                // so append the workplace server so links can still be clicked
1089                serverPrefix = OpenCms.getSiteManager().getWorkplaceServer(cms);
1090            }
1091            link = serverPrefix + link;
1092        }
1093        return link;
1094    }
1095}