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