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