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 GmbH & Co. KG, 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.relations;
029
030import org.opencms.file.CmsObject;
031import org.opencms.file.CmsRequestContext;
032import org.opencms.file.CmsResource;
033import org.opencms.file.CmsResourceFilter;
034import org.opencms.file.CmsVfsResourceNotFoundException;
035import org.opencms.file.wrapper.CmsObjectWrapper;
036import org.opencms.main.CmsException;
037import org.opencms.main.CmsLog;
038import org.opencms.main.CmsStaticResourceHandler;
039import org.opencms.main.OpenCms;
040import org.opencms.security.CmsSecurityException;
041import org.opencms.staticexport.CmsLinkProcessor;
042import org.opencms.util.CmsRequestUtil;
043import org.opencms.util.CmsStringUtil;
044import org.opencms.util.CmsUUID;
045import org.opencms.util.CmsUriSplitter;
046
047import java.util.Map;
048import java.util.Set;
049
050import org.apache.commons.logging.Log;
051
052import org.dom4j.Attribute;
053import org.dom4j.Element;
054
055/**
056 * A single link entry in the link table.<p>
057 *
058 * @since 6.0.0
059 */
060public class CmsLink {
061
062    /** Name of the internal attribute of the link node. */
063    public static final String ATTRIBUTE_INTERNAL = "internal";
064
065    /** Name of the name attribute of the elements node. */
066    public static final String ATTRIBUTE_NAME = "name";
067
068    /** Name of the type attribute of the elements node. */
069    public static final String ATTRIBUTE_TYPE = "type";
070
071    /** Default link name. */
072    public static final String DEFAULT_NAME = "ref";
073
074    /** Default link type. */
075    public static final CmsRelationType DEFAULT_TYPE = CmsRelationType.XML_WEAK;
076
077    /** A dummy uri. */
078    public static final String DUMMY_URI = "@@@";
079
080    /** Name of the anchor node. */
081    public static final String NODE_ANCHOR = "anchor";
082
083    /** Name of the query node. */
084    public static final String NODE_QUERY = "query";
085
086    /** Name of the target node. */
087    public static final String NODE_TARGET = "target";
088
089    /** Name of the UUID node. */
090    public static final String NODE_UUID = "uuid";
091
092    /** Constant for the NULL link. */
093    public static final CmsLink NULL_LINK = new CmsLink();
094
095    /** The log object for this class. */
096    private static final Log LOG = CmsLog.getLog(CmsLink.class);
097
098    /** request context attribute to pass in a custom link renderer. */
099    public static final String CUSTOM_LINK_HANDLER = "CmsLink.customLinkHandler";
100
101    /** The anchor of the URI, if any. */
102    private String m_anchor;
103
104    /** The XML element reference. */
105    private Element m_element;
106
107    /** Indicates if the link is an internal link within the OpenCms VFS. */
108    private boolean m_internal;
109
110    /** The internal name of the link. */
111    private String m_name;
112
113    /** The parameters of the query, if any. */
114    private Map<String, String[]> m_parameters;
115
116    /** Tracks whether there was a security error during the consistency check. */
117    private boolean m_securityErrorDuringConsistencyCheck;
118
119    /** The query, if any. */
120    private String m_query;
121
122    /** The site root of the (internal) link. */
123    private String m_siteRoot;
124
125    /** The structure id of the linked resource. */
126    private CmsUUID m_structureId;
127
128    /** The link target (destination). */
129    private String m_target;
130
131    /** The type of the link. */
132    private CmsRelationType m_type;
133
134    /** The raw uri. */
135    private String m_uri;
136
137    /** The resource the link points to. */
138    private CmsResource m_resource;
139
140    /**
141     * Creates a new link from the given link info bean.
142     *
143     * @param linkInfo the link info bean
144     */
145    public CmsLink(CmsLinkInfo linkInfo) {
146
147        m_name = DEFAULT_NAME;
148        m_type = linkInfo.getType();
149        m_internal = linkInfo.isInternal();
150        m_structureId = linkInfo.getStructureId();
151        m_target = linkInfo.getTarget();
152        m_anchor = linkInfo.getAnchor();
153        m_query = linkInfo.getQuery();
154        setUri();
155    }
156
157    /**
158     * Reconstructs a link object from the given XML node.<p>
159     *
160     * @param element the XML node containing the link information
161     */
162    public CmsLink(Element element) {
163
164        m_element = element;
165        Attribute attrName = element.attribute(ATTRIBUTE_NAME);
166        if (attrName != null) {
167            m_name = attrName.getValue();
168        } else {
169            m_name = DEFAULT_NAME;
170        }
171        Attribute attrType = element.attribute(ATTRIBUTE_TYPE);
172        if (attrType != null) {
173            m_type = CmsRelationType.valueOfXml(attrType.getValue());
174        } else {
175            m_type = DEFAULT_TYPE;
176        }
177        Attribute attrInternal = element.attribute(ATTRIBUTE_INTERNAL);
178        if (attrInternal != null) {
179            m_internal = Boolean.valueOf(attrInternal.getValue()).booleanValue();
180        } else {
181            m_internal = true;
182        }
183
184        Element uuid = element.element(NODE_UUID);
185        Element target = element.element(NODE_TARGET);
186        Element anchor = element.element(NODE_ANCHOR);
187        Element query = element.element(NODE_QUERY);
188
189        m_structureId = (uuid != null) ? new CmsUUID(uuid.getText()) : null;
190        m_target = (target != null) ? target.getText() : null;
191        m_anchor = (anchor != null) ? anchor.getText() : null;
192        setQuery((query != null) ? query.getText() : null);
193
194        // update the uri from the components
195        setUri();
196    }
197
198    /**
199     * Creates a new link object without a reference to the xml page link element.<p>
200     *
201     * @param name the internal name of this link
202     * @param type the type of this link
203     * @param structureId the structure id of the link
204     * @param uri the link uri
205     * @param internal indicates if the link is internal within OpenCms
206     */
207    public CmsLink(String name, CmsRelationType type, CmsUUID structureId, String uri, boolean internal) {
208
209        m_element = null;
210        m_name = name;
211        m_type = type;
212        m_internal = internal;
213        m_structureId = structureId;
214        m_uri = uri;
215        // update component members from the uri
216        setComponents();
217    }
218
219    /**
220     * Creates a new link object without a reference to the xml page link element.<p>
221     *
222     * @param name the internal name of this link
223     * @param type the type of this link
224     * @param uri the link uri
225     * @param internal indicates if the link is internal within OpenCms
226     */
227    public CmsLink(String name, CmsRelationType type, String uri, boolean internal) {
228
229        this(name, type, null, uri, internal);
230    }
231
232    /**
233     *  Empty constructor for NULL constant.<p>
234     */
235    private CmsLink() {
236
237        // empty constructor for NULL constant
238    }
239
240    /**
241     * Checks and updates the structure id or the path of the target.<p>
242     *
243     * @param cms the cms context
244     */
245    public void checkConsistency(CmsObject cms) {
246
247        m_securityErrorDuringConsistencyCheck = false;
248        if (!m_internal || (cms == null)) {
249            return;
250        }
251
252        // in case of static resource links use the null UUID
253        if (CmsStaticResourceHandler.isStaticResourceUri(m_target)) {
254            m_structureId = CmsUUID.getNullUUID();
255            return;
256        }
257
258        try {
259            if (m_structureId == null) {
260                // try by path
261                throw new CmsException(Messages.get().container(Messages.LOG_BROKEN_LINK_NO_ID_0));
262            }
263            // first look for the resource with the given structure id
264            String rootPath = null;
265            CmsResource res;
266            try {
267                res = cms.readResource(m_structureId, CmsResourceFilter.ALL);
268                m_resource = res;
269                rootPath = res.getRootPath();
270                if (!res.getRootPath().equals(m_target)) {
271                    // update path if needed
272                    if (LOG.isDebugEnabled()) {
273                        LOG.debug(
274                            Messages.get().getBundle().key(
275                                Messages.LOG_BROKEN_LINK_UPDATED_BY_ID_3,
276                                m_structureId,
277                                m_target,
278                                res.getRootPath()));
279                    }
280
281                }
282            } catch (CmsException e) {
283                if (e instanceof CmsSecurityException) {
284                    m_securityErrorDuringConsistencyCheck = true;
285                }
286                // not found
287                throw new CmsVfsResourceNotFoundException(
288                    org.opencms.db.generic.Messages.get().container(
289                        org.opencms.db.generic.Messages.ERR_READ_RESOURCE_1,
290                        m_target),
291                    e);
292            }
293            if ((rootPath != null) && !rootPath.equals(m_target)) {
294                // set the new target
295                m_target = res.getRootPath();
296                setUri();
297                // update xml node
298                CmsLinkUpdateUtil.updateXml(this, m_element, true);
299            }
300        } catch (CmsException e) {
301            if (LOG.isDebugEnabled()) {
302                LOG.debug(Messages.get().getBundle().key(Messages.LOG_BROKEN_LINK_BY_ID_2, m_target, m_structureId), e);
303            }
304            if (CmsStringUtil.isEmptyOrWhitespaceOnly(m_target)) {
305                // no correction is possible
306                return;
307            }
308            // go on with the resource with the given path
309            String siteRoot = cms.getRequestContext().getSiteRoot();
310            try {
311                cms.getRequestContext().setSiteRoot("");
312                // now look for the resource with the given path
313                CmsResource res = cms.readResource(m_target, CmsResourceFilter.ALL);
314                m_resource = res;
315                if (!res.getStructureId().equals(m_structureId)) {
316                    // update structure id if needed
317                    if (LOG.isDebugEnabled()) {
318                        LOG.debug(
319                            Messages.get().getBundle().key(
320                                Messages.LOG_BROKEN_LINK_UPDATED_BY_NAME_3,
321                                m_target,
322                                m_structureId,
323                                res.getStructureId()));
324                    }
325                    m_target = res.getRootPath(); // could change by a translation rule
326                    m_structureId = res.getStructureId();
327                    CmsLinkUpdateUtil.updateXml(this, m_element, true);
328                }
329            } catch (CmsException e1) {
330                if (e1 instanceof CmsSecurityException) {
331                    m_securityErrorDuringConsistencyCheck = true;
332                }
333                if (!m_securityErrorDuringConsistencyCheck) {
334                    m_structureId = null;
335                    // no correction was possible
336                    if (LOG.isDebugEnabled()) {
337                        LOG.debug(Messages.get().getBundle().key(Messages.LOG_BROKEN_LINK_BY_NAME_1, m_target), e1);
338                    }
339
340                }
341            } finally {
342                cms.getRequestContext().setSiteRoot(siteRoot);
343            }
344        }
345    }
346
347    /**
348     * A link is considered equal if the link target and the link type is equal.<p>
349     *
350     * @see java.lang.Object#equals(java.lang.Object)
351     */
352    @Override
353    public boolean equals(Object obj) {
354
355        if (obj == this) {
356            return true;
357        }
358        if (obj instanceof CmsLink) {
359            CmsLink other = (CmsLink)obj;
360            return (m_type == other.m_type) && CmsStringUtil.isEqual(m_target, other.m_target);
361        }
362        return false;
363    }
364
365    /**
366     * Returns the anchor of this link.<p>
367     *
368     * @return the anchor or null if undefined
369     */
370    public String getAnchor() {
371
372        return m_anchor;
373    }
374
375    /**
376     * Returns the xml node element representing this link object.<p>
377     *
378     * @return the xml node element representing this link object
379     */
380    public Element getElement() {
381
382        return m_element;
383    }
384
385    /**
386     * Returns the processed link.<p>
387     *
388     * @param cms the current OpenCms user context, can be <code>null</code>
389     *
390     * @return the processed link
391     */
392    public String getLink(CmsObject cms) {
393
394        if (m_internal) {
395            // if we have a local link, leave it unchanged
396            // cms may be null for unit tests
397            if ((cms == null) || (m_uri.length() == 0) || (m_uri.charAt(0) == '#')) {
398                return m_uri;
399            }
400
401            I_CmsCustomLinkRenderer handler = (I_CmsCustomLinkRenderer)cms.getRequestContext().getAttribute(
402                CmsLink.CUSTOM_LINK_HANDLER);
403            if (handler != null) {
404                String handlerResult = handler.getLink(cms, this);
405                if (handlerResult != null) {
406                    return handlerResult;
407                }
408            }
409            checkConsistency(cms);
410            String target = m_target;
411            String uri = computeUri(target, m_query, m_anchor);
412
413            CmsObjectWrapper wrapper = (CmsObjectWrapper)cms.getRequestContext().getAttribute(
414                CmsObjectWrapper.ATTRIBUTE_NAME);
415            if (wrapper != null) {
416                // if an object wrapper is used, rewrite the URI
417                m_uri = wrapper.rewriteLink(m_uri);
418                uri = wrapper.rewriteLink(uri);
419            }
420
421            if ((cms.getRequestContext().getSiteRoot().length() == 0)
422                && (cms.getRequestContext().getAttribute(CmsRequestContext.ATTRIBUTE_EDITOR) == null)) {
423                // Explanation why this check is required:
424                // If the site root name length is 0, this means that a user has switched
425                // the site root to the root site "/" in the Workplace.
426                // In this case the workplace site must also be the active site.
427                // If the editor is opened in the root site, because of this code the links are
428                // always generated _with_ server name / port so that the source code looks identical to code
429                // that would normally be created when running in a regular site.
430                // If normal link processing would be used, the site information in the link
431                // would be lost.
432                return OpenCms.getLinkManager().substituteLink(cms, uri);
433            }
434
435            // get the site root for this URI / link
436            // if there is no site root, we either have a /system link, or the site was deleted,
437            // return the full URI prefixed with the opencms context
438            String siteRoot = getSiteRoot();
439            if (siteRoot == null) {
440                return OpenCms.getLinkManager().substituteLink(cms, uri);
441            }
442
443            if (cms.getRequestContext().getAttribute(CmsRequestContext.ATTRIBUTE_FULLLINKS) != null) {
444                // full links should be generated even if we are in the same site
445                return OpenCms.getLinkManager().getServerLink(cms, uri);
446            }
447
448            // return the link with the server prefix, if necessary
449            return OpenCms.getLinkManager().substituteLink(cms, getSitePath(uri), siteRoot);
450        } else {
451
452            // don't touch external links
453            return m_uri;
454        }
455    }
456
457    /**
458     * Returns the processed link.<p>
459     *
460     * @param cms the current OpenCms user context, can be <code>null</code>
461     * @param processEditorLinks this parameter is not longer used
462     *
463     * @return the processed link
464     *
465     * @deprecated use {@link #getLink(CmsObject)} instead,
466     *      the process editor option is set using the OpenCms request context attributes
467     */
468    @Deprecated
469    public String getLink(CmsObject cms, boolean processEditorLinks) {
470
471        return getLink(cms);
472    }
473
474    /**
475     * Returns the macro name of this link.<p>
476     *
477     * @return the macro name name of this link
478     */
479    public String getName() {
480
481        return m_name;
482    }
483
484    /**
485     * Returns the first parameter value for the given parameter name.<p>
486     *
487     * @param name the name of the parameter
488     * @return the first value for this name or <code>null</code>
489     */
490    public String getParameter(String name) {
491
492        String[] p = getParameterMap().get(name);
493        if (p != null) {
494            return p[0];
495        }
496
497        return null;
498    }
499
500    /**
501     * Returns the map of parameters of this link.<p>
502     *
503     * @return the map of parameters
504     */
505    public Map<String, String[]> getParameterMap() {
506
507        if (m_parameters == null) {
508            m_parameters = CmsRequestUtil.createParameterMap(m_query);
509        }
510        return m_parameters;
511    }
512
513    /**
514     * Returns the set of available parameter names for this link.<p>
515     *
516     * @return the parameter names
517     */
518    public Set<String> getParameterNames() {
519
520        return getParameterMap().keySet();
521    }
522
523    /**
524     * Returns all parameter values for the given name.<p>
525     *
526     * @param name the name of the parameter
527     *
528     * @return all parameter values or <code>null</code>
529     */
530    public String[] getParameterValues(String name) {
531
532        return getParameterMap().get(name);
533    }
534
535    /**
536     * Returns the query of this link.<p>
537     *
538     * @return the query or null if undefined
539     */
540    public String getQuery() {
541
542        return m_query;
543    }
544
545    /**
546     * Returns the resource this link points to, if it is an internal link and has already been initialized via checkConsistency.
547     *
548     * <p>Returns null otherwise.
549     *
550     * @return the resource this link points to
551     */
552    public CmsResource getResource() {
553
554        return m_resource;
555    }
556
557    /**
558     * Returns the vfs link of the target if it is internal.<p>
559     *
560     * @return the full link destination or null if the link is not internal
561     *
562     * @deprecated use {@link #getSitePath(CmsObject)} instead
563     */
564    @Deprecated
565    public String getSitePath() {
566
567        return getSitePath(m_uri);
568    }
569
570    /**
571     * Returns the path of the link target relative to the current site.<p>
572     *
573     * @param cms the CMS context
574     *
575     * @return the site path
576     */
577    public String getSitePath(CmsObject cms) {
578
579        return cms.getRequestContext().removeSiteRoot(m_uri);
580    }
581
582    /**
583     * Return the site root if the target of this link is internal, or <code>null</code> otherwise.<p>
584     *
585     * @return the site root if the target of this link is internal, or <code>null</code> otherwise
586     */
587    public String getSiteRoot() {
588
589        if (m_internal && (m_siteRoot == null)) {
590            m_siteRoot = OpenCms.getSiteManager().getSiteRoot(m_target);
591            if (m_siteRoot == null) {
592                m_siteRoot = "";
593            }
594        }
595        return m_siteRoot;
596    }
597
598    /**
599     * The structure id of the linked resource.<p>
600     *
601     * @return structure id of the linked resource
602     */
603    public CmsUUID getStructureId() {
604
605        return m_structureId;
606    }
607
608    /**
609     * Returns the target (destination) of this link.<p>
610     *
611     * @return the target the target (destination) of this link
612     */
613    public String getTarget() {
614
615        return m_target;
616    }
617
618    /**
619     * Gets the target with the query appended, if there is one.
620     *
621     * @return the target with the query
622     */
623
624    public String getTargetWithQuery() {
625
626        return getTarget() + (getQuery() != null ? "?" + getQuery() : "");
627    }
628
629    /**
630     * Returns the type of this link.<p>
631     *
632     * @return the type of this link
633     */
634    public CmsRelationType getType() {
635
636        return m_type;
637    }
638
639    /**
640     * Returns the raw uri of this link.<p>
641     *
642     * @return the uri
643     */
644    public String getUri() {
645
646        return m_uri;
647    }
648
649    /**
650     * Returns the vfs link of the target if it is internal.<p>
651     *
652     * @return the full link destination or null if the link is not internal
653     *
654     * @deprecated Use {@link #getSitePath()} instead
655     */
656    @Deprecated
657    public String getVfsUri() {
658
659        return getSitePath();
660    }
661
662    /**
663     * Returns true if we had a security exception during the last consistency check.
664     *
665     * @return true if there was a security exception during the consistency check
666     */
667    public boolean hadSecurityErrorDuringLastConsistencyCheck() {
668
669        return m_securityErrorDuringConsistencyCheck;
670    }
671
672    /**
673     * @see java.lang.Object#hashCode()
674     */
675    @Override
676    public int hashCode() {
677
678        int result = m_type.hashCode();
679        if (m_target != null) {
680            result += m_target.hashCode();
681        }
682        return result;
683    }
684
685    /**
686     * Returns if the link is internal.<p>
687     *
688     * @return true if the link is a local link
689     */
690    public boolean isInternal() {
691
692        return m_internal;
693    }
694
695    /**
696     * Converts this link to a link info object.
697     *
698     * @return the link info object
699     */
700    public CmsLinkInfo toLinkInfo() {
701
702        return new CmsLinkInfo(m_structureId, m_target, m_query, m_anchor, m_type, m_internal);
703    }
704
705    /**
706     * @see java.lang.Object#toString()
707     */
708    @Override
709    public String toString() {
710
711        return m_uri;
712    }
713
714    /**
715     * Updates the uri of this link with a new value.<p>
716     *
717     * Also updates the structure of the underlying XML page document this link belongs to.<p>
718     *
719     * Note that you can <b>not</b> update the "internal" or "type" values of the link,
720     * so the new link must be of same type (A, IMG) and also remain either an internal or external link.<p>
721     *
722     * @param uri the uri to update this link with <code>scheme://authority/path#anchor?query</code>
723     */
724    public void updateLink(String uri) {
725
726        // set the uri
727        m_uri = uri;
728
729        // update the components
730        setComponents();
731
732        // update the xml
733        CmsLinkUpdateUtil.updateXml(this, m_element, true);
734    }
735
736    /**
737     * Updates the uri of this link with a new target, anchor and query.<p>
738     *
739     * If anchor and/or query are <code>null</code>, this features are not used.<p>
740     *
741     * Note that you can <b>not</b> update the "internal" or "type" values of the link,
742     * so the new link must be of same type (A, IMG) and also remain either an internal or external link.<p>
743     *
744     * Also updates the structure of the underlying XML page document this link belongs to.<p>
745     *
746     * @param target the target (destination) of this link
747     * @param anchor the anchor or null if undefined
748     * @param query the query or null if undefined
749     */
750    public void updateLink(String target, String anchor, String query) {
751
752        // set the components
753        m_target = target;
754        m_anchor = anchor;
755        setQuery(query);
756
757        // create the uri from the components
758        setUri();
759
760        // update the xml
761        CmsLinkUpdateUtil.updateXml(this, m_element, true);
762    }
763
764    /**
765     * Helper method for getting the site path for a uri.<p>
766     *
767     * @param uri a VFS uri
768     * @return the site path
769     */
770    protected String getSitePath(String uri) {
771
772        if (m_internal) {
773            String siteRoot = getSiteRoot();
774            if (siteRoot != null) {
775                return uri.substring(siteRoot.length());
776            } else {
777                return uri;
778            }
779        }
780        return null;
781    }
782
783    /**
784     * Helper method for creating a uri from its components.<p>
785     *
786     * @param target the uri target
787     * @param query the uri query component
788     * @param anchor the uri anchor component
789     *
790     * @return the uri
791     */
792    private String computeUri(String target, String query, String anchor) {
793
794        StringBuffer uri = new StringBuffer(64);
795        uri.append(target);
796        if (query != null) {
797            uri.append('?');
798            uri.append(query);
799        }
800        if (anchor != null) {
801            uri.append('#');
802            uri.append(anchor);
803        }
804        return uri.toString();
805
806    }
807
808    /**
809     * Sets the component member variables (target, anchor, query)
810     * by splitting the uri <code>scheme://authority/path#anchor?query</code>.<p>
811     */
812    private void setComponents() {
813
814        CmsUriSplitter splitter = new CmsUriSplitter(m_uri, true);
815        m_target = splitter.getPrefix();
816        m_anchor = CmsLinkProcessor.unescapeLink(splitter.getAnchor());
817        setQuery(splitter.getQuery());
818    }
819
820    /**
821     * Sets the query of the link.<p>
822     *
823     * @param query the query to set.
824     */
825    private void setQuery(String query) {
826
827        m_query = CmsLinkProcessor.unescapeLink(query);
828        m_parameters = null;
829    }
830
831    /**
832     * Joins the internal target, anchor and query components
833     * to one uri string, setting the internal uri and parameters fields.<p>
834     */
835    private void setUri() {
836
837        m_uri = computeUri(m_target, m_query, m_anchor);
838    }
839}