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