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 GNUAbstractCollection<String>
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.jsp.util;
029
030import org.opencms.file.CmsObject;
031import org.opencms.file.CmsResource;
032import org.opencms.jsp.CmsJspResourceWrapper;
033import org.opencms.main.CmsLog;
034import org.opencms.main.OpenCms;
035import org.opencms.relations.CmsLink;
036import org.opencms.relations.CmsRelationType;
037import org.opencms.site.CmsSite;
038import org.opencms.staticexport.CmsLinkProcessor;
039import org.opencms.staticexport.CmsLinkProcessor.ExternalLinkWhitelistInfo;
040import org.opencms.util.CmsStringUtil;
041import org.opencms.xml.types.CmsXmlVarLinkValue;
042
043import java.net.URI;
044import java.net.URISyntaxException;
045import java.util.AbstractCollection;
046import java.util.Collections;
047import java.util.Iterator;
048import java.util.Map;
049import java.util.Optional;
050import java.util.Set;
051import java.util.concurrent.ConcurrentHashMap;
052
053import org.apache.commons.lang3.StringUtils;
054import org.apache.commons.logging.Log;
055
056/**
057 * Wrapper for handling links in template/formatter JSP EL.
058 */
059public class CmsJspLinkWrapper extends AbstractCollection<String> {
060
061    /** Logger instance for this class. */
062    private static final Log LOG = CmsLog.getLog(CmsJspLinkWrapper.class);
063
064    /** Stored CMS context. */
065    protected CmsObject m_cms;
066
067    /** Cached internal/external state. */
068    protected Boolean m_internal;
069
070    /** The link literal from which this wrapper was created. */
071    protected String m_link;
072
073    /** Cached link target resource. */
074    protected Optional<CmsResource> m_resource;
075
076    /** Cached links (online, perma, server). */
077    protected Map<String, String> m_stringCache = new ConcurrentHashMap<>();
078
079    /** If <code>true</code> then empty links are allowed. */
080    private boolean m_allowEmpty;
081
082    /**
083     * Creates a new link wrapper for a specific resource.
084     *
085     * @param cms the CMS context
086     * @param resource the resource to link to
087     */
088    public CmsJspLinkWrapper(CmsObject cms, CmsResource resource) {
089
090        m_cms = cms;
091        m_link = cms.getSitePath(resource);
092        m_allowEmpty = false;
093        m_internal = Boolean.TRUE;
094
095    }
096
097    /**
098     * Creates a new link wrapper.<p>
099     *
100     * The link parameter should be in the same format that you enter in an XML content of field of type OpenCmsVarLink, i.e.
101     * either a full external URL or a site path with a query string attached.
102     *
103     * @param cms the CMS context
104     * @param link the link to wrap
105     */
106    public CmsJspLinkWrapper(CmsObject cms, String link) {
107
108        this(cms, link, false);
109    }
110
111    /**
112     * Creates a new link wrapper.<p>
113     *
114     * The link parameter should be in the same format that you enter in an XML content of field of type OpenCmsVarLink, i.e.
115     * either a full external URL or a site path with a query string attached.
116     *
117     * @param cms the CMS context
118     * @param link the link to wrap
119     * @param allowEmpty if <code>true</code> then empty links are allowed
120     */
121    public CmsJspLinkWrapper(CmsObject cms, String link, boolean allowEmpty) {
122
123        m_cms = cms;
124        m_link = link;
125        m_allowEmpty = allowEmpty;
126    }
127
128    /**
129     * @see java.lang.Object#equals(java.lang.Object)
130     */
131    @Override
132    public boolean equals(Object obj) {
133
134        if (obj == this) {
135            return true;
136        }
137        if (obj instanceof CmsJspLinkWrapper) {
138            return obj.toString().equals(toString());
139        }
140        return false;
141    }
142
143    /**
144     * Checks if the link is 'external', which is not just the opposite of 'internal', in this case.
145     *
146     * <p>A link is external if it's either an internal link pointing to a different site, or a link to a location outside
147     * OpenCms, unless the domain is listed in the sitemap attribute 'template.editor.links.externalWhitelist' for the current subsite.
148     *
149     * @return the 'external' status of the link
150     */
151    public boolean getIsExternal() {
152
153        ExternalLinkWhitelistInfo whitelistInfo = CmsLinkProcessor.getExternalLinkWhitelistInfo(m_cms);
154        Set<String> whitelistSiteRoots = whitelistInfo.getSiteRoots();
155        boolean result = false;
156        if (getIsInternal()) {
157            String rootPath = m_cms.getRequestContext().addSiteRoot(m_link);
158            CmsSite site = OpenCms.getSiteManager().getSiteForRootPath(rootPath);
159            CmsSite currentSite = OpenCms.getSiteManager().getSiteForRootPath(m_cms.getRequestContext().getRootUri());
160            if ((site != null) && (currentSite != null) && !currentSite.getSiteRoot().equals(site.getSiteRoot())) {
161                if (!whitelistSiteRoots.contains(site.getSiteRoot())) {
162                    result = true;
163                }
164            }
165        } else {
166            result = true;
167            try {
168                URI uri = new URI(m_link);
169                if (uri.getHost() != null) {
170                    if (whitelistInfo.getWhitelistEntries().stream().anyMatch(
171                        entry -> entry.equals(uri.getHost()) || StringUtils.endsWith(uri.getHost(), "." + entry))) {
172                        result = false;
173                    }
174                }
175            } catch (Exception e) {
176                LOG.error(e.getLocalizedMessage(), e);
177            }
178        }
179        return result;
180    }
181
182    /**
183     * Returns <code>true</code> if the link is internal.
184     *
185     * @return <code>true</code> if the link is internal
186     */
187    public boolean getIsInternal() {
188
189        if (m_internal == null) {
190            if (isEmpty()) {
191                m_internal = Boolean.FALSE;
192            } else {
193                m_internal = Boolean.valueOf(
194                    null != CmsXmlVarLinkValue.getInternalPathAndQuery(m_cms, getServerLink()));
195            }
196        }
197        return m_internal.booleanValue();
198    }
199
200    /**
201     * Performs normal link substitution.
202     *
203     * @return the substituted link
204     */
205    public String getLink() {
206
207        return m_stringCache.computeIfAbsent(
208            "link",
209            k -> (!isEmpty() ? A_CmsJspValueWrapper.substituteLink(m_cms, m_link) : ""));
210    }
211
212    /**
213     * Gets the literal from which this wrapper was constructed.
214     *
215     * @return the original link literal
216     */
217    public String getLiteral() {
218
219        return m_link;
220    }
221
222    /**
223     * Performs online link substitution.
224     *
225     * @return the online link
226     */
227    public String getOnlineLink() {
228
229        return m_stringCache.computeIfAbsent(
230            "online",
231            k -> (!isEmpty() ? OpenCms.getLinkManager().getOnlineLink(m_cms, m_link) : ""));
232    }
233
234    /**
235     * Performs permalink substitution.
236     *
237     * @return the permalink
238     */
239    public String getPermaLink() {
240
241        return m_stringCache.computeIfAbsent(
242            "perma",
243            k -> (!isEmpty() ? OpenCms.getLinkManager().getPermalink(m_cms, m_link) : ""));
244    }
245
246    /**
247     * Gets the resource wrapper for the link target.
248     *
249     * @return the resource wrapper for the target
250     */
251    public CmsJspResourceWrapper getResource() {
252
253        if (m_resource == null) {
254            try {
255                String link = CmsXmlVarLinkValue.getInternalPathAndQuery(m_cms, getServerLink());
256                if (link == null) {
257                    m_resource = Optional.empty();
258                } else {
259                    CmsLink linkObj = new CmsLink(/*name=*/null, CmsRelationType.HYPERLINK, link, true);
260                    linkObj.checkConsistency(m_cms);
261                    m_resource = Optional.ofNullable(linkObj.getResource());
262                }
263            } catch (Exception e) {
264                LOG.warn(e.getLocalizedMessage(), e);
265                m_resource = Optional.empty();
266            }
267        }
268        if (m_resource.isPresent()) {
269            return CmsJspResourceWrapper.wrap(m_cms, m_resource.get());
270        } else {
271            return null;
272        }
273
274    }
275
276    /**
277     * Performs server link substitution.
278     *
279     * @return the server link
280     */
281    public String getServerLink() {
282
283        return m_stringCache.computeIfAbsent(
284            "server",
285            k -> (!isEmpty() ? OpenCms.getLinkManager().getServerLink(m_cms, m_link) : ""));
286    }
287
288    /**
289     * Returns the wrapped link as a String as in {@link #toString()}.<p>
290     *
291     * @return the wrapped link as a String
292     */
293    public String getToString() {
294
295        return toString();
296    }
297
298    /**
299     * Converts the wrapped string to an URI object and returns it.
300     *
301     * <p>If the wrapped string cannont be converted, returns null.
302     *
303     * @return the URI object for the wrapped string, or null if conversion fails
304     */
305    public URI getToURI() {
306
307        return toURI();
308    }
309
310    /**
311     * @see org.opencms.jsp.util.A_CmsJspValueWrapper#hashCode()
312     */
313    @Override
314    public int hashCode() {
315
316        if (m_link == null) {
317            return 0;
318        }
319        return toString().hashCode();
320    }
321
322    /**
323     * Returns <code>true</code> if the wrapped link has been initialized.<p>
324     *
325     * @return <code>true</code> if the wrapped link has been initialized
326     */
327    @Override
328    public boolean isEmpty() {
329
330        if (m_allowEmpty) {
331            return m_link == null;
332        }
333        return CmsStringUtil.isEmptyOrWhitespaceOnly(m_link);
334    }
335
336    /**
337     * @see java.util.AbstractCollection#iterator()
338     */
339    @Override
340    public Iterator<String> iterator() {
341
342        return isEmpty() ? Collections.emptyIterator() : Collections.singletonList(toString()).iterator();
343    }
344
345    /**
346     * @see java.util.AbstractCollection#size()
347     */
348    @Override
349    public int size() {
350
351        return isEmpty() ? 0 : 1;
352    }
353
354    /**
355     * Returns the wrapped link as a String as in {@link #getLink()}.<p>
356     *
357     * @return the wrapped link as a String
358     *
359     * @see #getLiteral()
360     */
361    @Override
362    public String toString() {
363
364        return getLink();
365    }
366
367    /**
368     * Converts the wrapped string to an URI object and returns it.
369     *
370     * <p>If the wrapped string cannont be converted, returns null.
371     *
372     * @return the URI object for the wrapped string, or null if conversion fails
373     */
374    public URI toURI() {
375
376        if (m_link == null) {
377            return null;
378        }
379        try {
380            return new URI(m_link);
381        } catch (URISyntaxException e) {
382            return null;
383        }
384    }
385}