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