001/* 002 * Copyright 2010 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.web.synchronization; 017 018import java.time.ZonedDateTime; 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.Iterator; 022import java.util.List; 023import java.util.Map; 024 025import javax.jcr.ItemNotFoundException; 026import javax.jcr.Node; 027import javax.jcr.NodeIterator; 028import javax.jcr.Property; 029import javax.jcr.PropertyIterator; 030import javax.jcr.RepositoryException; 031import javax.jcr.Session; 032import javax.jcr.version.VersionHistory; 033import javax.mail.MessagingException; 034 035import org.apache.avalon.framework.activity.Initializable; 036import org.apache.avalon.framework.component.Component; 037import org.apache.avalon.framework.logger.AbstractLogEnabled; 038import org.apache.avalon.framework.service.ServiceException; 039import org.apache.avalon.framework.service.ServiceManager; 040import org.apache.avalon.framework.service.Serviceable; 041import org.apache.commons.collections.Predicate; 042import org.apache.commons.collections.PredicateUtils; 043import org.apache.commons.lang.StringUtils; 044 045import org.ametys.cms.repository.CloneComponent; 046import org.ametys.cms.repository.Content; 047import org.ametys.cms.support.AmetysPredicateUtils; 048import org.ametys.core.util.I18nUtils; 049import org.ametys.core.util.mail.SendMailHelper; 050import org.ametys.plugins.repository.AmetysObject; 051import org.ametys.plugins.repository.AmetysObjectIterable; 052import org.ametys.plugins.repository.AmetysObjectResolver; 053import org.ametys.plugins.repository.AmetysRepositoryException; 054import org.ametys.plugins.repository.UnknownAmetysObjectException; 055import org.ametys.plugins.repository.jcr.JCRAmetysObject; 056import org.ametys.plugins.repository.version.VersionableAmetysObject; 057import org.ametys.runtime.config.Config; 058import org.ametys.runtime.i18n.I18nizableText; 059import org.ametys.web.WebConstants; 060import org.ametys.web.repository.page.Page; 061import org.ametys.web.repository.page.Page.LinkType; 062import org.ametys.web.repository.page.Page.PageType; 063import org.ametys.web.repository.page.Zone; 064import org.ametys.web.repository.page.ZoneItem; 065import org.ametys.web.repository.page.jcr.DefaultPage; 066import org.ametys.web.repository.sitemap.Sitemap; 067import org.ametys.web.skin.Skin; 068import org.ametys.web.skin.SkinTemplate; 069import org.ametys.web.skin.SkinTemplateZone; 070import org.ametys.web.skin.SkinsManager; 071 072/** 073 * Helper for common processing used while synchronizing. 074 */ 075public class SynchronizeComponent extends AbstractLogEnabled implements Component, Serviceable, Initializable 076{ 077 /** Avalon Role */ 078 public static final String ROLE = SynchronizeComponent.class.getName(); 079 080 private static SynchronizeComponent _instance; 081 082 private CloneComponent _cloneComponent; 083 private AmetysObjectResolver _resolver; 084 private SkinsManager _skinsManager; 085 private I18nUtils _i18nUtils; 086 087 @Override 088 public void service(ServiceManager smanager) throws ServiceException 089 { 090 _cloneComponent = (CloneComponent) smanager.lookup(CloneComponent.ROLE); 091 _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE); 092 _skinsManager = (SkinsManager) smanager.lookup(SkinsManager.ROLE); 093 _i18nUtils = (I18nUtils) smanager.lookup(I18nUtils.ROLE); 094 } 095 096 @Override 097 public void initialize() throws Exception 098 { 099 _instance = this; 100 } 101 102 /** 103 * Get the unique instance 104 * @return the unique instance 105 */ 106 @Deprecated 107 public static SynchronizeComponent getInstance() 108 { 109 return _instance; 110 } 111 112 /** 113 * Returns true if the hierarchy of the given Page is valid, ie if all its ancestors are valid and synchronized in the live workspace. 114 * @param page the source page. 115 * @param liveSession the live Session. 116 * @return the hierarchy validity status. 117 * @throws RepositoryException if failed to check page hierarchy 118 */ 119 public boolean isHierarchyValid(Page page, Session liveSession) throws RepositoryException 120 { 121 AmetysObject parent = page.getParent(); 122 if (parent instanceof Sitemap) 123 { 124 return true; 125 } 126 127 Page parentPage = (Page) parent; 128 if (!(parentPage instanceof JCRAmetysObject)) 129 { 130 return isPageValid(parentPage, _skinsManager.getSkin(page.getSite().getSkinId())) && isHierarchyValid(parentPage, liveSession); 131 } 132 else if (parentPage.getType() == PageType.NODE) 133 { 134 return isDateValid(parentPage) && isHierarchyValid(parentPage, liveSession); 135 } 136 else 137 { 138 return liveSession.itemExists(((JCRAmetysObject) parentPage).getNode().getPath()); 139 } 140 } 141 142 /** 143 * Returns true if the given page should be synchronized in the live workspace. 144 * @param page the page to test. 145 * @param skin the skin of the page's site. 146 * @return true if the page is valid 147 */ 148 public boolean isPageValid(Page page, Skin skin) 149 { 150 if (!isDateValid(page)) 151 { 152 return false; 153 } 154 155 switch (page.getType()) 156 { 157 case LINK: 158 return _isLinkPageValid(page); 159 case NODE: 160 return _isNodePageValid(page, skin); 161 case CONTAINER: 162 return _isContainerPageValid(page, skin); 163 default: 164 return false; 165 } 166 } 167 168 /** 169 * Returns true if the publication date of the given page are valid. 170 * @param page the page to test. 171 * @return true if the publication dates are valid 172 */ 173 public boolean isDateValid (Page page) 174 { 175 ZonedDateTime startDate = page.getValue(DefaultPage.METADATA_PUBLICATION_START_DATE); 176 ZonedDateTime endDate = page.getValue(DefaultPage.METADATA_PUBLICATION_END_DATE); 177 178 if (startDate != null && startDate.isAfter(ZonedDateTime.now())) 179 { 180 return false; 181 } 182 183 if (endDate != null && endDate.isBefore(ZonedDateTime.now())) 184 { 185 return false; 186 } 187 188 return true; 189 } 190 191 private boolean _isInfiniteRedirection (Page page, List<String> pagesSequence) 192 { 193 Page redirectPage = _getPageRedirection (page); 194 if (redirectPage == null) 195 { 196 return false; 197 } 198 199 if (pagesSequence.contains(redirectPage.getId())) 200 { 201 return true; 202 } 203 204 pagesSequence.add(redirectPage.getId()); 205 return _isInfiniteRedirection (redirectPage, pagesSequence); 206 } 207 208 private Page _getPageRedirection (Page page) 209 { 210 if (PageType.LINK.equals(page.getType()) && LinkType.PAGE.equals(page.getURLType())) 211 { 212 try 213 { 214 String pageId = page.getURL(); 215 return _resolver.resolveById(pageId); 216 } 217 catch (AmetysRepositoryException e) 218 { 219 return null; 220 } 221 } 222 else if (PageType.NODE.equals(page.getType())) 223 { 224 AmetysObjectIterable<? extends Page> childPages = page.getChildrenPages(); 225 Iterator<? extends Page> it = childPages.iterator(); 226 if (it.hasNext()) 227 { 228 return it.next(); 229 } 230 } 231 232 return null; 233 } 234 235 private boolean _isLinkPageValid(Page page) 236 { 237 if (LinkType.WEB.equals(page.getURLType())) 238 { 239 return true; 240 } 241 242 // Check for infinitive loop redirection 243 ArrayList<String> pagesSequence = new ArrayList<>(); 244 pagesSequence.add(page.getId()); 245 if (_isInfiniteRedirection (page, pagesSequence)) 246 { 247 getLogger().error("An infinite loop redirection was detected for page '" + page.getPathInSitemap() + "'"); 248 _sendErrorMailForInfiniteRedirection (page); 249 return false; 250 } 251 252 try 253 { 254 String pageId = page.getURL(); 255 Page linkedPage = _resolver.resolveById(pageId); 256 257 Skin linkedPageSkin = _skinsManager.getSkin(linkedPage.getSite().getSkinId()); 258 return isPageValid(linkedPage, linkedPageSkin); 259 } 260 catch (UnknownAmetysObjectException e) 261 { 262 getLogger().error("Page '" + page.getPathInSitemap() + "' redirects to an unexisting page '" + page.getURL() + "'", e); 263 return false; 264 } 265 catch (AmetysRepositoryException e) 266 { 267 getLogger().error("Unable to check page validity for page link '" + page.getId() + "'", e); 268 return false; 269 } 270 } 271 272 private boolean _isNodePageValid(Page page, Skin skin) 273 { 274 // Check for infinitive loop redirection 275 ArrayList<String> pagesSequence = new ArrayList<>(); 276 pagesSequence.add(page.getId()); 277 if (_isInfiniteRedirection (page, pagesSequence)) 278 { 279 getLogger().error("An infinite loop redirection was detected for page '" + page.getPathInSitemap() + "'"); 280 _sendErrorMailForInfiniteRedirection (page); 281 return false; 282 } 283 284 // a node page is valid if at least one of its child pages is valid 285 AmetysObjectIterable<? extends Page> childPages = page.getChildrenPages(); 286 boolean hasOneConcreteChildPage = false; 287 Iterator<? extends Page> it = childPages.iterator(); 288 289 while (!hasOneConcreteChildPage && it.hasNext()) 290 { 291 Page childPage = it.next(); 292 if (isPageValid(childPage, skin)) 293 { 294 hasOneConcreteChildPage = true; 295 } 296 } 297 298 return hasOneConcreteChildPage; 299 } 300 301 private boolean _isContainerPageValid(Page page, Skin skin) 302 { 303 // a container page is valid if it has no zones or at least one zone is valid 304 if (skin == null) 305 { 306 return false; 307 } 308 else 309 { 310 SkinTemplate template = skin.getTemplate(page.getTemplate()); 311 if (template == null) 312 { 313 return false; 314 } 315 316 Map<String, SkinTemplateZone> modelZones = template.getZones(); 317 Iterator<String> zoneNames = modelZones.keySet().iterator(); 318 319 boolean pageIsValid = modelZones.size() == 0; 320 321 while (!pageIsValid && zoneNames.hasNext()) 322 { 323 // a zone is valid if it is not empty and if at least one ZoneItem is valid 324 String zoneName = zoneNames.next(); 325 326 if (page.hasZone(zoneName)) 327 { 328 Zone zone = page.getZone(zoneName); 329 AmetysObjectIterable<? extends ZoneItem> zoneItems = zone.getZoneItems(); 330 Iterator<? extends ZoneItem> it = zoneItems.iterator(); 331 boolean zoneIsValid = false; 332 333 while (!zoneIsValid && it.hasNext()) 334 { 335 ZoneItem zoneItem = it.next(); 336 zoneIsValid = _isZoneItemValid(zoneItem); 337 } 338 339 pageIsValid = zoneIsValid; 340 } 341 } 342 343 return pageIsValid; 344 } 345 } 346 347 private boolean _isZoneItemValid(ZoneItem zoneItem) 348 { 349 switch (zoneItem.getType()) 350 { 351 case SERVICE: 352 // a service is always valid 353 return true; 354 case CONTENT: 355 try 356 { 357 // a content is valid if it has been validated at least once 358 Content content = zoneItem.getContent(); 359 if (content instanceof VersionableAmetysObject) 360 { 361 return Arrays.asList(((VersionableAmetysObject) content).getAllLabels()).contains(WebConstants.LIVE_LABEL); 362 } 363 else 364 { 365 return false; 366 } 367 } 368 catch (AmetysRepositoryException e) 369 { 370 getLogger().error("Unable to get content property", e); 371 return false; 372 } 373 default: 374 throw new IllegalArgumentException("A zoneItem must be either a service or a content."); 375 } 376 } 377 378 /** 379 * Adds a node to a parent node using a source node for name, type 380 * and potential UUID. 381 * @param srcNode the source node. 382 * @param parentNode the parent node for the newly created node. 383 * @param nodeName the node name to use. 384 * @return the created node. 385 * @throws RepositoryException if an error occurs. 386 */ 387 public Node addNodeWithUUID(Node srcNode, Node parentNode, String nodeName) throws RepositoryException 388 { 389 Node node = null; 390 391 if (AmetysPredicateUtils.isAllowedForLiveContent().evaluate(srcNode)) 392 { 393 node = _cloneComponent.addNodeWithUUID(srcNode, parentNode, nodeName); 394 } 395 396 return node; 397 } 398 399 /** 400 * Clones properties of a node 401 * @param srcNode the source node. 402 * @param clonedNode the cloned node. 403 * @param propertyPredicate the property selector. 404 * @throws RepositoryException if an error occurs. 405 */ 406 public void cloneProperties(Node srcNode, Node clonedNode, Predicate propertyPredicate) throws RepositoryException 407 { 408 if (srcNode == null || clonedNode == null) 409 { 410 return; 411 } 412 413 // Ignore protected properties + filter node/property for live 414 final Predicate predicate = AmetysPredicateUtils.ignoreProtectedProperties(propertyPredicate); 415 416 // First remove existing matching properties from cloned Node 417 PropertyIterator clonedProperties = clonedNode.getProperties(); 418 while (clonedProperties.hasNext()) 419 { 420 Property property = clonedProperties.nextProperty(); 421 if (predicate.evaluate(property)) 422 { 423 property.remove(); 424 } 425 } 426 427 // Then copy properties 428 PropertyIterator itProperties = srcNode.getProperties(); 429 430 while (itProperties.hasNext()) 431 { 432 Property property = itProperties.nextProperty(); 433 434 435 if (predicate.evaluate(property)) 436 { 437 boolean propertyHandled = false; 438 /* 439 if (property.getType() == PropertyType.REFERENCE) 440 { 441 try 442 { 443 Node referencedNode = property.getNode(); 444 if (property.getName().equals("ametys-internal:initial-content")) 445 { 446 // Do not clone the initial-content reference. 447 propertyHandled = true; 448 } 449 450 } 451 catch (ItemNotFoundException e) 452 { 453 // the target node does not exist anymore, this could be due to workflow having been deleted 454 propertyHandled = true; 455 } 456 }*/ 457 458 if (!propertyHandled) 459 { 460 if (property.getDefinition().isMultiple()) 461 { 462 clonedNode.setProperty(property.getName(), property.getValues()); 463 } 464 else 465 { 466 clonedNode.setProperty(property.getName(), property.getValue()); 467 } 468 } 469 } 470 } 471 } 472 473 /** 474 * Clones a node by preserving the source node UUID for a content. 475 * @param srcNode the source node. 476 * @param clonedNode the cloned node. 477 * @param propertyPredicate the property selector. 478 * @param nodePredicate the node selector. 479 * @throws RepositoryException if an error occurs. 480 */ 481 public void cloneContentNodeAndPreserveUUID(Node srcNode, Node clonedNode, Predicate propertyPredicate, Predicate nodePredicate) throws RepositoryException 482 { 483 Predicate finalNodePredicate = PredicateUtils.andPredicate(AmetysPredicateUtils.isAllowedForLiveContent(), nodePredicate); 484 Predicate finalPropertiesPredicate = PredicateUtils.andPredicate(AmetysPredicateUtils.isAllowedForLiveContent(), propertyPredicate); 485 486 cloneNodeAndPreserveUUID(srcNode, clonedNode, finalPropertiesPredicate, finalNodePredicate); 487 } 488 489 /** 490 * Clones properties of a content node 491 * @param srcNode the source node. 492 * @param clonedNode the cloned node. 493 * @param propertyPredicate the property selector. 494 * @throws RepositoryException if an error occurs. 495 */ 496 public void cloneContentProperties(Node srcNode, Node clonedNode, Predicate propertyPredicate) throws RepositoryException 497 { 498 Predicate finalPropertiesPredicate = PredicateUtils.andPredicate(AmetysPredicateUtils.isAllowedForLiveContent(), propertyPredicate); 499 500 cloneProperties(srcNode, clonedNode, finalPropertiesPredicate); 501 } 502 503 /** 504 * Clones a node by preserving the source node UUID. 505 * @param srcNode the source node. 506 * @param clonedNode the cloned node. 507 * @param propertyPredicate the property selector. 508 * @param nodePredicate the node selector. 509 * @throws RepositoryException if an error occurs. 510 */ 511 public void cloneNodeAndPreserveUUID(Node srcNode, Node clonedNode, Predicate propertyPredicate, Predicate nodePredicate) throws RepositoryException 512 { 513 if (srcNode == null || clonedNode == null) 514 { 515 return; 516 } 517 518 // Clone properties 519 cloneProperties (srcNode, clonedNode, propertyPredicate); 520 521 // Remove all matching subNodes before cloning, for better handling of same name siblings 522 NodeIterator subNodes = clonedNode.getNodes(); 523 524 while (subNodes.hasNext()) 525 { 526 Node subNode = subNodes.nextNode(); 527 528 if (nodePredicate.evaluate(subNode)) 529 { 530 subNode.remove(); 531 } 532 } 533 534 // Then copy sub nodes 535 NodeIterator itNodes = srcNode.getNodes(); 536 537 while (itNodes.hasNext()) 538 { 539 Node subNode = itNodes.nextNode(); 540 541 if (nodePredicate.evaluate(subNode)) 542 { 543 Node clonedSubNode = addNodeWithUUID(subNode, clonedNode, subNode.getName()); 544 cloneNodeAndPreserveUUID(subNode, clonedSubNode, propertyPredicate, nodePredicate); 545 } 546 } 547 } 548 549 /** 550 * Will copy the LIVE version of the content into the LIVE workspace. Works only with JCRAmetysObject. 551 * This method DO NOT save the liveSession. 552 * @param content The content to copy 553 * @param liveSession The session for live 554 * @throws RepositoryException If an error occurred 555 */ 556 public void synchronizeContent(Content content, Session liveSession) throws RepositoryException 557 { 558 if (getLogger().isDebugEnabled()) 559 { 560 getLogger().debug("Synchronizing content " + content.getId()); 561 } 562 563 if (!(content instanceof JCRAmetysObject)) 564 { 565 return; 566 } 567 568 Node node = ((JCRAmetysObject) content).getNode(); 569 VersionHistory versionHistory = node.getSession().getWorkspace().getVersionManager().getVersionHistory(node.getPath()); 570 if (Arrays.asList(versionHistory.getVersionLabels()).contains(WebConstants.LIVE_LABEL)) 571 { 572 Node validatedContentNode = versionHistory.getVersionByLabel(WebConstants.LIVE_LABEL).getFrozenNode(); 573 574 Node clonedNode; 575 try 576 { 577 // content already exists in the live workspace 578 clonedNode = liveSession.getNodeByIdentifier(node.getIdentifier()); 579 } 580 catch (ItemNotFoundException e) 581 { 582 // content does not exist in the live workspace 583 584 // clone content itself 585 Node clonedContentParentNode = cloneAncestorsAndPreserveUUID(node, liveSession); 586 587 clonedNode = addNodeWithUUID(validatedContentNode, clonedContentParentNode, node.getName()); 588 589 } 590 591 // First, let's remove all child node and all properties (versionned or not) 592 // Second, clone all versionned node and properties 593 cloneContentNodeAndPreserveUUID(validatedContentNode, clonedNode, PredicateUtils.truePredicate(), PredicateUtils.truePredicate()); 594 595 // Third, clone all unversionned node and properties (such as acl, tags or all unversionned metadata but except initial-content or workflow) 596 cloneContentNodeAndPreserveUUID(node, clonedNode, AmetysPredicateUtils.isNonVersionned(node), AmetysPredicateUtils.isNonVersionned(node)); 597 598 // Clone validation metadata from the current version. 599 cloneContentProperties(node, clonedNode, AmetysPredicateUtils.propertyNamesPredicate("ametys:lastValidationDate", "ametys:lastMajorValidationDate", "ametys:privacy")); // FIXME CMS-7630 privacy should be not versionned 600 } 601 else 602 { 603 try 604 { 605 // content already exists in the live workspace 606 Node clonedNode = liveSession.getNodeByIdentifier(node.getIdentifier()); 607 clonedNode.remove(); 608 } 609 catch (ItemNotFoundException e) 610 { 611 // Ok, the content was not in live 612 } 613 } 614 } 615 616 /** 617 * Clones ancestors of a node by preserving the source node UUID. 618 * @param srcNode the source node. 619 * @param liveSession the session to the live workspace. 620 * @return the parent node in the live workspace. 621 * @throws RepositoryException if an error occurs. 622 */ 623 public Node cloneAncestorsAndPreserveUUID(Node srcNode, Session liveSession) throws RepositoryException 624 { 625 if (srcNode.getName().length() == 0) 626 { 627 // We are on the root node which already exists 628 return liveSession.getRootNode(); 629 } 630 else 631 { 632 Node liveRootNode = liveSession.getRootNode(); 633 Node parentNode = srcNode.getParent(); 634 String parentNodePath = parentNode.getPath().substring(1); 635 636 if (liveRootNode.hasNode(parentNodePath)) 637 { 638 // Found existing parent 639 return liveRootNode.getNode(parentNodePath); 640 } 641 else 642 { 643 Node clonedAncestorNode = cloneAncestorsAndPreserveUUID(parentNode, liveSession); 644 Node clonedParentNode = null; 645 646 if (clonedAncestorNode.hasNode(parentNode.getName())) 647 { 648 // Possible with autocreated children 649 clonedParentNode = clonedAncestorNode.getNode(parentNode.getName()); 650 } 651 else 652 { 653 clonedParentNode = addNodeWithUUID(parentNode, clonedAncestorNode, parentNode.getName()); 654 655 } 656 657 // reorder node when possible 658 if (parentNode.getParent().getPrimaryNodeType().hasOrderableChildNodes()) 659 { 660 orderNode(parentNode.getParent(), parentNode.getName(), clonedParentNode); 661 } 662 663 // update existing acl 664 synchronizeACL(parentNode, liveSession); 665 666 // Copy only properties 667 cloneNodeAndPreserveUUID(parentNode, clonedParentNode, PredicateUtils.truePredicate(), PredicateUtils.falsePredicate()); 668 669 return clonedParentNode; 670 } 671 } 672 } 673 674 /** 675 * Clones a page and its eligible child pages, recursively. 676 * It is assumed that parent page already exist in the live workspace 677 * @param page the page to clone. 678 * @param skin the skin of the page's site. 679 * @param liveSession the session to the live workspace. 680 * @throws RepositoryException if an error occurs. 681 */ 682 public void cloneEligiblePage(Page page, Skin skin, Session liveSession) throws RepositoryException 683 { 684 if (!(page instanceof JCRAmetysObject)) 685 { 686 return; 687 } 688 689 Node pageNode = ((JCRAmetysObject) page).getNode(); 690 String pagePathInJcr = pageNode.getPath().substring(1); 691 Node rootNode = liveSession.getRootNode(); 692 693 boolean isNew = false; 694 Node liveNode = null; 695 696 if (!rootNode.hasNode(pagePathInJcr)) 697 { 698 isNew = true; 699 Node parentNode = pageNode.getParent(); 700 String parentPath = parentNode.getPath().substring(1); 701 String pageName = pageNode.getName(); 702 703 // We assume that the parent Node exists. 704 Node liveParentNode = rootNode.getNode(parentPath); 705 liveNode = addNodeWithUUID(pageNode, liveParentNode, pageNode.getName()); 706 707 // reorder correctly 708 orderNode(parentNode, pageName, liveNode); 709 } 710 else 711 { 712 liveNode = rootNode.getNode(pagePathInJcr); 713 } 714 715 // Clone all but child pages and zones 716 Predicate childPredicate = PredicateUtils.andPredicate(PredicateUtils.notPredicate(AmetysPredicateUtils.nodeTypePredicate("ametys:page")), 717 PredicateUtils.notPredicate(AmetysPredicateUtils.nodeTypePredicate("ametys:zones"))); 718 719 cloneNodeAndPreserveUUID(pageNode, liveNode, PredicateUtils.truePredicate(), childPredicate); 720 721 // Clone zones, if relevant 722 cloneZones(pageNode, page, liveNode); 723 724 // In case of a new page , there may be valid children pages 725 if (isNew) 726 { 727 // Clone each child page 728 for (Page childPage : page.getChildrenPages()) 729 { 730 if ((childPage instanceof JCRAmetysObject) && isPageValid(childPage, skin)) 731 { 732 cloneEligiblePage(childPage, skin, liveSession); 733 } 734 } 735 } 736 } 737 738 /** 739 * Reorder a node, mirroring the order in the default workspace. 740 * @param parentNode the parent of the source Node in the default workspace. 741 * @param nodeName the node name. 742 * @param liveNode the node in the live workspace to be reordered. 743 * @throws RepositoryException if an error occurs. 744 */ 745 public void orderNode(Node parentNode, String nodeName, Node liveNode) throws RepositoryException 746 { 747 _cloneComponent.orderNode(parentNode, nodeName, liveNode); 748 } 749 750 /** 751 * Clones the zones of a page. 752 * @param pageNode the JCR Node of the page. 753 * @param page the page. 754 * @param liveNode the node in the live workspace. 755 * @throws RepositoryException if an error occurs. 756 */ 757 public void cloneZones(Node pageNode, Page page, Node liveNode) throws RepositoryException 758 { 759 if (pageNode.hasNode("ametys-internal:zones")) 760 { 761 if (liveNode.hasNode("ametys-internal:zones")) 762 { 763 liveNode.getNode("ametys-internal:zones").remove(); 764 } 765 766 Node zonesNode = addNodeWithUUID(pageNode.getNode("ametys-internal:zones"), liveNode, "ametys-internal:zones"); 767 768 for (Zone zone : page.getZones()) 769 { 770 Node zoneNode = addNodeWithUUID(((JCRAmetysObject) zone).getNode(), zonesNode, zone.getName()); 771 Node zoneItemsNode = zoneNode.getNode("ametys-internal:zoneItems"); 772 773 for (ZoneItem zoneItem : zone.getZoneItems()) 774 { 775 if (_isZoneItemValid(zoneItem)) 776 { 777 Node srcNode = ((JCRAmetysObject) zoneItem).getNode(); 778 Node zoneItemNode = addNodeWithUUID(srcNode, zoneItemsNode, "ametys:zoneItem"); 779 cloneNodeAndPreserveUUID(srcNode, zoneItemNode, PredicateUtils.truePredicate(), PredicateUtils.truePredicate()); 780 } 781 } 782 } 783 } 784 } 785 786 /** 787 * Invalidates the hierarchy of a page if needed 788 * @param page the page. 789 * @param skin the skin of the page's site. 790 * @param liveSession the session to the live workspace. 791 * @throws RepositoryException if an error occurs. 792 */ 793 public void invalidateHierarchy(Page page, Skin skin, Session liveSession) throws RepositoryException 794 { 795 String jcrPath = ((JCRAmetysObject) page).getNode().getPath(); 796 if (liveSession.itemExists(jcrPath)) 797 { 798 liveSession.getItem(jcrPath).remove(); 799 } 800 801 AmetysObject parent = page.getParent(); 802 803 if (parent instanceof Page) 804 { 805 if (!isPageValid((Page) parent, skin)) 806 { 807 invalidateHierarchy((Page) parent, skin, liveSession); 808 } 809 } 810 } 811 812 /** 813 * Synchronizes a page with the live workspace. Also synchronizes hierarchy. 814 * @param page the page. 815 * @param skin the skin of the page's site. 816 * @param liveSession the session to the live workspace. 817 * @throws RepositoryException if an error occurs. 818 */ 819 public void synchronizePage(Page page, Skin skin, Session liveSession) throws RepositoryException 820 { 821 if (isHierarchyValid(page, liveSession)) 822 { 823 if (isPageValid(page, skin)) 824 { 825 //FIXME clone ancestor pages, not only ancestor nodes 826 cloneAncestorsAndPreserveUUID(((JCRAmetysObject) page).getNode(), liveSession); 827 828 // clone page and valid children 829 cloneEligiblePage(page, skin, liveSession); 830 } 831 else 832 { 833 // page is invalid, remove it from live if it was previously valid, then potentially invalidate hierarchy 834 invalidateHierarchy(page, skin, liveSession); 835 } 836 } 837 } 838 839 private void _sendErrorMailForInfiniteRedirection (Page page) 840 { 841 String recipient = Config.getInstance().getValue("smtp.mail.sysadminto"); 842 String sender = Config.getInstance().getValue("smtp.mail.from"); 843 try 844 { 845 List<String> i18nParams = new ArrayList<>(); 846 i18nParams.add(page.getSite().getTitle()); 847 848 I18nizableText i18nSubject = new I18nizableText("plugin.web", "PLUGINS_WEB_SYNCHRONIZE_INFINITE_REDIRECTION_MAIL_SUBJECT", i18nParams); 849 String subject = _i18nUtils.translate(i18nSubject); 850 851 i18nParams.add(page.getSitemapName() + "/" + page.getPathInSitemap()); 852 i18nParams.add(StringUtils.stripEnd(StringUtils.removeEndIgnoreCase(Config.getInstance().getValue("cms.url"), "index.html"), "/")); 853 String body = _i18nUtils.translate(new I18nizableText("plugin.web", "PLUGINS_WEB_SYNCHRONIZE_INFINITE_REDIRECTION_MAIL_BODY", i18nParams)); 854 855 SendMailHelper.sendMail(subject, null, body, recipient, sender); 856 } 857 catch (MessagingException e) 858 { 859 if (getLogger().isWarnEnabled()) 860 { 861 getLogger().warn("Could not send an alert e-mail to " + recipient, e); 862 } 863 } 864 } 865 866 /** 867 * Synchronize a node ACL information with node in live session if available 868 * This method does NOT save live session 869 * @param node The trunk node in default workspace 870 * @param liveSession The live session 871 */ 872 public void synchronizeACL(Node node, Session liveSession) 873 { 874 try 875 { 876 if (getLogger().isDebugEnabled()) 877 { 878 getLogger().debug("Synchronizing ACL for node " + node.getIdentifier()); 879 } 880 881 try 882 { 883 // content already exists in the live workspace 884 Node clonedNode = liveSession.getNodeByIdentifier(node.getIdentifier()); 885 886 // Clone acl 887 String aclNodeNode = "ametys-internal:acl"; 888 if (node.hasNode(aclNodeNode)) 889 { 890 Node aclNode = node.getNode(aclNodeNode); 891 892 Node aclClonedNode; 893 if (clonedNode.hasNode(aclNodeNode)) 894 { 895 aclClonedNode = clonedNode.getNode(aclNodeNode); 896 } 897 else 898 { 899 aclClonedNode = clonedNode.addNode(aclNodeNode, aclNode.getPrimaryNodeType().getName()); 900 } 901 902 cloneNodeAndPreserveUUID(aclNode, aclClonedNode, PredicateUtils.truePredicate(), PredicateUtils.truePredicate()); 903 } 904 } 905 catch (ItemNotFoundException e) 906 { 907 // Nothing to synchronize: the content is not in live 908 } 909 } 910 catch (RepositoryException e) 911 { 912 throw new RuntimeException("Can not copy ACL for node", e); 913 } 914 } 915}