001/* 002 * Copyright 2015 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.repository.page; 017 018import java.util.ArrayList; 019import java.util.Collections; 020import java.util.Date; 021import java.util.HashMap; 022import java.util.HashSet; 023import java.util.Iterator; 024import java.util.List; 025import java.util.Map; 026import java.util.Set; 027import java.util.stream.Collectors; 028 029import javax.jcr.Node; 030import javax.jcr.Repository; 031import javax.jcr.RepositoryException; 032import javax.jcr.Session; 033 034import org.apache.avalon.framework.activity.Initializable; 035import org.apache.avalon.framework.component.Component; 036import org.apache.avalon.framework.service.ServiceException; 037import org.apache.avalon.framework.service.ServiceManager; 038import org.apache.commons.collections4.CollectionUtils; 039import org.apache.commons.lang3.LocaleUtils; 040import org.apache.commons.lang3.StringUtils; 041 042import org.ametys.cms.contenttype.ContentType; 043import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 044import org.ametys.cms.repository.Content; 045import org.ametys.cms.repository.ContentDAO.TagMode; 046import org.ametys.cms.repository.ModifiableContent; 047import org.ametys.cms.repository.WorkflowAwareContent; 048import org.ametys.cms.tag.CMSTag; 049import org.ametys.cms.tag.CMSTag.TagVisibility; 050import org.ametys.cms.tag.TagHelper; 051import org.ametys.cms.tag.TagProviderExtensionPoint; 052import org.ametys.core.cache.AbstractCacheManager; 053import org.ametys.core.cache.Cache; 054import org.ametys.core.observation.Event; 055import org.ametys.core.observation.ObservationManager; 056import org.ametys.core.observation.Observer; 057import org.ametys.core.right.RightManager; 058import org.ametys.core.right.RightManager.RightResult; 059import org.ametys.core.ui.Callable; 060import org.ametys.core.user.CurrentUserProvider; 061import org.ametys.core.user.UserIdentity; 062import org.ametys.plugins.core.impl.cache.AbstractCacheKey; 063import org.ametys.plugins.explorer.ExplorerNode; 064import org.ametys.plugins.explorer.resources.ModifiableResourceCollection; 065import org.ametys.plugins.explorer.resources.Resource; 066import org.ametys.plugins.repository.AmetysObject; 067import org.ametys.plugins.repository.AmetysObjectIterable; 068import org.ametys.plugins.repository.AmetysObjectIterator; 069import org.ametys.plugins.repository.AmetysObjectResolver; 070import org.ametys.plugins.repository.AmetysRepositoryException; 071import org.ametys.plugins.repository.CopiableAmetysObject; 072import org.ametys.plugins.repository.ModifiableAmetysObject; 073import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 074import org.ametys.plugins.repository.RemovableAmetysObject; 075import org.ametys.plugins.repository.RepositoryConstants; 076import org.ametys.plugins.repository.TraversableAmetysObject; 077import org.ametys.plugins.repository.UnknownAmetysObjectException; 078import org.ametys.plugins.repository.jcr.JCRAmetysObject; 079import org.ametys.plugins.repository.jcr.NameHelper; 080import org.ametys.plugins.repository.jcr.SimpleAmetysObject; 081import org.ametys.plugins.repository.lock.LockHelper; 082import org.ametys.plugins.repository.lock.LockableAmetysObject; 083import org.ametys.plugins.repository.query.expression.Expression.Operator; 084import org.ametys.plugins.repository.tag.TaggableAmetysObject; 085import org.ametys.plugins.repository.version.VersionableAmetysObject; 086import org.ametys.plugins.workflow.support.WorkflowProvider; 087import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 088import org.ametys.runtime.i18n.I18nizableText; 089import org.ametys.runtime.model.type.ModelItemTypeConstants; 090import org.ametys.web.ObservationConstants; 091import org.ametys.web.WebConstants; 092import org.ametys.web.alias.Alias.TargetType; 093import org.ametys.web.alias.AliasHelper; 094import org.ametys.web.alias.DefaultAlias; 095import org.ametys.web.repository.content.SharedContent; 096import org.ametys.web.repository.content.WebContent; 097import org.ametys.web.repository.content.shared.SharedContentManager; 098import org.ametys.web.repository.page.Page.LinkType; 099import org.ametys.web.repository.page.Page.PageType; 100import org.ametys.web.repository.page.ZoneItem.ZoneType; 101import org.ametys.web.repository.page.jcr.DefaultPage; 102import org.ametys.web.repository.site.Site; 103import org.ametys.web.repository.sitemap.Sitemap; 104import org.ametys.web.rights.PageRightAssignmentContext; 105import org.ametys.web.service.Service; 106import org.ametys.web.service.ServiceExtensionPoint; 107import org.ametys.web.site.CopyUpdaterExtensionPoint; 108import org.ametys.web.skin.Skin; 109import org.ametys.web.skin.SkinTemplate; 110import org.ametys.web.skin.SkinTemplateZone; 111import org.ametys.web.skin.SkinsManager; 112import org.ametys.web.skin.TemplatesAssignmentHandler; 113import org.ametys.web.synchronization.SynchronizeComponent; 114import org.ametys.web.tags.TagExpression; 115 116/** 117 * DAO for manipulating pages 118 */ 119public class PageDAO extends AbstractSitemapElementsDAO implements Component, Initializable, Observer 120{ 121 /** Constant for untouched binary metadata. */ 122 public static final String __SERVICE_PARAM_UNTOUCHED_BINARY = "untouched"; 123 124 /** Avalon Role */ 125 public static final String ROLE = PageDAO.class.getName(); 126 127 /** Constant for the {@link Cache} id for the pages in cache by sitename, lang, tag */ 128 private static final String MEMORY_PAGESTAGCACHE = PageDAO.class.getName() + "$PagesUUIDByTag"; 129 130 private AmetysObjectResolver _resolver; 131 private ObservationManager _observationManager; 132 private CurrentUserProvider _currentUserProvider; 133 private SkinsManager _skinsManager; 134 private TemplatesAssignmentHandler _templatesHandler; 135 private ServicesAssignmentHandler _serviceHandler; 136 private ContentTypesAssignmentHandler _cTypeHandler; 137 private ContentTypeExtensionPoint _contentTypeExtensionPoint; 138 private ServiceExtensionPoint _serviceExtensionPoint; 139 private WorkflowProvider _workflowProvider; 140 private SharedContentManager _sharedContentManager; 141 private TagProviderExtensionPoint _tagProvider; 142 private CopySiteComponent _copySiteComponent; 143 private RightManager _rightManager; 144 private SynchronizeComponent _synchronizeComponent; 145 private Repository _repository; 146 private AmetysObjectResolver _ametysObjectResolver; 147 private AbstractCacheManager _cacheManager; 148 149 private CopyUpdaterExtensionPoint _copyUpdaterEP; 150 151 @Override 152 public void service(ServiceManager smanager) throws ServiceException 153 { 154 super.service(smanager); 155 156 _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE); 157 _ametysObjectResolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE); 158 _observationManager = (ObservationManager) smanager.lookup(ObservationManager.ROLE); 159 _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE); 160 _skinsManager = (SkinsManager) smanager.lookup(SkinsManager.ROLE); 161 _templatesHandler = (TemplatesAssignmentHandler) smanager.lookup(TemplatesAssignmentHandler.ROLE); 162 _cTypeHandler = (ContentTypesAssignmentHandler) smanager.lookup(ContentTypesAssignmentHandler.ROLE); 163 _serviceHandler = (ServicesAssignmentHandler) smanager.lookup(ServicesAssignmentHandler.ROLE); 164 _contentTypeExtensionPoint = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE); 165 _serviceExtensionPoint = (ServiceExtensionPoint) smanager.lookup(ServiceExtensionPoint.ROLE); 166 _workflowProvider = (WorkflowProvider) smanager.lookup(WorkflowProvider.ROLE); 167 _sharedContentManager = (SharedContentManager) smanager.lookup(SharedContentManager.ROLE); 168 _tagProvider = (TagProviderExtensionPoint) smanager.lookup(TagProviderExtensionPoint.ROLE); 169 _copySiteComponent = (CopySiteComponent) smanager.lookup(CopySiteComponent.ROLE); 170 _rightManager = (RightManager) smanager.lookup(RightManager.ROLE); 171 _synchronizeComponent = (SynchronizeComponent) smanager.lookup(SynchronizeComponent.ROLE); 172 _repository = (Repository) smanager.lookup(Repository.class.getName()); 173 _cacheManager = (AbstractCacheManager) smanager.lookup(AbstractCacheManager.ROLE); 174 _copyUpdaterEP = (CopyUpdaterExtensionPoint) smanager.lookup(CopyUpdaterExtensionPoint.ROLE); 175 } 176 177 public void initialize() throws Exception 178 { 179 _createCaches(); 180 _observationManager.registerObserver(this); 181 } 182 183 private void _createCaches() 184 { 185 _cacheManager.createMemoryCache(MEMORY_PAGESTAGCACHE, 186 new I18nizableText("plugin.web", "PLUGINS_WEB_PAGEDAO_CACHE_PAGES_BY_TAG_LABEL"), 187 new I18nizableText("plugin.web", "PLUGINS_WEB_PAGEDAO_CACHE_PAGES_BY_TAG_DESCRIPTION"), 188 true, 189 null); 190 } 191 192 public int getPriority() 193 { 194 return 0; 195 } 196 197 public boolean supports(Event event) 198 { 199 return event.getId().equals(ObservationConstants.EVENT_PAGE_UPDATED) && event.getArguments().containsKey(ObservationConstants.ARGS_PAGE_TAGS); 200 } 201 202 public void observe(Event event, Map<String, Object> transientVars) throws Exception 203 { 204 Page page = (Page) event.getArguments().get(ObservationConstants.ARGS_PAGE); 205 if (page != null) 206 { 207 _getMemoryPageTagCache().invalidate(PageTagCacheKey.of(page.getSiteName(), page.getSitemapName())); 208 } 209 } 210 211 /** 212 * Get the properties of given pages 213 * @param pageIds the id of pages 214 * @param zoneName The optional zone name to limit informations of zones to that zone. Can be null or empty to avoid limitation. 215 * @param zoneItemId The optional zone item identifier to limit informations of zones to that zone item. Can be null or empty to avoid limitation. 216 * @return the properties of pages in a result map 217 */ 218 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 219 public Map<String, Object> getPagesInfos (List<String> pageIds, String zoneName, String zoneItemId) 220 { 221 // Assume that no read access is checked (required for bus message target) 222 Map<String, Object> result = new HashMap<>(); 223 224 List<Map<String, Object>> pages = new ArrayList<>(); 225 List<String> pagesNotFound = new ArrayList<>(); 226 227 for (String pageId : pageIds) 228 { 229 try 230 { 231 Page page = _resolver.resolveById(pageId); 232 pages.add(getPageInfos(page, zoneName, zoneItemId)); 233 } 234 catch (UnknownAmetysObjectException e) 235 { 236 pagesNotFound.add(pageId); 237 } 238 } 239 240 result.put("pages", pages); 241 result.put("pagesNotFound", pagesNotFound); 242 243 return result; 244 } 245 246 /** 247 * Get the properties of given pages 248 * @param pageIds the id of pages 249 * @return the properties of pages in a result map 250 */ 251 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 252 public Map<String, Object> getPagesInfos (List<String> pageIds) 253 { 254 // Assume that no read access is checked (required for bus message target) 255 return getPagesInfos(pageIds, null, null); 256 } 257 258 /** 259 * Get the page's properties 260 * @param pageId the page ID 261 * @return the properties 262 */ 263 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 264 public Map<String, Object> getPageInfos (String pageId) 265 { 266 // Assume that no read access is checked (required for bus message target) 267 return getPageInfos(pageId, null, null); 268 } 269 270 /** 271 * Get the page's properties 272 * @param pageId the page ID 273 * @param zoneName The optional zone name to limit informations of zones to that zone. Can be null or empty to avoid limitation. 274 * @param zoneItemId The optional zone item identifier to limit informations of zones to that zone item. Can be null or empty to avoid limitation. 275 * @return the properties 276 */ 277 public Map<String, Object> getPageInfos (String pageId, String zoneName, String zoneItemId) 278 { 279 // Assume that no read access is checked (required for bus message target) 280 Page page = _resolver.resolveById(pageId); 281 return getPageInfos(page, zoneName, zoneItemId); 282 } 283 284 /** 285 * Get the page's properties 286 * @param page the page 287 * @param zoneName The optional zone name to limit informations of zones to that zone. Can be null or empty to avoid limitation. 288 * @param zoneItemId The optional zone item identifier to limit informations of zones to that zone item. Can be null or empty to avoid limitation. 289 * @return the properties 290 */ 291 public Map<String, Object> getPageInfos (Page page, String zoneName, String zoneItemId) 292 { 293 Map<String, Object> infos = new HashMap<>(); 294 boolean isLocked = page instanceof LockablePage lockablePage && lockablePage.isLocked(); 295 infos.put("id", page.getId()); 296 infos.put("name", page.getName()); 297 infos.put("parentId", page.getParent().getId()); 298 infos.put("title", page.getTitle()); 299 infos.put("longTitle", page.getLongTitle()); 300 infos.put("path", page.getPathInSitemap()); 301 infos.put("siteName", page.getSiteName()); 302 infos.put("type", page.getType()); 303 infos.put("lang", page.getSitemapName()); 304 infos.put("isModifiable", page instanceof ModifiablePage); 305 infos.put("isMoveable", page instanceof MoveablePage && !isLocked); 306 infos.put("isTaggable", page instanceof TaggableAmetysObject); 307 infos.put("isVisible", page.isVisible()); 308 infos.put("isParentInvisible", _isParentInvisible(page)); 309 infos.put("locked", isLocked); 310 311 // Publication information 312 infos.put("publication", _publication2Json(page)); 313 infos.put("isPreviewable", _isPreviewable(page)); 314 315 // limitation information 316 if (StringUtils.isNotBlank(zoneName)) 317 { 318 infos.put("zoneName", zoneName); 319 } 320 if (StringUtils.isNotBlank(zoneItemId)) 321 { 322 infos.put("zoneItemId", zoneItemId); 323 } 324 325 String skinId = page.getSite().getSkinId(); 326 Skin skin = _skinsManager.getSkin(skinId); 327 infos.put("isPageValid", _synchronizeComponent.isPageValid(page, skin)); 328 329 Session liveSession = null; 330 try 331 { 332 liveSession = _repository.login(WebConstants.LIVE_WORKSPACE); 333 infos.put("isLiveHierarchyValid", _synchronizeComponent.isHierarchyValid(page, liveSession)); 334 } 335 catch (RepositoryException e) 336 { 337 throw new RuntimeException("Unable to check live workspace", e); 338 } 339 finally 340 { 341 if (liveSession != null) 342 { 343 liveSession.logout(); 344 } 345 } 346 347 PageType type = page.getType(); 348 switch (type) 349 { 350 case CONTAINER: 351 infos.putAll(_sitemapElement2json(page, zoneName, zoneItemId)); 352 break; 353 354 case LINK: 355 infos.put("url", page.getURL()); 356 357 LinkType urlType = page.getURLType(); 358 infos.put("urlType", urlType.toString()); 359 360 switch (urlType) 361 { 362 case PAGE: 363 try 364 { 365 Page targetPage = _resolver.resolveById(page.getURL()); 366 infos.put("urlTitle", targetPage.getTitle()); 367 } 368 catch (UnknownAmetysObjectException e) 369 { 370 getLogger().error("Page '" + page.getId() + "' redirects to an unexisting page '" + page.getURL() + "'"); 371 } 372 break; 373 374 default: 375 break; 376 } 377 break; 378 default: 379 AmetysObjectIterator< ? extends Page> iterator = page.getChildrenPages().iterator(); 380 if (iterator.hasNext()) 381 { 382 Page firstSubPage = iterator.next(); 383 infos.put("url", firstSubPage.getId()); 384 infos.put("urlTitle", firstSubPage.getTitle()); 385 infos.put("urlType", LinkType.PAGE.toString()); 386 break; 387 388 } 389 break; 390 } 391 392 393 infos.put("rights", getUserRights(page)); 394 395 return infos; 396 } 397 398 /** 399 * Get the page's properties 400 * @param page the page 401 * @return the properties 402 */ 403 public Map<String, Object> getPageInfos (Page page) 404 { 405 return getPageInfos(page, null, null); 406 } 407 408 /** 409 * Check current user right on given page or sitemap 410 * @param id The id of the page or sitemap 411 * @param rightId The if of right to check 412 * @return true if user has right 413 */ 414 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 415 public boolean hasRight(String id, String rightId) 416 { 417 UserIdentity user = _currentUserProvider.getUser(); 418 SitemapElement page = _resolver.resolveById(id); 419 420 return _rightManager.hasRight(user, rightId, page) == RightResult.RIGHT_ALLOW; 421 } 422 423 /** 424 * Create a new page 425 * @param parentId The parent id. Can not be null. 426 * @param title The page's title. Can not be null. 427 * @param longTitle The page's long title. Can be null or blank. 428 * @return The result map with id of created page 429 */ 430 @Callable (rights = "Web_Rights_Page_Create", rightContext = PageRightAssignmentContext.ID, paramIndex = 0) 431 public Map<String, Object> createPage (String parentId, String title, String longTitle) 432 { 433 Map<String, Object> result = new HashMap<>(); 434 435 SitemapElement parent = _resolver.resolveById(parentId); 436 437 assert parent instanceof ModifiableSitemapElement; 438 439 try 440 { 441 ModifiablePage page = createPage((ModifiableSitemapElement) parent, null, title, longTitle); 442 result.put("id", page.getId()); 443 result.put("parentId", parent.getId()); 444 result.put("lang", page.getSitemapName()); 445 result.put("title", page.getTitle()); 446 } 447 catch (IllegalArgumentException e) 448 { 449 result.put("invalid-name", title); 450 } 451 return result; 452 } 453 454 /** 455 * Create a new page blank page. 456 * 457 * In the case where {@code name} is {@code null}, the value of title will be used. 458 * In all case, the name (or title) will be used to derive the page name (and therefore 459 * path by filtering it with {@link NameHelper#filterName(String)} and appending an integer 460 * if a page with the same name already exist. 461 * 462 * @param parent the sitemap element where the new page is created 463 * @param name the name to use as path for the page. (if null, the title will be used) 464 * @param title the title of the page 465 * @param longTitle the long title of the page 466 * @return The result map with id of the created page 467 * @throws IllegalArgumentException If the provided name (or title) can not be used in URI 468 */ 469 public ModifiablePage createPage(ModifiableSitemapElement parent, String name, String title, String longTitle) throws IllegalArgumentException 470 { 471 Site site = parent.getSite(); 472 473 String pageName = PageDAO.findFreePageNameByTitle(parent, name != null ? name : title); 474 475 ModifiablePage page = parent.createChild(pageName, "ametys:defaultPage"); 476 477 page.setTitle(title); 478 page.setType(PageType.NODE); 479 page.setSiteName(site.getName()); 480 page.setSitemapName(page.getSitemap().getName()); 481 482 if (!StringUtils.isBlank(longTitle)) 483 { 484 page.setLongTitle(longTitle); 485 } 486 487 site.saveChanges(); 488 489 Map<String, Object> eventParams = new HashMap<>(); 490 eventParams.put(ObservationConstants.ARGS_PAGE, page); 491 _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_ADDED, _currentUserProvider.getUser(), eventParams)); 492 493 return page; 494 } 495 496 /** 497 * Copy a page 498 * @param id The id of page to copy 499 * @param target The id of parent target page 500 * @param keepReferences true to keep references 501 * @return the result map 502 * @throws RepositoryException if an error occurred during copy 503 */ 504 @Callable (rights = "Web_Rights_Page_Create", rightContext = PageRightAssignmentContext.ID, paramIndex = 1) 505 public Map<String, String> copyPage (String id, String target, boolean keepReferences) throws RepositoryException 506 { 507 Map<String, String> result = new HashMap<>(); 508 509 Page page = _resolver.resolveById(id); 510 511 if (!(page instanceof CopiableAmetysObject)) 512 { 513 throw new IllegalArgumentException("The page '" + page.getId() + "' is not a copiable ametys object, it can not be copied"); 514 } 515 516 if (!(page instanceof JCRAmetysObject)) 517 { 518 throw new IllegalArgumentException("The page '" + page.getId() + "' is not a JCR ametys object, it can not be copied"); 519 } 520 521 SitemapElement parent = _resolver.resolveById(target); 522 523 if (parent instanceof ModifiableSitemapElement modifiableParent) 524 { 525 ModifiablePage cPage = null; 526 527 String initialTitle; 528 if (page instanceof DefaultPage defaultPage) 529 { 530 initialTitle = defaultPage.getInitialTitle(); 531 } 532 else 533 { 534 initialTitle = page.getTitle(); 535 } 536 537 String newPageName = PageDAO.findFreePageNameByTitle(parent, initialTitle); 538 539 if (!keepReferences) 540 { 541 // Restrict the copy to the page and its current children to avoid infinitive loop 542 List<String> pagesToCopy = new ArrayList<>(); 543 pagesToCopy.add(page.getId()); 544 pagesToCopy.addAll(_getChildrenPageIds(page)); 545 546 // Copy and duplicate contents 547 cPage = (ModifiablePage) ((CopiableAmetysObject) page).copyTo(modifiableParent, newPageName, pagesToCopy); 548 _copySiteComponent.updateReferencesAfterCopy(page, cPage); 549 550 // Creates the first version on all copied contents 551 _updateContentsAfterCopy(page, cPage); 552 } 553 else 554 { 555 // Copy without duplicating contents (keep references) 556 557 if (!page.getSitemapName().equals(modifiableParent.getSitemapName())) 558 { 559 throw new IllegalArgumentException("The page '" + page.getId() + "' from sitemap '" + page.getSitemapName() + "' cannot be copied to a different sitemap '" + parent.getSitemapName() + "' while keeping references to contents"); 560 } 561 562 563 String pagePath = ((SimpleAmetysObject) parent).getNode().getPath() + "/" + newPageName; 564 Node node = ((JCRAmetysObject) page).getNode(); 565 node.getSession().getWorkspace().copy(node.getPath(), pagePath); 566 567 cPage = modifiableParent.getChild(newPageName); 568 } 569 570 cPage.setTitle(PageDAO.findFreeTitle(modifiableParent, initialTitle)); 571 if (cPage instanceof DefaultPage defaultPage) 572 { 573 defaultPage.setInitialTitle(initialTitle); 574 } 575 576 ((ModifiableTraversableAmetysObject) parent).saveChanges(); 577 result.put("id", cPage.getId()); 578 579 Map<String, Object> eventParams = new HashMap<>(); 580 eventParams.put(ObservationConstants.ARGS_PAGE, cPage); 581 _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_ADDED, _currentUserProvider.getUser(), eventParams)); 582 } 583 584 return result; 585 } 586 587 /** 588 * find a free name under a given parent node 589 * @param parent The parent 590 * @param initialTitle The title of the page 591 * @return The free name 592 */ 593 public static String findFreePageNameByTitle(TraversableAmetysObject parent, String initialTitle) 594 { 595 return findFreePageNameByTitle(parent, initialTitle, null); 596 } 597 598 /** 599 * find a free name under a given parent node 600 * @param parent The parent 601 * @param initialTitle The title of the page 602 * @param currentName The current name of the node, that can thus be authorized 603 * @return The free name 604 */ 605 public static String findFreePageNameByTitle(TraversableAmetysObject parent, String initialTitle, String currentName) 606 { 607 String originalPageName = NameHelper.filterName(initialTitle); 608 609 return findFreePageName(parent, originalPageName, currentName); 610 } 611 612 /** 613 * find a free name under a given parent node 614 * @param parent The parent 615 * @param originalPageName The name of the page 616 * @return The free name 617 */ 618 public static String findFreePageName(TraversableAmetysObject parent, String originalPageName) 619 { 620 return findFreePageName(parent, originalPageName, null); 621 } 622 623 /** 624 * find a free name under a given parent node 625 * @param parent The parent 626 * @param originalPageName The name of the page 627 * @param currentName The current name of the node, that can thus be authorized 628 * @return The free name 629 */ 630 public static String findFreePageName(TraversableAmetysObject parent, String originalPageName, String currentName) 631 { 632 int index = 2; 633 634 String pageName = originalPageName; 635 while (parent.hasChild(pageName) && !pageName.equals(currentName)) 636 { 637 // Find unused name on new parent node 638 pageName = originalPageName + "-" + index++; 639 } 640 return pageName; 641 } 642 643 /** 644 * find a free title under a given parent node 645 * @param parent The parent 646 * @param initialTitle The title of the page 647 * @return The free title 648 */ 649 public static String findFreeTitle(SitemapElement parent, String initialTitle) 650 { 651 List<String> existingTitles = parent.getChildrenPages().stream().map(Page::getTitle).toList(); 652 653 int index = 2; 654 655 String title = initialTitle; 656 while (existingTitles.contains(title)) 657 { 658 title = initialTitle + " (" + index++ + ")"; 659 } 660 661 return title; 662 } 663 664 /** 665 * Copy a page under another page 666 * @param targetId the page to copy in 667 * @param sourceId the page to copy 668 * @param keepReferences true to keep references for contents, or false to duplicate contents 669 * @return The id of created page is a result map. 670 * @throws RepositoryException if an error occurs 671 */ 672 @Callable (rights = "Web_Rights_Page_Create", rightContext = PageRightAssignmentContext.ID, paramIndex = 0) 673 @Deprecated 674 public Map<String, String> pastePage(String targetId, String sourceId, boolean keepReferences) throws RepositoryException 675 { 676 return copyPage(sourceId, targetId, keepReferences); 677 } 678 679 /** 680 * Move a page 681 * @param id The page id 682 * @param parentId The id of parent destination 683 * @param index The position in parent child nodes where page will be inserted. -1 means as the last child. 684 * @return the result map 685 */ 686 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 687 public Map<String, Object> movePage (String id, String parentId, int index) 688 { 689 Map<String, Object> result = new HashMap<>(); 690 691 String oldPathInSitemap = null; 692 String newPathInSitemap = null; 693 694 Page page = _resolver.resolveById(id); 695 SitemapElement srcParent = page.getParent(); 696 oldPathInSitemap = page.getPathInSitemap(); 697 Sitemap sitemap = page.getSitemap(); 698 699 // check rights of deletion on old parent page 700 if (!srcParent.getId().equals(parentId) && !_canDelete(page)) 701 { 702 throw new IllegalStateException("You do not have the rights to delete the page '/" + page.getSitemapName() + "/" + oldPathInSitemap + "'"); 703 } 704 705 if (!(page instanceof MoveablePage)) 706 { 707 throw new IllegalArgumentException("The page '/" + page.getSitemapName() + "/" + oldPathInSitemap + "' is not a moveable page"); 708 } 709 710 if (srcParent.getId().equals(parentId) && index != -1) 711 { 712 try 713 { 714 // in case of reorder, check right of creation on current parent page 715 if (!_canCreate(srcParent)) 716 { 717 throw new IllegalStateException("You do not have to reorder a page under '/" + srcParent.getSitemapName() + "/" + srcParent.getPathInSitemap() + "'"); 718 } 719 720 Page brother = srcParent.getChildPageAt(index); 721 ((MoveablePage) page).orderBefore(brother); 722 } 723 catch (UnknownAmetysObjectException e) 724 { 725 // Move the last child position 726 ((MoveablePage) page).orderBefore(null); 727 } 728 729 // Path is not modified 730 newPathInSitemap = oldPathInSitemap; 731 } 732 else 733 { 734 SitemapElement newParentPage = _resolver.resolveById(parentId); 735 736 if (page instanceof LockablePage lockablePage && lockablePage.isLocked()) 737 { 738 throw new IllegalStateException("You cannot move page '/" + lockablePage.getSitemapName() + "/" + lockablePage.getPathInSitemap() + "' because it is locked"); 739 } 740 741 // check right of creation on new parent page 742 if (!_canCreate(newParentPage)) 743 { 744 throw new IllegalStateException("You do not have the rights to create a page under '/" + newParentPage.getSitemapName() + "/" + newParentPage.getPathInSitemap() + "'"); 745 } 746 747 ((MoveablePage) page).moveTo(newParentPage, true); 748 if (index != -1) 749 { 750 Page brother = newParentPage.getChildPageAt(index); 751 752 ((MoveablePage) page).orderBefore(brother); 753 } 754 755 // Path is modified 756 newPathInSitemap = page.getPathInSitemap(); 757 } 758 759 if (sitemap.needsSave()) 760 { 761 sitemap.saveChanges(); 762 } 763 764 // Notify observers that the page has been moved 765 Map<String, Object> eventParams = new HashMap<>(); 766 eventParams.put(ObservationConstants.ARGS_SITEMAP, sitemap); 767 eventParams.put(ObservationConstants.ARGS_PAGE, page); 768 eventParams.put("page.old.path", oldPathInSitemap); 769 eventParams.put("page.old.parent", srcParent); 770 eventParams.put(ObservationConstants.ARGS_PAGE_PATH, newPathInSitemap); 771 _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_MOVED, _currentUserProvider.getUser(), eventParams)); 772 773 result.put("id", page.getId()); 774 result.put("parentId", page.getParent().getId()); 775 776 return result; 777 } 778 779 private boolean _canCreate(SitemapElement parentPage) 780 { 781 UserIdentity user = _currentUserProvider.getUser(); 782 if (_rightManager.hasRight(user, "Web_Rights_Page_Create", parentPage) == RightResult.RIGHT_ALLOW) 783 { 784 return true; 785 } 786 787 if (getLogger().isInfoEnabled()) 788 { 789 getLogger().info("The user '" + user + "' tried to create page under '/" + parentPage.getSitemapName() + "/" + parentPage.getPathInSitemap() + "' without sufficient rights"); 790 } 791 792 return false; 793 } 794 795 private boolean _canDelete(Page page) 796 { 797 UserIdentity user = _currentUserProvider.getUser(); 798 SitemapElement parent = page.getParent(); 799 if (_rightManager.hasRight(user, "Web_Rights_Page_Delete", parent) == RightResult.RIGHT_ALLOW) 800 { 801 return true; 802 } 803 804 if (getLogger().isInfoEnabled()) 805 { 806 getLogger().info("The user '" + user + "' tried to move page '/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "' without sufficient rights"); 807 } 808 809 return false; 810 } 811 812 /** 813 * Set pages as redirection 814 * @param pageIds the id of pages to modify 815 * @param url the url of redirection 816 * @param urlType the type of redirection 817 * @return the id of pages which succeeded or failed. 818 */ 819 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 820 public Map<String, Object> setLink (List<String> pageIds, String url, String urlType) 821 { 822 List<String> allRightIds = new ArrayList<>(); 823 List<Map<String, Object>> noRightPages = new ArrayList<>(); 824 List<Map<String, Object>> errorPages = new ArrayList<>(); 825 List<Map<String, Object>> noModifiablePages = new ArrayList<>(); 826 List<Map<String, Object>> lockedPages = new ArrayList<>(); 827 828 if (StringUtils.isEmpty(url)) 829 { 830 throw new IllegalArgumentException("Can not set page as a redirection with an empty url"); 831 } 832 833 for (String pageId : pageIds) 834 { 835 Page page = _resolver.resolveById(pageId); 836 837 try 838 { 839 if (_rightManager.currentUserHasRight("Web_Rights_Page_LinkPage", page) != RightResult.RIGHT_ALLOW) 840 { 841 noRightPages.add(Map.of("id", pageId, "title", page.getTitle())); 842 } 843 else if (!(page instanceof ModifiablePage)) 844 { 845 noModifiablePages.add(Map.of("id", pageId, "title", page.getTitle())); 846 } 847 else if (page instanceof LockablePage lockablePage && lockablePage.isLocked()) 848 { 849 lockedPages.add(Map.of("id", pageId, "title", page.getTitle())); 850 } 851 else 852 { 853 ModifiablePage mPage = (ModifiablePage) page; 854 855 if (page.getType().equals(PageType.CONTAINER)) 856 { 857 // Remove zones 858 for (ModifiableZone zone : mPage.getZones()) 859 { 860 zone.remove(); 861 } 862 } 863 864 if (pageId.equals(url)) 865 { 866 throw new IllegalArgumentException("A page can not redirect to itself"); 867 } 868 869 mPage.setType(PageType.LINK); 870 mPage.setURL(LinkType.valueOf(urlType), url); 871 mPage.getSitemap().saveChanges(); 872 873 allRightIds.add(pageId); 874 875 Map<String, Object> eventParams = new HashMap<>(); 876 eventParams.put(ObservationConstants.ARGS_PAGE, page); 877 _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_CHANGED, _currentUserProvider.getUser(), eventParams)); 878 } 879 } 880 catch (Exception e) 881 { 882 getLogger().error("Cannot set the page '" + pageId + "' as link [" + url + ", " + urlType.toString() + "]", e); 883 errorPages.add(Map.of("id", pageId, "title", page.getTitle())); 884 } 885 } 886 887 return Map.of("allright-pages", allRightIds, "noright-pages", noRightPages, "error-pages", errorPages, "nomodifiable-pages", noModifiablePages, "locked-pages", lockedPages); 888 } 889 890 /** 891 * Get available template for specified page 892 * @param pageId The page's id 893 * @return the list of available template as JSON 894 */ 895 private List<Map<String, Object>> _getAvailableTemplates (String pageId) 896 { 897 List<Map<String, Object>> templates = new ArrayList<>(); 898 899 Page page = _resolver.resolveById(pageId); 900 901 Set<String> availableTemplateIds = _templatesHandler.getAvailablesTemplates(page); 902 for (String templateName : availableTemplateIds) 903 { 904 String skinId = page.getSite().getSkinId(); 905 Skin skin = _skinsManager.getSkin(skinId); 906 907 SkinTemplate template = skin.getTemplate(templateName); 908 909 Map<String, Object> template2json = new HashMap<>(); 910 template2json.put("id", template.getId()); 911 template2json.put("label", template.getLabel()); 912 template2json.put("description", template.getDescription()); 913 template2json.put("iconSmall", template.getSmallImage()); 914 template2json.put("iconMedium", template.getMediumImage()); 915 template2json.put("iconLarge", template.getLargeImage()); 916 template2json.put("zone", template.getDefaultZoneId()); 917 918 templates.add(template2json); 919 } 920 921 return templates; 922 } 923 924 /** 925 * Get available content types for specified page 926 * @param pageId The page's id 927 * @param zoneName the name of the zone 928 * @return the list of available content types 929 */ 930 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 931 public List<Map<String, Object>> getAvailableContentTypes (String pageId, String zoneName) 932 { 933 List<Map<String, Object>> contenttypes = new ArrayList<>(); 934 935 Page page = _resolver.resolveById(pageId); 936 937 Set<String> contentTypeIds = _cTypeHandler.getAvailableContentTypes(page, zoneName); 938 for (String contentTypeId : contentTypeIds) 939 { 940 ContentType cType = _contentTypeExtensionPoint.getExtension(contentTypeId); 941 942 if (cType != null && _hasRight(cType, page)) 943 { 944 Map<String, Object> ctype2json = new HashMap<>(); 945 ctype2json.put("id", cType.getId()); 946 ctype2json.put("label", cType.getLabel()); 947 ctype2json.put("description", cType.getDescription()); 948 ctype2json.put("iconGlyph", cType.getIconGlyph()); 949 ctype2json.put("iconDecorator", cType.getIconDecorator()); 950 ctype2json.put("iconSmall", cType.getSmallIcon()); 951 ctype2json.put("iconMedium", cType.getMediumIcon()); 952 ctype2json.put("iconLarge", cType.getLargeIcon()); 953 ctype2json.put("defaultTitle", cType.getDefaultTitle()); 954 ctype2json.put("viewNames", cType.getViewNames(true)); 955 ctype2json.put("category", cType.getCategory().isI18n() ? cType.getCategory().getKey() : cType.getCategory().getLabel()); 956 ctype2json.put("categoryLabel", cType.getCategory()); 957 958 contenttypes.add(ctype2json); 959 } 960 } 961 962 return contenttypes; 963 } 964 965 /** 966 * Get available content types for a page being created 967 * @param pageId The page's id. Can be null of the page is not yet created 968 * @param zoneName the name of the zone 969 * @param parentId The id of parent page 970 * @param pageTitle The title of page to create 971 * @param template The template of page to create 972 * @return the list of available services 973 */ 974 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 975 public List<Map<String, Object>> getAvailableContentTypesForCreation(String pageId, String zoneName, String parentId, String pageTitle, String template) 976 { 977 if (StringUtils.isNotEmpty(pageId)) 978 { 979 // Get available services for a page 980 return getAvailableContentTypes(pageId, zoneName); 981 } 982 else if (StringUtils.isNotEmpty(parentId)) 983 { 984 // Get available services for a not yet existing page 985 SitemapElement parent = _resolver.resolveById(parentId); 986 987 // Create page temporarily 988 Page page = _createPage(parent, pageTitle, template); 989 990 List<Map<String, Object>> availableContentTypes = getAvailableContentTypes(page.getId(), zoneName); 991 992 // Cancel page creation 993 page.getSitemap().revertChanges(); 994 995 return availableContentTypes; 996 } 997 998 return Collections.EMPTY_LIST; 999 } 1000 1001 private boolean _hasRight(ContentType contentType, Page page) 1002 { 1003 String right = contentType.getRight(); 1004 1005 if (right == null) 1006 { 1007 return true; 1008 } 1009 else 1010 { 1011 UserIdentity user = _currentUserProvider.getUser(); 1012 return _rightManager.hasRight(user, right, page) == RightResult.RIGHT_ALLOW; 1013 } 1014 } 1015 1016 /** 1017 * Get available services for specified page 1018 * @param pageId The page's id 1019 * @param zoneName the name of the zone 1020 * @return the list of available services 1021 */ 1022 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 1023 public List<Map<String, Object>> getAvailableServices (String pageId, String zoneName) 1024 { 1025 List<Map<String, Object>> services = new ArrayList<>(); 1026 1027 SitemapElement sitemapElement = _resolver.resolveById(pageId); 1028 1029 Set<String> serviceIds = _serviceHandler.getAvailableServices(sitemapElement, zoneName); 1030 for (String serviceId : serviceIds) 1031 { 1032 Service service = _serviceExtensionPoint.getExtension(serviceId); 1033 if (service != null && _hasRight(service, sitemapElement)) 1034 { 1035 Map<String, Object> serviceMap = new HashMap<>(); 1036 serviceMap.put("id", service.getId()); 1037 serviceMap.put("label", service.getLabel()); 1038 serviceMap.put("description", service.getDescription()); 1039 serviceMap.put("iconGlyph", service.getIconGlyph()); 1040 serviceMap.put("iconDecorator", service.getIconDecorator()); 1041 serviceMap.put("iconSmall", service.getSmallIcon()); 1042 serviceMap.put("iconMedium", service.getMediumIcon()); 1043 serviceMap.put("iconLarge", service.getLargeIcon()); 1044 serviceMap.put("parametersAction", service.getParametersScript().getScriptClassname()); 1045 serviceMap.put("category", service.getCategory().isI18n() ? service.getCategory().getKey() : service.getCategory().getLabel()); 1046 serviceMap.put("categoryLabel", service.getCategory()); 1047 1048 services.add(serviceMap); 1049 } 1050 } 1051 1052 return services; 1053 } 1054 1055 /** 1056 * Get available services for a page being created 1057 * @param pageId The page's id. Can be null of the page is not yet created 1058 * @param zoneName the name of the zone 1059 * @param parentId The id of parent page 1060 * @param pageTitle The title of page to create 1061 * @param template The template of page to create 1062 * @return the list of available services 1063 */ 1064 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 1065 public List<Map<String, Object>> getAvailableServicesForCreation(String pageId, String zoneName, String parentId, String pageTitle, String template) 1066 { 1067 if (StringUtils.isNotEmpty(pageId)) 1068 { 1069 // Get available services for a page 1070 return getAvailableServices(pageId, zoneName); 1071 } 1072 else if (StringUtils.isNotEmpty(parentId)) 1073 { 1074 // Get available services for a not yet existing page 1075 SitemapElement parent = _resolver.resolveById(parentId); 1076 1077 // Create page temporarily 1078 Page page = _createPage(parent, pageTitle, template); 1079 1080 List<Map<String, Object>> availableServices = getAvailableServices(page.getId(), zoneName); 1081 1082 // Cancel page creation 1083 page.getSitemap().revertChanges(); 1084 1085 return availableServices; 1086 } 1087 1088 return Collections.EMPTY_LIST; 1089 } 1090 1091 private Page _createPage (SitemapElement parent, String pageTitle, String template) 1092 { 1093 Site site = parent.getSite(); 1094 1095 String pageName = PageDAO.findFreePageNameByTitle(parent, pageTitle); 1096 1097 ModifiablePage page = ((ModifiableTraversableAmetysObject) parent).createChild(pageName, "ametys:defaultPage"); 1098 1099 page.setTitle(pageTitle); 1100 page.setType(PageType.NODE); 1101 page.setSiteName(site.getName()); 1102 page.setSitemapName(page.getSitemap().getName()); 1103 1104 if (template != null) 1105 { 1106 String skinId = page.getSite().getSkinId(); 1107 SkinTemplate tpl = _skinsManager.getSkin(skinId).getTemplate(template); 1108 if (tpl == null) 1109 { 1110 throw new IllegalStateException("Template '" + template + "' does not exist on skin '" + skinId + "'"); 1111 } 1112 1113 // Set temporary the template to get available services 1114 page.setType(PageType.CONTAINER); 1115 page.setTemplate(template); 1116 } 1117 1118 return page; 1119 } 1120 1121 private boolean _hasRight(Service service, SitemapElement sitemapElement) 1122 { 1123 String right = service.getRight(); 1124 1125 if (right == null) 1126 { 1127 return true; 1128 } 1129 else 1130 { 1131 UserIdentity user = _currentUserProvider.getUser(); 1132 return _rightManager.hasRight(user, right, sitemapElement) == RightResult.RIGHT_ALLOW; 1133 } 1134 } 1135 1136 /** 1137 * Get available content types for a page being created 1138 * @param pageId The page's id. Can be null of the page is not yet created 1139 * @param parentId The id of parent page 1140 * @param pageTitle The title of page to create 1141 * @return the list of available services 1142 */ 1143 @Callable (rights = "Web_Rights_Page_Create", rightContext = PageRightAssignmentContext.ID, paramIndex = 1) 1144 public List<Map<String, Object>> getAvailableTemplatesForCreation (String pageId, String parentId, String pageTitle) 1145 { 1146 if (StringUtils.isNotEmpty(pageId)) 1147 { 1148 // Get available template for a page 1149 return _getAvailableTemplates(pageId); 1150 } 1151 else if (StringUtils.isNotEmpty(parentId)) 1152 { 1153 // Get available template for a not yet existing page 1154 SitemapElement parent = _resolver.resolveById(parentId); 1155 1156 // Create page temporarily 1157 Page page = _createPage(parent, pageTitle, null); 1158 1159 List<Map<String, Object>> availableTemplates = _getAvailableTemplates(page.getId()); 1160 1161 // Cancel page creation 1162 page.getSitemap().revertChanges(); 1163 1164 return availableTemplates; 1165 } 1166 1167 return Collections.EMPTY_LIST; 1168 } 1169 1170 /** 1171 * Get service info 1172 * @param pageId Optional, the page id of the service. To get some basic info about the page. 1173 * @param serviceId The id of the service 1174 * @return a Map containing some info about the service (label, url..) 1175 */ 1176 @Callable (rights = Callable.READ_ACCESS, rightContext = PageRightAssignmentContext.ID, paramIndex = 0) 1177 public Map<String, Object> getServiceInfo(String pageId, String serviceId) 1178 { 1179 Map<String, Object> info = new HashMap<>(); 1180 1181 if (StringUtils.isNotEmpty(pageId)) 1182 { 1183 SitemapElement sitemapElement = _resolver.resolveById(pageId); 1184 info.put("page-id", sitemapElement.getId()); 1185 info.put("page-title", sitemapElement.getTitle()); 1186 } 1187 1188 Service service = _serviceExtensionPoint.getExtension(serviceId); 1189 info.put("id", service.getId()); 1190 info.put("label", service.getLabel()); 1191 info.put("url", service.getURL()); 1192 info.put("smallIcon", service.getSmallIcon()); 1193 info.put("iconGlyph", service.getIconGlyph()); 1194 info.put("iconDecorator", service.getIconDecorator()); 1195 return info; 1196 } 1197 1198 /** 1199 * Rename a page 1200 * @param pageId The id of page to rename 1201 * @param title The page's title 1202 * @param longTitle The page's long title. 1203 * @param updatePath true to update page's path 1204 * @param createAlias true to create a alias 1205 * @return the result map 1206 */ 1207 @Callable (rights = "Web_Rights_Page_Rename", rightContext = PageRightAssignmentContext.ID, paramIndex = 0) 1208 public Map<String, Object> renamePage (String pageId, String title, String longTitle, boolean updatePath, boolean createAlias) 1209 { 1210 Map<String, Object> result = new HashMap<>(); 1211 1212 Page page = _resolver.resolveById(pageId); 1213 1214 if (!(page instanceof ModifiablePage)) 1215 { 1216 throw new IllegalArgumentException("Can not rename a non-modifiable page '/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "'"); 1217 } 1218 1219 if (page instanceof LockablePage lockablePage && lockablePage.isLocked()) 1220 { 1221 throw new IllegalArgumentException("Can not rename a locked page '/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "'"); 1222 } 1223 1224 ModifiablePage mPage = (ModifiablePage) page; 1225 mPage.setTitle(title); 1226 mPage.setLongTitle(longTitle); 1227 1228 if (updatePath) 1229 { 1230 String oldPathInSitemap = page.getPathInSitemap(); 1231 String oldPath = "/" + page.getSitemapName() + "/" + page.getPathInSitemap() + ".html"; 1232 String oldPathForChild = "/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "/**.html"; 1233 1234 String pageName = ""; 1235 try 1236 { 1237 pageName = PageDAO.findFreePageNameByTitle(mPage.getParent(), title, page.getName()); 1238 } 1239 catch (IllegalArgumentException e) 1240 { 1241 result.put("invalid-name", title); 1242 return result; 1243 } 1244 1245 if (!page.getName().equals(pageName)) 1246 { 1247 mPage.rename(pageName); 1248 1249 if (createAlias) 1250 { 1251 ModifiableTraversableAmetysObject rootNode = AliasHelper.getRootNode(page.getSite()); 1252 1253 DefaultAlias alias = rootNode.createChild(AliasHelper.getAliasNextUniqueName(rootNode), "ametys:alias"); 1254 alias.setUrl(oldPath); 1255 alias.setTarget(page.getId()); 1256 alias.setType(TargetType.PAGE); 1257 alias.setCreationDate(new Date()); 1258 1259 // Alias for child pages 1260 alias = rootNode.createChild(AliasHelper.getAliasNextUniqueName(rootNode), "ametys:alias"); 1261 alias.setUrl(oldPathForChild); 1262 alias.setTarget("/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "/{1}.html"); 1263 alias.setType(TargetType.URL); 1264 alias.setCreationDate(new Date()); 1265 1266 rootNode.saveChanges(); 1267 } 1268 1269 // Notify observers that the page has been renamed 1270 Map<String, Object> eventParams = new HashMap<>(); 1271 eventParams.put(ObservationConstants.ARGS_PAGE, page); 1272 eventParams.put("path.old.path", oldPathInSitemap); 1273 eventParams.put(ObservationConstants.ARGS_PAGE_PATH, page.getPathInSitemap()); 1274 _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_RENAMED, _currentUserProvider.getUser(), eventParams)); 1275 1276 } 1277 else 1278 { 1279 // Notify observers that the page's title has been modified 1280 Map<String, Object> eventParams = new HashMap<>(); 1281 eventParams.put(ObservationConstants.ARGS_PAGE, page); 1282 _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_UPDATED, _currentUserProvider.getUser(), eventParams)); 1283 } 1284 1285 } 1286 else 1287 { 1288 // Notify observers that the page's title has been modified 1289 Map<String, Object> eventParams = new HashMap<>(); 1290 eventParams.put(ObservationConstants.ARGS_PAGE, page); 1291 _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_UPDATED, _currentUserProvider.getUser(), eventParams)); 1292 } 1293 1294 Sitemap sitemap = page.getSitemap(); 1295 if (sitemap.needsSave()) 1296 { 1297 sitemap.saveChanges(); 1298 } 1299 1300 result.put("id", page.getId()); 1301 result.put("path", page.getPath()); 1302 result.put("title", page.getTitle()); 1303 1304 return result; 1305 } 1306 1307 /** 1308 * Unlock or lock page 1309 * @param pageId the id of the page to unlock or lock 1310 * @param mode the mode ('lock' or 'unlock') 1311 * @return the result JSON map 1312 */ 1313 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 1314 public Map<String, Object> unlockOrLock(String pageId, String mode) 1315 { 1316 Map<String, Object> result = new HashMap<>(); 1317 1318 UserIdentity currentUser = _currentUserProvider.getUser(); 1319 LockablePage lockablePage = (LockablePage) _resolver.resolveById(pageId); 1320 boolean canChangeLockStatus = _rightManager.hasRight(currentUser, "Web_Rights_Page_Lock", "/cms") == RightResult.RIGHT_ALLOW && _rightManager.currentUserHasReadAccess(lockablePage); 1321 1322 boolean unlock = "unlock".equals(mode); 1323 1324 if (!canChangeLockStatus) 1325 { 1326 getLogger().warn("The user '" + currentUser + "' tried to unlock or lock page '" + pageId + "' but does not have the right"); 1327 result.put("error", unlock ? "still-locked-page" : "fail-locked-page"); 1328 } 1329 1330 if (unlock && !lockablePage.isLocked()) 1331 { 1332 getLogger().warn("The page '" + pageId + "' is not locked anymore."); 1333 } 1334 else if (!unlock && lockablePage.isLocked()) 1335 { 1336 getLogger().warn("The page '" + pageId + "' is already locked."); 1337 result.put("error", "already-locked-page"); 1338 } 1339 else 1340 { 1341 try 1342 { 1343 if (unlock) 1344 { 1345 lockablePage.unlock(); 1346 if (getLogger().isDebugEnabled()) 1347 { 1348 getLogger().debug("The user has unlocked page '" + pageId + "'"); 1349 } 1350 } 1351 else 1352 { 1353 lockablePage.lock(); 1354 } 1355 1356 lockablePage.saveChanges(); 1357 1358 } 1359 catch (AmetysRepositoryException e) 1360 { 1361 getLogger().error("Unable to lock or unlock page '" + pageId + "'", e); 1362 result.put("error", unlock ? "fail-unlocked-page" : "fail-locked-page"); 1363 } 1364 } 1365 1366 result.put("pageId", pageId); 1367 result.put("pageTitle", lockablePage.getTitle()); 1368 result.put("mode", mode); 1369 return result; 1370 } 1371 1372 /** 1373 * Delete a page and its sub-pages. 1374 * The contents that belong only to the deleted page will be deleted if 'deleteBelongingContents' is set to true. 1375 * The newly created contents are deleted whatever the value if 'deleteBelongingContents'. 1376 * @param pageId the id of page to delete 1377 * @param deleteBelongingContents true to delete the contents that belong to the page and its sub-pages only 1378 * @return The id of deleted pages 1379 */ 1380 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 1381 public Map<String, Object> deletePage(String pageId, boolean deleteBelongingContents) 1382 { 1383 ModifiablePage page = _resolver.resolveById(pageId); 1384 1385 // Check rights on parent 1386 if (!_canDelete(page)) 1387 { 1388 throw new IllegalStateException("You do not have the rights to delete the page '/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "'"); 1389 } 1390 1391 if (page instanceof LockablePage lockablePage && lockablePage.isLocked()) 1392 { 1393 throw new IllegalStateException("Can not delete the locked page '/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "'"); 1394 } 1395 1396 return deletePage(page, deleteBelongingContents); 1397 } 1398 1399 /** 1400 * Delete a page and its sub-pages. 1401 * The contents that belong only to the deleted page will be deleted if 'deleteBelongingContents' is set to true. 1402 * The newly created contents are deleted whatever the value if 'deleteBelongingContents'. 1403 * @param page the page to delete 1404 * @param deleteBelongingContents true to delete the contents that belong to the page and its sub-pages only 1405 * @return The id of deleted pages 1406 */ 1407 public Map<String, Object> deletePage(ModifiablePage page, boolean deleteBelongingContents) 1408 { 1409 Map<String, Object> result = new HashMap<>(); 1410 1411 List<String> contentToDelete = getDeleteablePageContentIds(page.getId(), !deleteBelongingContents); 1412 1413 Sitemap sitemap = page.getSitemap(); 1414 SitemapElement parent = page.getParent(); 1415 String pagePathInSitemap = page.getPathInSitemap(); 1416 1417 List<String> childPagesIds = _getChildrenPageIds(page); 1418 1419 Map<String, Object> eventParams = new HashMap<>(); 1420 eventParams.put(ObservationConstants.ARGS_PAGE_ID, page.getId()); 1421 eventParams.put(ObservationConstants.ARGS_PAGE_PARENT, parent); 1422 eventParams.put(ObservationConstants.ARGS_PAGE_PATH, pagePathInSitemap); 1423 eventParams.put(ObservationConstants.ARGS_SITEMAP, sitemap); 1424 eventParams.put(ObservationConstants.ARGS_PAGE_CONTENTS, getPageContents(page)); 1425 _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_DELETING, _currentUserProvider.getUser(), eventParams)); 1426 1427 // FIXME API test if this is not modifiable 1428 page.getParent(); 1429 page.remove(); 1430 ((ModifiableAmetysObject) parent).saveChanges(); 1431 1432 _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_DELETED, _currentUserProvider.getUser(), eventParams)); 1433 1434 result.put("id", page.getId()); 1435 result.put("childPages", childPagesIds); 1436 1437 result.putAll(_contentDAO.deleteContents(contentToDelete, true)); 1438 return result; 1439 } 1440 1441 /** 1442 * Set pages as blank page 1443 * @param pageIds the id of pages to modify 1444 * @return the id of pages which succeeded or failed. 1445 */ 1446 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 1447 public Map<String, Object> setBlank (List<String> pageIds) 1448 { 1449 List<String> allRightIds = new ArrayList<>(); 1450 List<Map<String, Object>> noRightPages = new ArrayList<>(); 1451 List<Map<String, Object>> errorPages = new ArrayList<>(); 1452 List<Map<String, Object>> noModifiablePages = new ArrayList<>(); 1453 List<Map<String, Object>> lockedPages = new ArrayList<>(); 1454 1455 for (String pageId : pageIds) 1456 { 1457 Page page = _resolver.resolveById(pageId); 1458 1459 try 1460 { 1461 if (_rightManager.currentUserHasRight("Web_Rights_Page_BlankPage", page) != RightResult.RIGHT_ALLOW) 1462 { 1463 noRightPages.add(Map.of("id", pageId, "title", page.getTitle())); 1464 } 1465 else if (!(page instanceof ModifiablePage)) 1466 { 1467 noModifiablePages.add(Map.of("id", pageId, "title", page.getTitle())); 1468 } 1469 else if (page instanceof LockablePage lockablePage && lockablePage.isLocked()) 1470 { 1471 lockedPages.add(Map.of("id", pageId, "title", page.getTitle())); 1472 } 1473 else 1474 { 1475 ModifiablePage mPage = (ModifiablePage) page; 1476 1477 if (page.getType().equals(PageType.CONTAINER)) 1478 { 1479 // Remove zones 1480 for (ModifiableZone zone : mPage.getZones()) 1481 { 1482 zone.remove(); 1483 } 1484 } 1485 1486 mPage.setType(PageType.NODE); 1487 mPage.getSitemap().saveChanges(); 1488 1489 allRightIds.add(pageId); 1490 1491 Map<String, Object> eventParams = new HashMap<>(); 1492 eventParams.put(ObservationConstants.ARGS_PAGE, page); 1493 _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_CHANGED, _currentUserProvider.getUser(), eventParams)); 1494 } 1495 } 1496 catch (Exception e) 1497 { 1498 getLogger().error("Cannot set the page '" + pageId + "' as blank page", e); 1499 errorPages.add(Map.of("id", pageId, "title", page.getTitle())); 1500 } 1501 } 1502 1503 return Map.of("allright-pages", allRightIds, "noright-pages", noRightPages, "error-pages", errorPages, "nomodifiable-pages", noModifiablePages, "locked-pages", lockedPages); 1504 } 1505 1506 /** 1507 * Set a template to pages 1508 * @param pageIds the id of pages to update 1509 * @param templateName The template name 1510 * @return the id of pages which succeeded 1511 */ 1512 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 1513 public Map<String, Object> setTemplate (List<String> pageIds, String templateName) 1514 { 1515 return setTemplate(pageIds, templateName, true); 1516 } 1517 1518 /** 1519 * Set a template to pages 1520 * @param pageIds the id of pages to update 1521 * @param templateName The template name 1522 * @param checkAvailableTemplate true if you want to check available template 1523 * @return the id of pages which succeeded 1524 */ 1525 public Map<String, Object> setTemplate (List<String> pageIds, String templateName, boolean checkAvailableTemplate) 1526 { 1527 List<String> allRightIds = new ArrayList<>(); 1528 List<Map<String, Object>> noRightPages = new ArrayList<>(); 1529 List<Map<String, Object>> errorPages = new ArrayList<>(); 1530 List<Map<String, Object>> noModifiablePages = new ArrayList<>(); 1531 1532 String defaultZoneName = null; 1533 1534 for (String pageId : pageIds) 1535 { 1536 Page page = _resolver.resolveById(pageId); 1537 1538 if (_rightManager.currentUserHasRight("Web_Rights_Page_Templates", page) != RightResult.RIGHT_ALLOW) 1539 { 1540 noRightPages.add(Map.of("id", pageId, "title", page.getTitle())); 1541 } 1542 else if (!(page instanceof ModifiablePage)) 1543 { 1544 noModifiablePages.add(Map.of("id", pageId, "title", page.getTitle())); 1545 } 1546 else if (page instanceof LockablePage lockablePage && lockablePage.isLocked()) 1547 { 1548 throw new IllegalStateException("You cannot set template on page '/" + lockablePage.getSitemapName() + "/" + lockablePage.getPathInSitemap() + "' because it's locked"); 1549 } 1550 else 1551 { 1552 ModifiablePage mPage = (ModifiablePage) page; 1553 1554 if (defaultZoneName == null) 1555 { 1556 String skinId = page.getSite().getSkinId(); 1557 SkinTemplate tpl = _skinsManager.getSkin(skinId).getTemplate(templateName); 1558 if (tpl == null) 1559 { 1560 throw new IllegalStateException("Template '" + templateName + "' does not exist on skin '" + skinId + "'"); 1561 } 1562 1563 defaultZoneName = tpl.getDefaultZoneId(); 1564 } 1565 1566 if (checkAvailableTemplate && !_templatesHandler.getAvailablesTemplates(mPage).contains(templateName)) 1567 { 1568 throw new IllegalStateException("Template '" + templateName + "' is not available for page '" + pageId + "'"); 1569 } 1570 1571 if (page.getType().equals(PageType.CONTAINER)) 1572 { 1573 _removeOldZones (mPage, templateName); 1574 } 1575 1576 mPage.setTemplate(templateName); 1577 mPage.setType(PageType.CONTAINER); 1578 mPage.getSitemap().saveChanges(); 1579 1580 allRightIds.add(pageId); 1581 1582 Map<String, Object> eventParams = new HashMap<>(); 1583 eventParams.put(ObservationConstants.ARGS_PAGE, page); 1584 _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_CHANGED, _currentUserProvider.getUser(), eventParams)); 1585 } 1586 1587 } 1588 1589 Map<String, Object> results = new HashMap<>(); 1590 results.put("allright-pages", allRightIds); 1591 results.put("noright-pages", noRightPages); 1592 results.put("error-pages", errorPages); 1593 results.put("nomodifiable-pages", noModifiablePages); 1594 1595 if (defaultZoneName != null) 1596 { 1597 results.put("zonename", defaultZoneName); 1598 } 1599 return results; 1600 } 1601 1602 /** 1603 * Get the script class name to execute to add or update this service 1604 * @param serviceId the service id 1605 * @return the script class name. Can be empty 1606 */ 1607 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 1608 public String getServiceParametersAction (String serviceId) 1609 { 1610 try 1611 { 1612 Service service = _serviceExtensionPoint.getExtension(serviceId); 1613 return service.getParametersScript().getScriptClassname(); 1614 } 1615 catch (IllegalArgumentException e) 1616 { 1617 throw new IllegalArgumentException("Service with id '" + serviceId + "' does not exist", e); 1618 } 1619 } 1620 1621 private void _removeOldZones (ModifiablePage page, String templateName) 1622 { 1623 String skinId = page.getSite().getSkinId(); 1624 1625 SkinTemplate oldTemplate = _skinsManager.getSkin(skinId).getTemplate(templateName); 1626 1627 Map<String, SkinTemplateZone> templateZones = oldTemplate.getZones(); 1628 1629 for (ModifiableZone zone : page.getZones()) 1630 { 1631 if (!templateZones.containsKey(zone.getName())) 1632 { 1633 zone.remove(); 1634 } 1635 } 1636 } 1637 1638 /** 1639 * Get the tags from the pages 1640 * @param pageIds The ids of the pages 1641 * @return the tags of the pages 1642 */ 1643 @Callable(rights = Callable.SKIP_BUILTIN_CHECK) 1644 public Set<String> getTags (List<String> pageIds) 1645 { 1646 Set<String> tags = new HashSet<>(); 1647 1648 for (String pageId : pageIds) 1649 { 1650 Page page = _resolver.resolveById(pageId); 1651 if (_rightManager.currentUserHasReadAccess(page)) 1652 { 1653 tags.addAll(page.getTags()); 1654 } 1655 } 1656 1657 return tags; 1658 } 1659 1660 /** 1661 * Tag a list of pages 1662 * @param pageIds The ids of pages to tag 1663 * @param tagNames The tags 1664 * @param mode The mode for updating tags: 'REPLACE' to replace tags, 'INSERT' to add tags or 'REMOVE' to remove tags. 1665 * @param contextualParameters Contextual parameters. Must contain the site name 1666 * @return the result 1667 */ 1668 @Callable(rights = Callable.SKIP_BUILTIN_CHECK) 1669 public Map<String, Object> tag (List<String> pageIds, List<String> tagNames, String mode, Map<String, Object> contextualParameters) 1670 { 1671 return tag(pageIds, tagNames, TagMode.valueOf(mode), contextualParameters, false); 1672 } 1673 1674 /** 1675 * Tag a list of pages 1676 * @param pageIds The ids of pages to tag 1677 * @param tagNames The tags 1678 * @param mode The mode for updating tags: 'REPLACE' to replace tags, 'INSERT' to add tags or 'REMOVE' to remove tags. 1679 * @param contextualParameters Contextual parameters. Must contain the site name 1680 * @param ignoreRights <code>true</code> to ignore rights on tag 1681 * @return the result 1682 */ 1683 public Map<String, Object> tag (List<String> pageIds, List<String> tagNames, TagMode mode, Map<String, Object> contextualParameters, boolean ignoreRights) 1684 { 1685 Map<String, Object> result = new HashMap<>(); 1686 1687 result.put("nomodifiable-pages", new ArrayList<>()); 1688 result.put("invalid-tags", new ArrayList<>()); 1689 result.put("allright-pages", new ArrayList<>()); 1690 result.put("noright-pages", new ArrayList<>()); 1691 1692 for (String pageId : pageIds) 1693 { 1694 Page page = _resolver.resolveById(pageId); 1695 1696 Map<String, Object> page2json = new HashMap<>(); 1697 page2json.put("id", page.getId()); 1698 page2json.put("title", page.getTitle()); 1699 1700 if (!ignoreRights && !_hasTagRights(page, tagNames, mode, contextualParameters)) 1701 { 1702 @SuppressWarnings("unchecked") 1703 List<Map<String, Object>> noRightPages = (List<Map<String, Object>>) result.get("noright-pages"); 1704 noRightPages.add(page2json); 1705 } 1706 else 1707 { 1708 if (page instanceof ModifiablePage mPage) 1709 { 1710 @SuppressWarnings("unchecked") 1711 List<String> invalidTags = (List<String>) result.get("invalid-tags"); 1712 invalidTags.addAll(tag(mPage, tagNames, mode)); 1713 1714 page2json.put("tags", page.getTags()); 1715 @SuppressWarnings("unchecked") 1716 List<Map<String, Object>> allRightPages = (List<Map<String, Object>>) result.get("allright-pages"); 1717 allRightPages.add(page2json); 1718 } 1719 else 1720 { 1721 @SuppressWarnings("unchecked") 1722 List<Map<String, Object>> nomodifiablePages = (List<Map<String, Object>>) result.get("nomodifiable-pages"); 1723 nomodifiablePages.add(page2json); 1724 } 1725 } 1726 } 1727 1728 return result; 1729 } 1730 1731 private boolean _hasTagRights(Page page, List<String> tagNames, TagMode mode, Map<String, Object> contextualParameters) 1732 { 1733 List<CMSTag> tags = tagNames.stream() 1734 .map(t -> _tagProvider.getTag(t, contextualParameters)) 1735 .toList(); 1736 1737 // In case of replace, only check the right on tag that are modified, ie added or removed tags 1738 if (TagMode.REPLACE.equals(mode)) 1739 { 1740 List<CMSTag> existingTags = page.getTags().stream() 1741 .map(t -> _tagProvider.getTag(t, contextualParameters)) 1742 .toList(); 1743 1744 tags = new ArrayList<>(CollectionUtils.disjunction(tags, existingTags)); 1745 } 1746 1747 boolean hasPublicTagRight = _rightManager.currentUserHasRight("Web_Rights_Page_Tag", page) == RightResult.RIGHT_ALLOW; 1748 boolean hasPrivateTagRight = _rightManager.currentUserHasRight("Web_Rights_Page_Private_Tag", page) == RightResult.RIGHT_ALLOW; 1749 1750 // Test if the current user has the right to tag public tag on page only if there are at least one public tag 1751 boolean hasRight = TagHelper.filterTags(tags, TagVisibility.PUBLIC, "PAGE").isEmpty() || hasPublicTagRight || hasPrivateTagRight; 1752 1753 // Test if the current user has the right to tag private tag on page only if there are at least one private tag 1754 return hasRight && (TagHelper.filterTags(tags, TagVisibility.PRIVATE, "PAGE").isEmpty() || hasPrivateTagRight); 1755 } 1756 1757 /** 1758 * Tag a page 1759 * @param page The page to tag 1760 * @param tagNames The tags 1761 * @param tagMode The mode for updating tags: 'REPLACE' to replace tags, 'INSERT' to add tags or 'REMOVE' to remove tags. 1762 * @return the list of invalid tags that were not set 1763 */ 1764 public List<String> tag(ModifiablePage page, List<String> tagNames, TagMode tagMode) 1765 { 1766 List<String> invalidTags = new ArrayList<>(); 1767 1768 Set<String> oldTags = page.getTags(); 1769 if (TagMode.REPLACE.equals(tagMode)) 1770 { 1771 // First delete old tags 1772 for (String tagName : oldTags) 1773 { 1774 page.untag(tagName); 1775 } 1776 } 1777 1778 // Then set new tags 1779 for (String tagName : tagNames) 1780 { 1781 if (_isTagValid(page, tagName)) 1782 { 1783 if (TagMode.REMOVE.equals(tagMode)) 1784 { 1785 page.untag(tagName); 1786 } 1787 else if (TagMode.REPLACE.equals(tagMode) || !oldTags.contains(tagName)) 1788 { 1789 page.tag(tagName); 1790 } 1791 } 1792 else 1793 { 1794 invalidTags.add(tagName); 1795 } 1796 } 1797 1798 page.saveChanges(); 1799 1800 if (!oldTags.equals(page.getTags())) 1801 { 1802 // Notify observers that the page has been tagged 1803 Map<String, Object> eventParams = new HashMap<>(); 1804 eventParams.put(ObservationConstants.ARGS_PAGE, page); 1805 eventParams.put(ObservationConstants.ARGS_PAGE_TAGS, page.getTags()); 1806 eventParams.put(ObservationConstants.ARGS_PAGE_OLD_TAGS, oldTags); 1807 _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_UPDATED, _currentUserProvider.getUser(), eventParams)); 1808 } 1809 1810 return invalidTags; 1811 } 1812 1813 /** 1814 * Tag a list of contents with the given tags 1815 * @param pageIds The ids of pages to tag 1816 * @param contentIds The ids of contents to tag 1817 * @param tagNames The tags 1818 * @param contextualParameters The contextual parameters 1819 * @return the result map 1820 */ 1821 @Callable(rights = Callable.SKIP_BUILTIN_CHECK) 1822 public Map<String, Object> tag (List<String> pageIds, List<String> contentIds, List<String> tagNames, Map<String, Object> contextualParameters) 1823 { 1824 return tag(pageIds, contentIds, tagNames, TagMode.REPLACE.toString(), contextualParameters); 1825 } 1826 1827 /** 1828 * Tag a list of contents and/org pages 1829 * @param pageIds The ids of pages to tag 1830 * @param contentIds The ids of contents to tag 1831 * @param tagNames The tags 1832 * @param mode The mode for updating tags: 'REPLACE' to replace tags, 'INSERT' to add tags or 'REMOVE' to remove tags. 1833 * @param contextualParameters The contextual parameters 1834 * @return the result 1835 */ 1836 @Callable(rights = Callable.SKIP_BUILTIN_CHECK) 1837 public Map<String, Object> tag (List<String> pageIds, List<String> contentIds, List<String> tagNames, String mode, Map<String, Object> contextualParameters) 1838 { 1839 TagMode tagMode = TagMode.valueOf(mode); 1840 1841 // Tag pages 1842 Map<String, Object> result = tag(pageIds, tagNames, tagMode, contextualParameters, false); 1843 1844 // Tag contents 1845 result.putAll(_contentDAO.tag(contentIds, tagNames, tagMode, contextualParameters, false)); 1846 1847 // Invalid tags are ignored 1848 result.remove("invalid-tags"); 1849 return result; 1850 } 1851 1852 /** 1853 * Test if a tag is valid for a specific page 1854 * @param page The page 1855 * @param tagName The tag name 1856 * @return True if the tag is valid 1857 */ 1858 public boolean _isTagValid (Page page, String tagName) 1859 { 1860 Map<String, Object> params = new HashMap<>(); 1861 params.put("siteName", page.getSiteName()); 1862 CMSTag tag = _tagProvider.getTag(tagName, params); 1863 1864 return tag.getTarget().getName().equals("PAGE"); 1865 } 1866 1867 /** 1868 * Returns the ids of the pages matching the tag 1869 * @param sitename The site id 1870 * @param lang The language code 1871 * @param tag The tag id 1872 * @return Array of pages ids 1873 */ 1874 public List<String> findPagedIdsByTag(String sitename, String lang, String tag) 1875 { 1876 return _getMemoryPageTagCache().get(PageTagCacheKey.of(sitename, lang, tag), __ -> _computePagesIds(sitename, lang, tag)) 1877 .stream() 1878 .filter(_resolver::hasAmetysObjectForId) // Cache remember for tag in default, we have to check if page exists in current workspace 1879 .collect(Collectors.toList()); 1880 } 1881 1882 private List<String> _computePagesIds(String sitename, String lang, String tag) 1883 { 1884 1885 Session defaultSession = null; 1886 try 1887 { 1888 List<String> pagesIds = new ArrayList<>(); 1889 1890 // Force default workspace to execute query 1891 defaultSession = _repository.login(RepositoryConstants.DEFAULT_WORKSPACE); 1892 1893 String xpath = PageQueryHelper.getPageXPathQuery(sitename, lang, null, new TagExpression(Operator.EQ, tag), null); 1894 AmetysObjectIterable<Page> pages = _ametysObjectResolver.query(xpath, defaultSession); 1895 Iterator<Page> it = pages.iterator(); 1896 1897 while (it.hasNext()) 1898 { 1899 pagesIds.add(it.next().getId()); 1900 } 1901 1902 return pagesIds; 1903 } 1904 catch (RepositoryException e) 1905 { 1906 throw new AmetysRepositoryException(e); 1907 } 1908 finally 1909 { 1910 if (defaultSession != null) 1911 { 1912 defaultSession.logout(); 1913 } 1914 } 1915 } 1916 1917 private Cache<PageTagCacheKey, List<String>> _getMemoryPageTagCache() 1918 { 1919 return _cacheManager.get(MEMORY_PAGESTAGCACHE); 1920 } 1921 1922 /** 1923 * Set the visible of pages 1924 * @param pageIds The id of pages 1925 * @param visible <code>true</code> to set pages as visible, <code>false</code> otherwise 1926 * @return The result map 1927 */ 1928 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 1929 public Map<String, Object> setVisibility (List<String> pageIds, boolean visible) 1930 { 1931 Map<String, Object> result = new HashMap<>(); 1932 1933 result.put("nomodifiable-pages", new ArrayList<>()); 1934 result.put("noright-pages", new ArrayList<>()); 1935 result.put("allright-pages", new ArrayList<>()); 1936 result.put("locked-pages", new ArrayList<>()); 1937 1938 for (String id : pageIds) 1939 { 1940 Page page = _resolver.resolveById(id); 1941 1942 Map<String, Object> page2json = new HashMap<>(); 1943 page2json.put("id", page.getId()); 1944 page2json.put("title", page.getTitle()); 1945 1946 if (_rightManager.currentUserHasRight("Web_Right_Page_Visibility", page) != RightResult.RIGHT_ALLOW) 1947 { 1948 @SuppressWarnings("unchecked") 1949 List<Map<String, Object>> norightPages = (List<Map<String, Object>>) result.get("noright-pages"); 1950 norightPages.add(page2json); 1951 } 1952 else if (page instanceof LockablePage lockablePage && lockablePage.isLocked()) 1953 { 1954 @SuppressWarnings("unchecked") 1955 List<Map<String, Object>> lockedPages = (List<Map<String, Object>>) result.get("nomodifiable-pages"); 1956 lockedPages.add(page2json); 1957 } 1958 else if (page instanceof ModifiablePage) 1959 { 1960 ModifiablePage mPage = (ModifiablePage) page; 1961 mPage.setVisible(visible); 1962 mPage.saveChanges(); 1963 1964 Map<String, Object> eventParams = new HashMap<>(); 1965 eventParams.put(ObservationConstants.ARGS_PAGE, page); 1966 eventParams.put(ObservationConstants.ARGS_PAGE_ID, page.getId()); 1967 _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_UPDATED, _currentUserProvider.getUser(), eventParams)); 1968 1969 @SuppressWarnings("unchecked") 1970 List<Map<String, Object>> allRightPages = (List<Map<String, Object>>) result.get("allright-pages"); 1971 allRightPages.add(page2json); 1972 } 1973 else 1974 { 1975 @SuppressWarnings("unchecked") 1976 List<Map<String, Object>> nomodifiablePages = (List<Map<String, Object>>) result.get("nomodifiable-pages"); 1977 nomodifiablePages.add(page2json); 1978 } 1979 } 1980 1981 return result; 1982 } 1983 1984 /** 1985 * Get the contents of a {@link Page} and its subpages which can be deleted. 1986 * A content is deleteable if user has right, the content is not locked and not referenced by other pages. 1987 * @param id The id of page 1988 * @return The list of deletable contents 1989 */ 1990 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 1991 public Map<String, Object> getDeleteablePageContents (String id) 1992 { 1993 Map<String, Object> results = new HashMap<>(); 1994 1995 results.put("deleteable-contents", new ArrayList<>()); 1996 results.put("referenced-contents", new ArrayList<>()); 1997 results.put("unauthorized-contents", new ArrayList<>()); 1998 results.put("locked-contents", new ArrayList<>()); 1999 2000 Page page = _resolver.resolveById(id); 2001 List<Content> contents = getPageContents(page, true); 2002 2003 for (Content content : contents) 2004 { 2005 Map<String, Object> contentParams = new HashMap<>(); 2006 contentParams.put("id", content.getId()); 2007 contentParams.put("title", content.getTitle(LocaleUtils.toLocale(page.getSitemapName()))); 2008 contentParams.put("name", content.getName()); 2009 2010 if (_isReferenced(content)) 2011 { 2012 // Content is referenced by at least another page 2013 @SuppressWarnings("unchecked") 2014 List<Map<String, Object>> referencedContents = (List<Map<String, Object>>) results.get("referenced-contents"); 2015 referencedContents.add(contentParams); 2016 } 2017 else if (!_contentDAO.canDelete(content)) 2018 { 2019 @SuppressWarnings("unchecked") 2020 List<Map<String, Object>> unauthorizedContents = (List<Map<String, Object>>) results.get("unauthorized-contents"); 2021 unauthorizedContents.add(contentParams); 2022 } 2023 else if (_isLocked(content)) 2024 { 2025 // If the content is locked by other 2026 @SuppressWarnings("unchecked") 2027 List<Map<String, Object>> lockedContents = (List<Map<String, Object>>) results.get("locked-contents"); 2028 lockedContents.add(contentParams); 2029 } 2030 else 2031 { 2032 Map<String, Object> content2json = new HashMap<>(); 2033 content2json.put("id", content.getId()); 2034 content2json.put("name", content.getName()); 2035 content2json.put("title", content.getTitle(LocaleUtils.toLocale(page.getSitemapName()))); 2036 content2json.put("isNew", _isNew(content)); 2037 content2json.put("isShared", content instanceof SharedContent); 2038 content2json.put("hasShared", _sharedContentManager.hasSharedContents(content)); 2039 2040 @SuppressWarnings("unchecked") 2041 List<Map<String, Object>> allrightContents = (List<Map<String, Object>>) results.get("deleteable-contents"); 2042 allrightContents.add(content2json); 2043 } 2044 } 2045 2046 return results; 2047 } 2048 2049 /** 2050 * Get the contents that belong to the {@link Page} and its sub-pages and that can be deleted. 2051 * A content is deleteable if user has right, the content is not locked and it's not referenced by other pages. 2052 * If 'onlyNewlyCreatedContents' is set to 'true', only newly created contents will be returned 2053 * @param pageId The id of page 2054 * @param onlyNewlyCreatedContents true to return only the newly created contents 2055 * @return The ids of deleteable contents 2056 */ 2057 public List<String> getDeleteablePageContentIds (String pageId, boolean onlyNewlyCreatedContents) 2058 { 2059 List<String> contentsId = new ArrayList<>(); 2060 2061 Page page = _resolver.resolveById(pageId); 2062 2063 List<Content> contents = getPageContents(page, true); 2064 2065 for (Content content : contents) 2066 { 2067 if (_contentDAO.canDelete(content) && !_isLocked(content) && !_isReferenced(content)) 2068 { 2069 if (!onlyNewlyCreatedContents || _isNew(content)) 2070 { 2071 contentsId.add(content.getId()); 2072 } 2073 } 2074 } 2075 2076 return contentsId; 2077 } 2078 2079 /** 2080 * Get the unreferenced contents of a {@link Page} or a {@link ZoneItem} 2081 * @param id The id of page or zone item 2082 * @return The list of unreferenced contents 2083 */ 2084 @Callable (rights = Callable.READ_ACCESS, rightContext = PageRightAssignmentContext.ID, paramIndex = 0) 2085 public List<Map<String, Object>> getUnreferencedContents (String id) 2086 { 2087 List<Map<String, Object>> unreferencedContents = new ArrayList<>(); 2088 2089 Page page = _resolver.resolveById(id); 2090 List<Content> contents = getPageContents(page, true); 2091 2092 for (Content content : contents) 2093 { 2094 if (!_isReferenced(content)) 2095 { 2096 Map<String, Object> content2json = new HashMap<>(); 2097 content2json.put("id", content.getId()); 2098 content2json.put("name", content.getName()); 2099 content2json.put("title", content.getTitle(LocaleUtils.toLocale(page.getSitemapName()))); 2100 content2json.put("isNew", _isNew(content)); 2101 content2json.put("isShared", content instanceof SharedContent); 2102 content2json.put("hasShared", _sharedContentManager.hasSharedContents(content)); 2103 2104 unreferencedContents.add(content2json); 2105 } 2106 } 2107 2108 return unreferencedContents; 2109 } 2110 2111 /** 2112 * Returns the page's attachments root node 2113 * @param id the page's id 2114 * @return The attachments' root node informations 2115 */ 2116 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 2117 public Map<String, Object> getAttachmentsRootNode (String id) 2118 { 2119 Map<String, Object> result = new HashMap<>(); 2120 2121 Page page = _resolver.resolveById(id); 2122 2123 result.put("title", page.getTitle()); 2124 result.put("pageId", page.getId()); 2125 2126 if (_rightManager.currentUserHasRight("Web_Rights_Page_Attachments", page) != RightResult.RIGHT_ALLOW 2127 && _rightManager.currentUserHasRight("Web_Rights_Page_AttachmentHandle", page) != RightResult.RIGHT_ALLOW) 2128 { 2129 return result; 2130 } 2131 2132 TraversableAmetysObject attachments = page.getRootAttachments(); 2133 2134 if (attachments != null) 2135 { 2136 result.put("id", attachments.getId()); 2137 if (attachments instanceof ModifiableAmetysObject) 2138 { 2139 result.put("isModifiable", true); 2140 } 2141 if (attachments instanceof ModifiableResourceCollection) 2142 { 2143 result.put("canCreateChild", true); 2144 } 2145 2146 boolean hasChildNodes = false; 2147 boolean hasResources = false; 2148 2149 for (AmetysObject child : attachments.getChildren()) 2150 { 2151 if (child instanceof Resource) 2152 { 2153 hasResources = true; 2154 } 2155 else if (child instanceof ExplorerNode) 2156 { 2157 hasChildNodes = true; 2158 } 2159 } 2160 2161 if (hasChildNodes) 2162 { 2163 result.put("hasChildNodes", true); 2164 } 2165 2166 if (hasResources) 2167 { 2168 result.put("hasResources", true); 2169 } 2170 2171 return result; 2172 } 2173 2174 throw new IllegalArgumentException("Page with id '" + id + "' does not support attachments."); 2175 } 2176 /** 2177 * Returns the page's parents ids 2178 * @param id the page's id 2179 * @return The attachments' root node informations 2180 */ 2181 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 2182 public Map<String, Object> getPageParents (String id) 2183 { 2184 Map<String, Object> result = new HashMap<>(); 2185 List<Map<String, Object>> pages = new ArrayList<>(); 2186 Page page = _resolver.resolveById(id); 2187 pages.add(_page2Json(page)); 2188 while (page.getParent() != null && page.getParent() instanceof Page) 2189 { 2190 page = page.getParent(); 2191 pages.add(_page2Json(page)); 2192 } 2193 result.put("parents", pages); 2194 return result; 2195 } 2196 2197 /** 2198 * Get the contents of a page and its child pages 2199 * @param sitemapElement The page 2200 * @return The list of contents 2201 */ 2202 public List<Content> getPageContents (SitemapElement sitemapElement) 2203 { 2204 return getPageContents(sitemapElement, false); 2205 } 2206 2207 /** 2208 * Get the contents of a page and its child pages 2209 * @param sitemapElement The page 2210 * @param ignoreContentsOfNonRemovablePage true to ignore contents of non-removable pages (virtual pages) 2211 * @return The list of contents 2212 */ 2213 public List<Content> getPageContents (SitemapElement sitemapElement, boolean ignoreContentsOfNonRemovablePage) 2214 { 2215 List<Content> contents = new ArrayList<>(); 2216 2217 if ((!ignoreContentsOfNonRemovablePage || sitemapElement instanceof RemovableAmetysObject) 2218 && sitemapElement.getTemplate() != null) 2219 { 2220 for (Zone zone : sitemapElement.getZones()) 2221 { 2222 try (AmetysObjectIterable< ? extends ZoneItem> zoneItems = zone.getZoneItems()) 2223 { 2224 for (ZoneItem zoneItem : zoneItems) 2225 { 2226 if (zoneItem.getType() == ZoneItem.ZoneType.CONTENT) 2227 { 2228 contents.add(zoneItem.getContent()); 2229 } 2230 } 2231 } 2232 } 2233 } 2234 2235 AmetysObjectIterable< ? extends Page> childrenPages = sitemapElement.getChildrenPages(); 2236 for (Page childPage : childrenPages) 2237 { 2238 contents.addAll(getPageContents(childPage, ignoreContentsOfNonRemovablePage)); 2239 } 2240 2241 return contents; 2242 } 2243 2244 /** 2245 * Get the user rights on sitemap element (page or sitemap) 2246 * @param pagesCt The sitemap element 2247 * @return The user's rights 2248 */ 2249 protected Set<String> getUserRights (SitemapElement pagesCt) 2250 { 2251 UserIdentity user = _currentUserProvider.getUser(); 2252 2253 Set<String> userRights = _rightManager.getUserRights(user, pagesCt); 2254 2255 // Do some specific stuff here, because the right 'Web_Rights_Page_Delete' is a right to delete child pages and not the page itself. 2256 // So the right should be checked on parent context. 2257 if (pagesCt instanceof Page) 2258 { 2259 SitemapElement parent = pagesCt.getParent(); 2260 boolean canDelete = _rightManager.hasRight(user, "Web_Rights_Page_Delete", parent) == RightResult.RIGHT_ALLOW; 2261 if (!canDelete) 2262 { 2263 // No right on parent page, so remove the right if exists. 2264 userRights.remove("Web_Rights_Page_Delete"); 2265 } 2266 } 2267 else 2268 { 2269 // There is no right of deletion on the sitemap 2270 userRights.remove("Web_Rights_Page_Delete"); 2271 } 2272 2273 return userRights; 2274 } 2275 2276 private boolean _isReferenced (Content content) 2277 { 2278 return content instanceof WebContent && ((WebContent) content).getReferencingPages().size() > 1; 2279 } 2280 2281 private boolean _isLocked (Content content) 2282 { 2283 if (content instanceof LockableAmetysObject) 2284 { 2285 LockableAmetysObject lockableContent = (LockableAmetysObject) content; 2286 if (lockableContent.isLocked()) 2287 { 2288 boolean canUnlockAll = _rightManager.hasRight(_currentUserProvider.getUser(), "CMS_Rights_UnlockAll", "/cms") == RightResult.RIGHT_ALLOW; 2289 if (!LockHelper.isLockOwner(lockableContent, _currentUserProvider.getUser()) && !canUnlockAll) 2290 { 2291 return true; 2292 } 2293 } 2294 } 2295 2296 return false; 2297 } 2298 2299 private boolean _isNew (Content content) 2300 { 2301 boolean isNew = false; 2302 if (content instanceof WorkflowAwareContent) 2303 { 2304 WorkflowAwareContent waContent = (WorkflowAwareContent) content; 2305 long workflowId = waContent.getWorkflowId(); 2306 2307 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent); 2308 isNew = workflow.getHistorySteps(workflowId).isEmpty(); 2309 } 2310 return isNew; 2311 } 2312 2313 private Map<String, Object> _page2Json (Page page) 2314 { 2315 Map<String, Object> page2json = new HashMap<>(); 2316 page2json.put("id", page.getId()); 2317 page2json.put("title", page.getTitle()); 2318 page2json.put("siteName", page.getSiteName()); 2319 page2json.put("path", page.getPathInSitemap()); 2320 return page2json; 2321 } 2322 2323 private Map<String, Object> _publication2Json (Page page) 2324 { 2325 Map<String, Object> pub2json = new HashMap<>(); 2326 2327 if (page.hasValue(DefaultPage.METADATA_PUBLICATION_START_DATE, ModelItemTypeConstants.DATETIME_TYPE_ID)) 2328 { 2329 Object startDateAsJSON = page.dataToJSON(DefaultPage.METADATA_PUBLICATION_START_DATE); 2330 pub2json.put("startDate", startDateAsJSON); 2331 } 2332 2333 if (page.hasValue(DefaultPage.METADATA_PUBLICATION_END_DATE, ModelItemTypeConstants.DATETIME_TYPE_ID)) 2334 { 2335 Object endDateAsJSON = page.dataToJSON(DefaultPage.METADATA_PUBLICATION_END_DATE); 2336 pub2json.put("endDate", endDateAsJSON); 2337 } 2338 2339 return pub2json; 2340 2341 } 2342 2343 private void _updateContentsAfterCopy(Page initialPage, Page createdPage) throws AmetysRepositoryException 2344 { 2345 for (Zone zone : createdPage.getZones()) 2346 { 2347 Zone initialZone = initialPage.getZone(zone.getName()); 2348 try (AmetysObjectIterable< ? extends ZoneItem> zoneItems = zone.getZoneItems(); AmetysObjectIterable< ? extends ZoneItem> initialZoneItems = initialZone.getZoneItems()) 2349 { 2350 AmetysObjectIterator<? extends ZoneItem> iterator = zoneItems.iterator(); 2351 AmetysObjectIterator< ? extends ZoneItem> initialIterator = initialZoneItems.iterator(); 2352 if (iterator.getSize() != initialIterator.getSize()) 2353 { 2354 throw new IllegalStateException("An error occured during the copy of " + initialPage.getName() + " (" + initialPage.getId() + "). The resulting page have a different number of zoneItems."); 2355 } 2356 while (iterator.hasNext()) 2357 { 2358 ZoneItem zoneItem = iterator.next(); 2359 ZoneItem initialZoneItem = initialIterator.next(); 2360 if (zoneItem.getType().equals(ZoneType.CONTENT)) 2361 { 2362 Content content = zoneItem.getContent(); 2363 WebContent initialContent = initialZoneItem.getContent(); 2364 2365 // Updaters 2366 Set<String> ids = _copyUpdaterEP.getExtensionsIds(); 2367 for (String id : ids) 2368 { 2369 _copyUpdaterEP.getExtension(id).updateContent(initialPage.getSite(), createdPage.getSite(), initialContent, content); 2370 } 2371 2372 // Convert content language if necessary 2373 if (content instanceof ModifiableContent && content.getLanguage() != null && content.getLanguage() != createdPage.getSitemapName()) 2374 { 2375 content.setLanguage(createdPage.getSitemapName()); 2376 ((ModifiableContent) content).saveChanges(); 2377 } 2378 2379 // Create the first version 2380 if (content instanceof VersionableAmetysObject) 2381 { 2382 ((VersionableAmetysObject) content).checkpoint(); 2383 } 2384 } 2385 } 2386 } 2387 } 2388 2389 // Browse child pages 2390 for (Page childPage : createdPage.getChildrenPages()) 2391 { 2392 Page initialChildPage = initialPage.getChild(childPage.getName()); 2393 _updateContentsAfterCopy (initialChildPage, childPage); 2394 } 2395 } 2396 2397 private List<String> _getChildrenPageIds (Page page) 2398 { 2399 List<String> childIds = new ArrayList<>(); 2400 2401 for (Page childPage : page.getChildrenPages()) 2402 { 2403 childIds.add(childPage.getId()); 2404 childIds.addAll(_getChildrenPageIds(childPage)); 2405 } 2406 2407 return childIds; 2408 } 2409 2410 /** 2411 * Check each parent and return true if one of them is invisible 2412 * @param page page to check 2413 * @return true if at least one parent is invisible 2414 */ 2415 private boolean _isParentInvisible (Page page) 2416 { 2417 AmetysObject parent = page.getParent(); 2418 while (parent != null && parent instanceof Page) 2419 { 2420 boolean invisible = !((Page) parent).isVisible(); 2421 if (invisible) 2422 { 2423 return true; 2424 } 2425 parent = parent.getParent(); 2426 } 2427 return false; 2428 } 2429 2430 /* start of a group of methods for _isPreviewable */ 2431 /** 2432 * Determine if this page is previewable 2433 * @param page The page to look at 2434 * @return true if the page can be previewed 2435 */ 2436 private boolean _isPreviewable(Page page) 2437 { 2438 // Check for infinitive loop redirection 2439 ArrayList<String> pagesSequence = new ArrayList<>(); 2440 pagesSequence.add(page.getId()); 2441 if (_isInfiniteRedirection (page, pagesSequence)) 2442 { 2443 getLogger().error("An infinite loop redirection was detected for page '" + page.getPathInSitemap() + "'"); 2444 return false; 2445 } 2446 2447 if (page.getType() == PageType.LINK && LinkType.PAGE.equals(page.getURLType())) 2448 { 2449 return _isPageExist(page.getURL()) && _isPreviewable((Page) _resolver.resolveById(page.getURL())); 2450 } 2451 2452 if (page.getType() != PageType.NODE) 2453 { 2454 return true; 2455 } 2456 else 2457 { 2458 try (AmetysObjectIterable< ? extends Page> childrenPages = page.getChildrenPages()) 2459 { 2460 for (Page subPage : childrenPages) 2461 { 2462 if (_isPreviewable(subPage)) 2463 { 2464 return true; 2465 } 2466 } 2467 } 2468 } 2469 return false; 2470 } 2471 2472 private boolean _isPageExist (String id) 2473 { 2474 try 2475 { 2476 _resolver.resolveById(id); 2477 return true; 2478 } 2479 catch (UnknownAmetysObjectException e) 2480 { 2481 return false; 2482 } 2483 } 2484 2485 private boolean _isInfiniteRedirection (Page page, List<String> pagesSequence) 2486 { 2487 Page redirectPage = _getPageRedirection (page); 2488 if (redirectPage == null) 2489 { 2490 return false; 2491 } 2492 2493 if (pagesSequence.contains(redirectPage.getId())) 2494 { 2495 return true; 2496 } 2497 2498 pagesSequence.add(redirectPage.getId()); 2499 return _isInfiniteRedirection (redirectPage, pagesSequence); 2500 } 2501 2502 private Page _getPageRedirection (Page page) 2503 { 2504 if (PageType.LINK.equals(page.getType()) && LinkType.PAGE.equals(page.getURLType())) 2505 { 2506 try 2507 { 2508 String pageId = page.getURL(); 2509 return _resolver.resolveById(pageId); 2510 } 2511 catch (AmetysRepositoryException e) 2512 { 2513 return null; 2514 } 2515 } 2516 else if (PageType.NODE.equals(page.getType())) 2517 { 2518 AmetysObjectIterable<? extends Page> childPages = page.getChildrenPages(); 2519 Iterator<? extends Page> it = childPages.iterator(); 2520 if (it.hasNext()) 2521 { 2522 return it.next(); 2523 } 2524 } 2525 2526 return null; 2527 } 2528 /* end of a group of methods for _isPreviewable */ 2529 2530 private static final class PageTagCacheKey extends AbstractCacheKey 2531 { 2532 private PageTagCacheKey(String sitename, String lang, String tag) 2533 { 2534 super(sitename, lang, tag); 2535 } 2536 2537 private PageTagCacheKey(String sitename, String lang) 2538 { 2539 super(sitename, lang); 2540 } 2541 2542 static PageTagCacheKey of(String sitename, String lang) 2543 { 2544 return new PageTagCacheKey(sitename, lang, null); 2545 } 2546 2547 static PageTagCacheKey of(String sitename, String lang, String tag) 2548 { 2549 return new PageTagCacheKey(sitename, lang, tag); 2550 } 2551 } 2552}