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}