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