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                    eventParams.put(ObservationConstants.ARGS_PAGE_ID, page.getId());
889                    _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_CHANGED, _currentUserProvider.getUser(), eventParams));
890                }
891            }
892            catch (Exception e)
893            {
894                getLogger().error("Cannot set the page '" + pageId + "' as link [" + url + ", " + urlType.toString() + "]", e);
895                errorPages.add(Map.of("id", pageId, "title", page.getTitle()));
896            }
897        }
898
899        return Map.of("allright-pages", allRightIds, "noright-pages", noRightPages, "error-pages", errorPages, "nomodifiable-pages", noModifiablePages, "locked-pages", lockedPages);
900    }
901    
902    /**
903     * Get available template for specified page
904     * @param pageId The page's id
905     * @return the list of available template as JSON
906     */
907    private List<Map<String, Object>> _getAvailableTemplates (String pageId)
908    {
909        List<Map<String, Object>> templates = new ArrayList<>();
910        
911        Page page = _resolver.resolveById(pageId);
912        
913        Set<String> availableTemplateIds = _templatesHandler.getAvailablesTemplates(page);
914        for (String templateName : availableTemplateIds)
915        {
916            String skinId = page.getSite().getSkinId();
917            Skin skin = _skinsManager.getSkin(skinId);
918            
919            SkinTemplate template = skin.getTemplate(templateName);
920            
921            Map<String, Object> template2json = new HashMap<>();
922            template2json.put("id", template.getId());
923            template2json.put("label", template.getLabel());
924            template2json.put("description", template.getDescription());
925            template2json.put("iconSmall", template.getSmallImage());
926            template2json.put("iconMedium", template.getMediumImage());
927            template2json.put("iconLarge", template.getLargeImage());
928            template2json.put("zone", template.getDefaultZoneId());
929            
930            templates.add(template2json);
931        }
932        
933        return templates;
934    }
935    
936    /**
937     * Get available content types for specified page
938     * @param pageId The page's id
939     * @param zoneName the name of the zone
940     * @return the list of available content types
941     */
942    @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION)
943    public List<Map<String, Object>> getAvailableContentTypes (String pageId, String zoneName)
944    {
945        List<Map<String, Object>> contenttypes = new ArrayList<>();
946        
947        Page page = _resolver.resolveById(pageId);
948        
949        Set<String> contentTypeIds = _cTypeHandler.getAvailableContentTypes(page, zoneName);
950        for (String contentTypeId : contentTypeIds)
951        {
952            ContentType cType = _contentTypeExtensionPoint.getExtension(contentTypeId);
953            
954            if (cType != null && _hasRight(cType, page))
955            {
956                Map<String, Object> ctype2json = new HashMap<>();
957                ctype2json.put("id", cType.getId());
958                ctype2json.put("label", cType.getLabel());
959                ctype2json.put("description", cType.getDescription());
960                ctype2json.put("iconGlyph", cType.getIconGlyph());
961                ctype2json.put("iconDecorator", cType.getIconDecorator());
962                ctype2json.put("iconSmall", cType.getSmallIcon());
963                ctype2json.put("iconMedium", cType.getMediumIcon());
964                ctype2json.put("iconLarge", cType.getLargeIcon());
965                ctype2json.put("defaultTitle", cType.getDefaultTitle());
966                ctype2json.put("viewNames", cType.getViewNames(true));
967                ctype2json.put("category", cType.getCategory().isI18n() ? cType.getCategory().getKey() : cType.getCategory().getLabel());
968                ctype2json.put("categoryLabel", cType.getCategory());
969                
970                contenttypes.add(ctype2json);
971            }
972        }
973        
974        return contenttypes;
975    }
976    
977    /**
978     * Get available content types for a page being created
979     * @param pageId The page's id. Can be null of the page is not yet created
980     * @param zoneName the name of the zone
981     * @param parentId The id of parent page
982     * @param pageTitle The title of page to create
983     * @param template The template of page to create
984     * @return the list of available services
985     */
986    @Callable (rights = Callable.NO_CHECK_REQUIRED)
987    public List<Map<String, Object>> getAvailableContentTypesForCreation(String pageId, String zoneName, String parentId, String pageTitle, String template)
988    {
989        if (StringUtils.isNotEmpty(pageId))
990        {
991            // Get available services for a page
992            return getAvailableContentTypes(pageId, zoneName);
993        }
994        else if (StringUtils.isNotEmpty(parentId))
995        {
996            // Get available services for a not yet existing page
997            SitemapElement parent = _resolver.resolveById(parentId);
998            
999            // Create page temporarily
1000            Page page = _createPage(parent, pageTitle, template);
1001            
1002            List<Map<String, Object>> availableContentTypes = getAvailableContentTypes(page.getId(), zoneName);
1003            
1004            // Cancel page creation
1005            page.getSitemap().revertChanges();
1006            
1007            return availableContentTypes;
1008        }
1009        
1010        return Collections.EMPTY_LIST;
1011    }
1012    
1013    private boolean _hasRight(ContentType contentType, Page page)
1014    {
1015        String right = contentType.getRight();
1016        
1017        if (right == null)
1018        {
1019            return true;
1020        }
1021        else
1022        {
1023            UserIdentity user = _currentUserProvider.getUser();
1024            return _rightManager.hasRight(user, right, page) == RightResult.RIGHT_ALLOW;
1025        }
1026    }
1027    
1028    /**
1029     * Get available services for specified page
1030     * @param pageId The page's id
1031     * @param zoneName the name of the zone
1032     * @return the list of available services
1033     */
1034    @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION)
1035    public List<Map<String, Object>> getAvailableServices (String pageId, String zoneName)
1036    {
1037        List<Map<String, Object>> services = new ArrayList<>();
1038        
1039        SitemapElement sitemapElement = _resolver.resolveById(pageId);
1040        
1041        Set<String> serviceIds = _serviceHandler.getAvailableServices(sitemapElement, zoneName);
1042        for (String serviceId : serviceIds)
1043        {
1044            Service service = _serviceExtensionPoint.getExtension(serviceId);
1045            if (service != null && _hasRight(service, sitemapElement))
1046            {
1047                Map<String, Object> serviceMap = new HashMap<>();
1048                serviceMap.put("id", service.getId());
1049                serviceMap.put("label", service.getLabel());
1050                serviceMap.put("description", service.getDescription());
1051                serviceMap.put("iconGlyph", service.getIconGlyph());
1052                serviceMap.put("iconDecorator", service.getIconDecorator());
1053                serviceMap.put("iconSmall", service.getSmallIcon());
1054                serviceMap.put("iconMedium", service.getMediumIcon());
1055                serviceMap.put("iconLarge", service.getLargeIcon());
1056                serviceMap.put("parametersAction", service.getParametersScript().getScriptClassname());
1057                serviceMap.put("category", service.getCategory().isI18n() ? service.getCategory().getKey() : service.getCategory().getLabel());
1058                serviceMap.put("categoryLabel", service.getCategory());
1059                
1060                services.add(serviceMap);
1061            }
1062        }
1063        
1064        return services;
1065    }
1066    
1067    /**
1068     * Get available services for a page being created
1069     * @param pageId The page's id. Can be null of the page is not yet created
1070     * @param zoneName the name of the zone
1071     * @param parentId The id of parent page
1072     * @param pageTitle The title of page to create
1073     * @param template The template of page to create
1074     * @return the list of available services
1075     */
1076    @Callable (rights = Callable.NO_CHECK_REQUIRED)
1077    public List<Map<String, Object>> getAvailableServicesForCreation(String pageId, String zoneName, String parentId, String pageTitle, String template)
1078    {
1079        if (StringUtils.isNotEmpty(pageId))
1080        {
1081            // Get available services for a page
1082            return getAvailableServices(pageId, zoneName);
1083        }
1084        else if (StringUtils.isNotEmpty(parentId))
1085        {
1086            // Get available services for a not yet existing page
1087            SitemapElement parent = _resolver.resolveById(parentId);
1088            
1089            // Create page temporarily
1090            Page page = _createPage(parent, pageTitle, template);
1091            
1092            List<Map<String, Object>> availableServices = getAvailableServices(page.getId(), zoneName);
1093            
1094            // Cancel page creation
1095            page.getSitemap().revertChanges();
1096            
1097            return availableServices;
1098        }
1099        
1100        return Collections.EMPTY_LIST;
1101    }
1102    
1103    private Page _createPage (SitemapElement parent, String pageTitle, String template)
1104    {
1105        Site site = parent.getSite();
1106        
1107        String pageName = PageDAO.findFreePageNameByTitle(parent, pageTitle);
1108        
1109        ModifiablePage page = ((ModifiableTraversableAmetysObject) parent).createChild(pageName, "ametys:defaultPage");
1110        
1111        page.setTitle(pageTitle);
1112        page.setType(PageType.NODE);
1113        page.setSiteName(site.getName());
1114        page.setSitemapName(page.getSitemap().getName());
1115
1116        if (template != null)
1117        {
1118            String skinId = page.getSite().getSkinId();
1119            SkinTemplate tpl = _skinsManager.getSkin(skinId).getTemplate(template);
1120            if (tpl == null)
1121            {
1122                throw new IllegalStateException("Template '" + template + "' does not exist on skin '" + skinId + "'");
1123            }
1124            
1125            // Set temporary the template to get available services
1126            page.setType(PageType.CONTAINER);
1127            page.setTemplate(template);
1128        }
1129        
1130        return page;
1131    }
1132    
1133    private boolean _hasRight(Service service, SitemapElement sitemapElement)
1134    {
1135        String right = service.getRight();
1136        
1137        if (right == null)
1138        {
1139            return true;
1140        }
1141        else
1142        {
1143            UserIdentity user = _currentUserProvider.getUser();
1144            return _rightManager.hasRight(user, right, sitemapElement) == RightResult.RIGHT_ALLOW;
1145        }
1146    }
1147    
1148    /**
1149     * Get available content types for a page being created
1150     * @param pageId The page's id. Can be null of the page is not yet created
1151     * @param parentId The id of parent page
1152     * @param pageTitle The title of page to create
1153     * @return the list of available services
1154     */
1155    @Callable (rights = "Web_Rights_Page_Create", rightContext = PageRightAssignmentContext.ID, paramIndex = 1)
1156    public List<Map<String, Object>> getAvailableTemplatesForCreation (String pageId, String parentId, String pageTitle)
1157    {
1158        if (StringUtils.isNotEmpty(pageId))
1159        {
1160            // Get available template for a page
1161            return _getAvailableTemplates(pageId);
1162        }
1163        else if (StringUtils.isNotEmpty(parentId))
1164        {
1165            // Get available template for a not yet existing page
1166            SitemapElement parent = _resolver.resolveById(parentId);
1167            
1168            // Create page temporarily
1169            Page page = _createPage(parent, pageTitle, null);
1170            
1171            List<Map<String, Object>> availableTemplates = _getAvailableTemplates(page.getId());
1172            
1173            // Cancel page creation
1174            page.getSitemap().revertChanges();
1175            
1176            return availableTemplates;
1177        }
1178        
1179        return Collections.EMPTY_LIST;
1180    }
1181    
1182    /**
1183     * Get service info
1184     * @param pageId Optional, the page id of the service. To get some basic info about the page.
1185     * @param serviceId The id of the service
1186     * @return a Map containing some info about the service (label, url..)
1187     */
1188    @Callable (rights = Callable.READ_ACCESS, rightContext = PageRightAssignmentContext.ID, paramIndex = 0)
1189    public Map<String, Object> getServiceInfo(String pageId, String serviceId)
1190    {
1191        Map<String, Object> info = new HashMap<>();
1192        
1193        if (StringUtils.isNotEmpty(pageId))
1194        {
1195            SitemapElement sitemapElement = _resolver.resolveById(pageId);
1196            info.put("page-id", sitemapElement.getId());
1197            info.put("page-title", sitemapElement.getTitle());
1198        }
1199
1200        Service service = _serviceExtensionPoint.getExtension(serviceId);
1201        info.put("id", service.getId());
1202        info.put("label", service.getLabel());
1203        info.put("url", service.getURL());
1204        info.put("smallIcon", service.getSmallIcon());
1205        info.put("iconGlyph", service.getIconGlyph());
1206        info.put("iconDecorator", service.getIconDecorator());
1207        return info;
1208    }
1209    
1210    /**
1211     * Rename a page
1212     * @param pageId The id of page to rename
1213     * @param title The page's  title
1214     * @param longTitle The page's long title.
1215     * @param updatePath true to update page's path
1216     * @param createAlias true to create a alias
1217     * @return the result map
1218     */
1219    @Callable (rights = "Web_Rights_Page_Rename", rightContext = PageRightAssignmentContext.ID, paramIndex = 0)
1220    public Map<String, Object> renamePage (String pageId, String title, String longTitle, boolean updatePath, boolean createAlias)
1221    {
1222        Map<String, Object> result = new HashMap<>();
1223        
1224        Page page = _resolver.resolveById(pageId);
1225        
1226        if (!(page instanceof ModifiablePage))
1227        {
1228            throw new IllegalArgumentException("Can not rename a non-modifiable page '/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "'");
1229        }
1230        
1231        if (page instanceof LockablePage lockablePage && lockablePage.isLocked())
1232        {
1233            throw new IllegalArgumentException("Can not rename a locked page '/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "'");
1234        }
1235        
1236        ModifiablePage mPage = (ModifiablePage) page;
1237        mPage.setTitle(title);
1238        mPage.setLongTitle(longTitle);
1239
1240        if (updatePath)
1241        {
1242            String oldPathInSitemap = page.getPathInSitemap();
1243            String oldPath = "/" + page.getSitemapName() + "/" + page.getPathInSitemap() + ".html";
1244            String oldPathForChild = "/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "/**.html";
1245
1246            String pageName = "";
1247            try
1248            {
1249                pageName = PageDAO.findFreePageNameByTitle(mPage.getParent(), title, page.getName());
1250            }
1251            catch (IllegalArgumentException e)
1252            {
1253                result.put("invalid-name", title);
1254                return result;
1255            }
1256            
1257            if (!page.getName().equals(pageName))
1258            {
1259                mPage.rename(pageName);
1260                
1261                if (createAlias)
1262                {
1263                    ModifiableTraversableAmetysObject rootNode = AliasHelper.getRootNode(page.getSite());
1264                    
1265                    DefaultAlias alias = rootNode.createChild(AliasHelper.getAliasNextUniqueName(rootNode), "ametys:alias");
1266                    alias.setUrl(oldPath);
1267                    alias.setTarget(page.getId());
1268                    alias.setType(TargetType.PAGE);
1269                    alias.setCreationDate(new Date());
1270
1271                    // Alias for child pages
1272                    alias = rootNode.createChild(AliasHelper.getAliasNextUniqueName(rootNode), "ametys:alias");
1273                    alias.setUrl(oldPathForChild);
1274                    alias.setTarget("/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "/{1}.html");
1275                    alias.setType(TargetType.URL);
1276                    alias.setCreationDate(new Date());
1277
1278                    rootNode.saveChanges();
1279                }
1280                
1281                // Notify observers that the page has been renamed
1282                Map<String, Object> eventParams = new HashMap<>();
1283                eventParams.put(ObservationConstants.ARGS_PAGE, page);
1284                eventParams.put("path.old.path", oldPathInSitemap);
1285                eventParams.put(ObservationConstants.ARGS_PAGE_PATH, page.getPathInSitemap());
1286                _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_RENAMED, _currentUserProvider.getUser(), eventParams));
1287                
1288            }
1289            else
1290            {
1291                // Notify observers that the page's title has been modified
1292                Map<String, Object> eventParams = new HashMap<>();
1293                eventParams.put(ObservationConstants.ARGS_PAGE, page);
1294                _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_UPDATED, _currentUserProvider.getUser(), eventParams));
1295            }
1296                
1297        }
1298        else
1299        {
1300            // Notify observers that the page's title has been modified
1301            Map<String, Object> eventParams = new HashMap<>();
1302            eventParams.put(ObservationConstants.ARGS_PAGE, page);
1303            _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_UPDATED, _currentUserProvider.getUser(), eventParams));
1304        }
1305        
1306        Sitemap sitemap = page.getSitemap();
1307        if (sitemap.needsSave())
1308        {
1309            sitemap.saveChanges();
1310        }
1311        
1312        result.put("id", page.getId());
1313        result.put("path", page.getPath());
1314        result.put("title", page.getTitle());
1315        
1316        return result;
1317    }
1318    
1319    /**
1320     * Unlock or lock page
1321     * @param pageId the id of the page to unlock or lock
1322     * @param mode the mode ('lock' or 'unlock')
1323     * @return the result JSON map
1324     */
1325    @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION)
1326    public Map<String, Object> unlockOrLock(String pageId, String mode)
1327    {
1328        Map<String, Object> result = new HashMap<>();
1329        
1330        UserIdentity currentUser = _currentUserProvider.getUser();
1331        LockablePage lockablePage = (LockablePage) _resolver.resolveById(pageId);
1332        boolean canChangeLockStatus = _rightManager.hasRight(currentUser, "Web_Rights_Page_Lock", "/cms") == RightResult.RIGHT_ALLOW  && _rightManager.currentUserHasReadAccess(lockablePage);
1333        
1334        boolean unlock = "unlock".equals(mode);
1335        
1336        if (!canChangeLockStatus)
1337        {
1338            getLogger().warn("The user '" + currentUser + "' tried to unlock or lock page '" + pageId + "' but does not have the right");
1339            result.put("error", unlock ? "still-locked-page" : "fail-locked-page");
1340        }
1341        
1342        if (unlock && !lockablePage.isLocked())
1343        {
1344            getLogger().warn("The page '" + pageId + "' is not locked anymore.");
1345        }
1346        else if (!unlock && lockablePage.isLocked())
1347        {
1348            getLogger().warn("The page '" + pageId + "' is already locked.");
1349            result.put("error", "already-locked-page");
1350        }
1351        else
1352        {
1353            try
1354            {
1355                if (unlock)
1356                {
1357                    lockablePage.unlock();
1358                    if (getLogger().isDebugEnabled())
1359                    {
1360                        getLogger().debug("The user has unlocked page '" + pageId + "'");
1361                    }
1362                }
1363                else
1364                {
1365                    lockablePage.lock();
1366                }
1367                
1368                lockablePage.saveChanges();
1369                
1370            }
1371            catch (AmetysRepositoryException e)
1372            {
1373                getLogger().error("Unable to lock or unlock page '" + pageId + "'", e);
1374                result.put("error", unlock ? "fail-unlocked-page" : "fail-locked-page");
1375            }
1376        }
1377        
1378        result.put("pageId", pageId);
1379        result.put("pageTitle", lockablePage.getTitle());
1380        result.put("mode", mode);
1381        return result;
1382    }
1383    
1384    /**
1385     * Delete a page and its sub-pages.
1386     * The contents that belong only to the deleted page will be deleted if 'deleteBelongingContents' is set to true.
1387     * The newly created contents are deleted whatever the value if 'deleteBelongingContents'.
1388     * @param pageId the id of page to delete
1389     * @param deleteBelongingContents true to delete the contents that belong to the page and its sub-pages only
1390     * @return The id of deleted pages
1391     */
1392    @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION)
1393    public Map<String, Object> deletePage(String pageId, boolean deleteBelongingContents)
1394    {
1395        ModifiablePage page = _resolver.resolveById(pageId);
1396        
1397        // Check rights on parent
1398        if (!_canDelete(page))
1399        {
1400            throw new IllegalStateException("You do not have the rights to delete the page '/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "'");
1401        }
1402        
1403        if (page instanceof LockablePage lockablePage && lockablePage.isLocked())
1404        {
1405            throw new IllegalStateException("Can not delete the locked page '/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "'");
1406        }
1407        
1408        return deletePage(page, deleteBelongingContents);
1409    }
1410    
1411    /**
1412     * Delete a page and its sub-pages.
1413     * The contents that belong only to the deleted page will be deleted if 'deleteBelongingContents' is set to true.
1414     * The newly created contents are deleted whatever the value if 'deleteBelongingContents'.
1415     * @param page the page to delete
1416     * @param deleteBelongingContents true to delete the contents that belong to the page and its sub-pages only
1417     * @return The id of deleted pages
1418     */
1419    public Map<String, Object> deletePage(ModifiablePage page, boolean deleteBelongingContents)
1420    {
1421        Map<String, Object> result = new HashMap<>();
1422        
1423        PageHierarchy pageHierarchy = _getPageContents(page, true);
1424        List<String> contentsToDelete = _getDeleteablePageContentIds(pageHierarchy, !deleteBelongingContents);
1425        
1426        Sitemap sitemap = page.getSitemap();
1427        SitemapElement parent = page.getParent();
1428        String pagePathInSitemap = page.getPathInSitemap();
1429        
1430        List<String> childPagesIds = _getChildrenPageIds(page);
1431        
1432        Map<String, Object> eventParams = new HashMap<>();
1433        eventParams.put(ObservationConstants.ARGS_PAGE_ID, page.getId());
1434        eventParams.put(ObservationConstants.ARGS_PAGE_PARENT, parent);
1435        eventParams.put(ObservationConstants.ARGS_PAGE_PATH, pagePathInSitemap);
1436        eventParams.put(ObservationConstants.ARGS_SITEMAP, sitemap);
1437        eventParams.put(ObservationConstants.ARGS_PAGE_CONTENTS, getPageContents(page));
1438        _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_DELETING, _currentUserProvider.getUser(), eventParams));
1439        
1440        // FIXME API test if this is not modifiable
1441        page.getParent();
1442        
1443        if (page instanceof TrashableAmetysObject trashableAO)
1444        {
1445            _trashElementDAO.trash(trashableAO, false, pageHierarchy.contents().stream().map(Content::getId).toArray(String[]::new));
1446            
1447            result.put("trashed", true);
1448        }
1449        else
1450        {
1451            page.remove();
1452            ((ModifiableAmetysObject) parent).saveChanges();
1453        }
1454        
1455        _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_DELETED, _currentUserProvider.getUser(), eventParams));
1456        
1457        result.put("id", page.getId());
1458        result.put("childPages", childPagesIds);
1459        result.putAll(_contentDAO.trashContents(contentsToDelete, true));
1460        
1461        for (String contentId : contentsToDelete)
1462        {
1463            TrashElement trashElement = _trashElementDAO.find(contentId);
1464            if (trashElement != null)
1465            {
1466                trashElement.setHidden(true);
1467                trashElement.saveChanges();
1468                
1469                // Notify observers
1470                eventParams = new HashMap<>();
1471                eventParams.put(org.ametys.cms.ObservationConstants.ARGS_TRASH_ELEMENT_ID, trashElement.getId());
1472                eventParams.put(org.ametys.cms.ObservationConstants.ARGS_AMETYS_OBJECT_ID, contentId);
1473                _observationManager.notify(new Event(org.ametys.cms.ObservationConstants.EVENT_TRASH_UPDATED, _currentUserProvider.getUser(), eventParams));
1474            }
1475        }
1476        
1477        return result;
1478    }
1479    
1480    /**
1481     * Set pages as blank page
1482     * @param pageIds the id of pages to modify
1483     * @return the id of pages which succeeded or failed.
1484     */
1485    @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION)
1486    public Map<String, Object> setBlank (List<String> pageIds)
1487    {
1488        List<String> allRightIds = new ArrayList<>();
1489        List<Map<String, Object>> noRightPages = new ArrayList<>();
1490        List<Map<String, Object>> errorPages = new ArrayList<>();
1491        List<Map<String, Object>> noModifiablePages = new ArrayList<>();
1492        List<Map<String, Object>> lockedPages = new ArrayList<>();
1493        
1494        for (String pageId : pageIds)
1495        {
1496            Page page = _resolver.resolveById(pageId);
1497            
1498            try
1499            {
1500                if (_rightManager.currentUserHasRight("Web_Rights_Page_BlankPage", page) != RightResult.RIGHT_ALLOW)
1501                {
1502                    noRightPages.add(Map.of("id", pageId, "title", page.getTitle()));
1503                }
1504                else if (!(page instanceof ModifiablePage))
1505                {
1506                    noModifiablePages.add(Map.of("id", pageId, "title", page.getTitle()));
1507                }
1508                else if (page instanceof LockablePage lockablePage && lockablePage.isLocked())
1509                {
1510                    lockedPages.add(Map.of("id", pageId, "title", page.getTitle()));
1511                }
1512                else
1513                {
1514                    ModifiablePage mPage = (ModifiablePage) page;
1515                    
1516                    if (page.getType().equals(PageType.CONTAINER))
1517                    {
1518                        // Remove zones
1519                        for (ModifiableZone zone : mPage.getZones())
1520                        {
1521                            zone.remove();
1522                        }
1523                    }
1524                    
1525                    mPage.setType(PageType.NODE);
1526                    mPage.getSitemap().saveChanges();
1527                    
1528                    allRightIds.add(pageId);
1529                    
1530                    Map<String, Object> eventParams = new HashMap<>();
1531                    eventParams.put(ObservationConstants.ARGS_PAGE, page);
1532                    eventParams.put(ObservationConstants.ARGS_PAGE_ID, page.getId());
1533                    _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_CHANGED, _currentUserProvider.getUser(), eventParams));
1534                }
1535            }
1536            catch (Exception e)
1537            {
1538                getLogger().error("Cannot set the page '" + pageId + "' as blank page", e);
1539                errorPages.add(Map.of("id", pageId, "title", page.getTitle()));
1540            }
1541        }
1542
1543        return Map.of("allright-pages", allRightIds, "noright-pages", noRightPages, "error-pages", errorPages, "nomodifiable-pages", noModifiablePages, "locked-pages", lockedPages);
1544    }
1545    
1546    /**
1547     * Set a template to pages
1548     * @param pageIds the id of pages to update
1549     * @param templateName The template name
1550     * @return the id of pages which succeeded
1551     */
1552    @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION)
1553    public Map<String, Object> setTemplate (List<String> pageIds, String templateName)
1554    {
1555        return setTemplate(pageIds, templateName, true);
1556    }
1557    
1558    /**
1559     * Set a template to pages
1560     * @param pageIds the id of pages to update
1561     * @param templateName The template name
1562     * @param checkAvailableTemplate true if you want to check available template
1563     * @return the id of pages which succeeded
1564     */
1565    public Map<String, Object> setTemplate (List<String> pageIds, String templateName, boolean checkAvailableTemplate)
1566    {
1567        List<String> allRightIds = new ArrayList<>();
1568        List<Map<String, Object>> noRightPages = new ArrayList<>();
1569        List<Map<String, Object>> errorPages = new ArrayList<>();
1570        List<Map<String, Object>> noModifiablePages = new ArrayList<>();
1571        
1572        String defaultZoneName = null;
1573        
1574        for (String pageId : pageIds)
1575        {
1576            Page page = _resolver.resolveById(pageId);
1577            
1578            if (_rightManager.currentUserHasRight("Web_Rights_Page_Templates", page) != RightResult.RIGHT_ALLOW)
1579            {
1580                noRightPages.add(Map.of("id", pageId, "title", page.getTitle()));
1581            }
1582            else if (!(page instanceof ModifiablePage))
1583            {
1584                noModifiablePages.add(Map.of("id", pageId, "title", page.getTitle()));
1585            }
1586            else if (page instanceof LockablePage lockablePage && lockablePage.isLocked())
1587            {
1588                throw new IllegalStateException("You cannot set template on page '/" + lockablePage.getSitemapName() + "/" + lockablePage.getPathInSitemap() + "' because it's locked");
1589            }
1590            else
1591            {
1592                ModifiablePage mPage = (ModifiablePage) page;
1593                
1594                if (defaultZoneName == null)
1595                {
1596                    String skinId = page.getSite().getSkinId();
1597                    SkinTemplate tpl = _skinsManager.getSkin(skinId).getTemplate(templateName);
1598                    if (tpl == null)
1599                    {
1600                        throw new IllegalStateException("Template '" + templateName + "' does not exist on skin '" + skinId + "'");
1601                    }
1602                    
1603                    defaultZoneName = tpl.getDefaultZoneId();
1604                }
1605                
1606                if (checkAvailableTemplate && !_templatesHandler.getAvailablesTemplates(mPage).contains(templateName))
1607                {
1608                    throw new IllegalStateException("Template '" + templateName + "' is not available for page '" + pageId + "'");
1609                }
1610                
1611                if (page.getType().equals(PageType.CONTAINER))
1612                {
1613                    _removeOldZones (mPage, templateName);
1614                }
1615                
1616                mPage.setTemplate(templateName);
1617                mPage.setType(PageType.CONTAINER);
1618                mPage.getSitemap().saveChanges();
1619                
1620                allRightIds.add(pageId);
1621                
1622                Map<String, Object> eventParams = new HashMap<>();
1623                eventParams.put(ObservationConstants.ARGS_PAGE, page);
1624                eventParams.put(ObservationConstants.ARGS_PAGE_ID, page.getId());
1625                _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_CHANGED, _currentUserProvider.getUser(), eventParams));
1626            }
1627            
1628        }
1629        
1630        Map<String, Object> results = new HashMap<>();
1631        results.put("allright-pages", allRightIds);
1632        results.put("noright-pages", noRightPages);
1633        results.put("error-pages", errorPages);
1634        results.put("nomodifiable-pages", noModifiablePages);
1635        
1636        if (defaultZoneName != null)
1637        {
1638            results.put("zonename", defaultZoneName);
1639        }
1640        return results;
1641    }
1642    
1643    /**
1644     * Get the script class name to execute to add or update this service
1645     * @param serviceId the service id
1646     * @return the script class name. Can be empty
1647     */
1648    @Callable (rights = Callable.NO_CHECK_REQUIRED)
1649    public String getServiceParametersAction (String serviceId)
1650    {
1651        try
1652        {
1653            Service service = _serviceExtensionPoint.getExtension(serviceId);
1654            return service.getParametersScript().getScriptClassname();
1655        }
1656        catch (IllegalArgumentException e)
1657        {
1658            throw new IllegalArgumentException("Service with id '" + serviceId + "' does not exist", e);
1659        }
1660    }
1661    
1662    private void _removeOldZones (ModifiablePage page, String templateName)
1663    {
1664        String skinId = page.getSite().getSkinId();
1665        
1666        SkinTemplate oldTemplate = _skinsManager.getSkin(skinId).getTemplate(templateName);
1667        
1668        Map<String, SkinTemplateZone> templateZones = oldTemplate.getZones();
1669        
1670        for (ModifiableZone zone : page.getZones())
1671        {
1672            if (!templateZones.containsKey(zone.getName()))
1673            {
1674                zone.remove();
1675            }
1676        }
1677    }
1678    
1679    /**
1680     * Get the tags from the pages
1681     * @param pageIds The ids of the pages
1682     * @return the tags of the pages
1683     */
1684    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
1685    public Set<String> getTags (List<String> pageIds)
1686    {
1687        Set<String> tags = new HashSet<>();
1688        
1689        for (String pageId : pageIds)
1690        {
1691            Page page = _resolver.resolveById(pageId);
1692            if (_rightManager.currentUserHasReadAccess(page))
1693            {
1694                tags.addAll(page.getTags());
1695            }
1696        }
1697        
1698        return tags;
1699    }
1700    
1701    /**
1702     * Tag a list of pages
1703     * @param pageIds The ids of pages to tag
1704     * @param tagNames The tags
1705     * @param mode The mode for updating tags: 'REPLACE' to replace tags, 'INSERT' to add tags or 'REMOVE' to remove tags.
1706     * @param contextualParameters Contextual parameters. Must contain the site name
1707     * @return the result
1708     */
1709    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
1710    public Map<String, Object> tag (List<String> pageIds, List<String> tagNames, String mode, Map<String, Object> contextualParameters)
1711    {
1712        return tag(pageIds, tagNames, TagMode.valueOf(mode), contextualParameters, false);
1713    }
1714    
1715    /**
1716     * Tag a list of pages
1717     * @param pageIds The ids of pages to tag
1718     * @param tagNames The tags
1719     * @param mode The mode for updating tags: 'REPLACE' to replace tags, 'INSERT' to add tags or 'REMOVE' to remove tags.
1720     * @param contextualParameters Contextual parameters. Must contain the site name
1721     * @param ignoreRights <code>true</code> to ignore rights on tag
1722     * @return the result
1723     */
1724    public Map<String, Object> tag (List<String> pageIds, List<String> tagNames, TagMode mode, Map<String, Object> contextualParameters, boolean ignoreRights)
1725    {
1726        Map<String, Object> result = new HashMap<>();
1727        
1728        result.put("nomodifiable-pages", new ArrayList<>());
1729        result.put("invalid-tags", new ArrayList<>());
1730        result.put("allright-pages", new ArrayList<>());
1731        result.put("noright-pages", new ArrayList<>());
1732        
1733        for (String pageId : pageIds)
1734        {
1735            Page page = _resolver.resolveById(pageId);
1736
1737            Map<String, Object> page2json = new HashMap<>();
1738            page2json.put("id", page.getId());
1739            page2json.put("title", page.getTitle());
1740            
1741            if (!ignoreRights && !_hasTagRights(page, tagNames, mode, contextualParameters))
1742            {
1743                @SuppressWarnings("unchecked")
1744                List<Map<String, Object>> noRightPages = (List<Map<String, Object>>) result.get("noright-pages");
1745                noRightPages.add(page2json);
1746            }
1747            else
1748            {
1749                if (page instanceof ModifiablePage mPage)
1750                {
1751                    @SuppressWarnings("unchecked")
1752                    List<String> invalidTags = (List<String>) result.get("invalid-tags");
1753                    invalidTags.addAll(tag(mPage, tagNames, mode));
1754                    
1755                    page2json.put("tags", page.getTags());
1756                    @SuppressWarnings("unchecked")
1757                    List<Map<String, Object>> allRightPages = (List<Map<String, Object>>) result.get("allright-pages");
1758                    allRightPages.add(page2json);
1759                }
1760                else
1761                {
1762                    @SuppressWarnings("unchecked")
1763                    List<Map<String, Object>> nomodifiablePages = (List<Map<String, Object>>) result.get("nomodifiable-pages");
1764                    nomodifiablePages.add(page2json);
1765                }
1766            }
1767        }
1768        
1769        return result;
1770    }
1771    
1772    private boolean _hasTagRights(Page page, List<String> tagNames, TagMode mode, Map<String, Object> contextualParameters)
1773    {
1774        List<CMSTag> tags = tagNames.stream()
1775            .map(t -> _tagProvider.getTag(t, contextualParameters))
1776            .filter(Objects::nonNull)
1777            .toList();
1778        
1779        // In case of replace, only check the right on tag that are modified, ie added or removed tags
1780        if (TagMode.REPLACE.equals(mode))
1781        {
1782            List<CMSTag> existingTags = page.getTags().stream()
1783                    .map(t -> _tagProvider.getTag(t, contextualParameters))
1784                    .filter(Objects::nonNull)
1785                    .toList();
1786            
1787            tags = new ArrayList<>(CollectionUtils.disjunction(tags, existingTags));
1788        }
1789        
1790        boolean hasPublicTagRight = _rightManager.currentUserHasRight("Web_Rights_Page_Tag", page) == RightResult.RIGHT_ALLOW;
1791        boolean hasPrivateTagRight = _rightManager.currentUserHasRight("Web_Rights_Page_Private_Tag", page) == RightResult.RIGHT_ALLOW;
1792        
1793        // Test if the current user has the right to tag public tag on page only if there are at least one public tag
1794        boolean hasRight = TagHelper.filterTags(tags, TagVisibility.PUBLIC, "PAGE").isEmpty() || hasPublicTagRight || hasPrivateTagRight;
1795        
1796        // Test if the current user has the right to tag private tag on page only if there are at least one private tag
1797        return hasRight && (TagHelper.filterTags(tags, TagVisibility.PRIVATE, "PAGE").isEmpty() || hasPrivateTagRight);
1798    }
1799
1800    /**
1801     * Tag a page
1802     * @param page The page to tag
1803     * @param tagNames The tags
1804     * @param tagMode The mode for updating tags: 'REPLACE' to replace tags, 'INSERT' to add tags or 'REMOVE' to remove tags.
1805     * @return the list of invalid tags that were not set
1806     */
1807    public List<String> tag(ModifiablePage page, List<String> tagNames, TagMode tagMode)
1808    {
1809        List<String> invalidTags = new ArrayList<>();
1810        
1811        Set<String> oldTags = page.getTags();
1812        if (TagMode.REPLACE.equals(tagMode))
1813        {
1814            // First delete old tags
1815            for (String tagName : oldTags)
1816            {
1817                page.untag(tagName);
1818            }
1819        }
1820        
1821        // Then set new tags
1822        for (String tagName : tagNames)
1823        {
1824            if (_isTagValid(page, tagName))
1825            {
1826                if (TagMode.REMOVE.equals(tagMode))
1827                {
1828                    page.untag(tagName);
1829                }
1830                else if (TagMode.REPLACE.equals(tagMode) || !oldTags.contains(tagName))
1831                {
1832                    page.tag(tagName);
1833                }
1834            }
1835            else
1836            {
1837                invalidTags.add(tagName);
1838            }
1839        }
1840        
1841        page.saveChanges();
1842        
1843        if (!oldTags.equals(page.getTags()))
1844        {
1845            // Notify observers that the page has been tagged
1846            Map<String, Object> eventParams = new HashMap<>();
1847            eventParams.put(ObservationConstants.ARGS_PAGE, page);
1848            eventParams.put(ObservationConstants.ARGS_PAGE_TAGS, page.getTags());
1849            eventParams.put(ObservationConstants.ARGS_PAGE_OLD_TAGS, oldTags);
1850            _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_UPDATED, _currentUserProvider.getUser(), eventParams));
1851        }
1852        
1853        return invalidTags;
1854    }
1855    
1856    /**
1857     * Tag a list of contents with the given tags
1858     * @param pageIds The ids of pages to tag
1859     * @param contentIds The ids of contents to tag
1860     * @param tagNames The tags
1861     * @param contextualParameters The contextual parameters
1862     * @return the result map
1863     */
1864    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
1865    public Map<String, Object> tag (List<String> pageIds, List<String> contentIds, List<String> tagNames, Map<String, Object> contextualParameters)
1866    {
1867        return tag(pageIds, contentIds, tagNames, TagMode.REPLACE.toString(), contextualParameters);
1868    }
1869    
1870    /**
1871     * Tag a list of contents and/org pages
1872     * @param pageIds The ids of pages to tag
1873     * @param contentIds The ids of contents to tag
1874     * @param tagNames The tags
1875     * @param mode The mode for updating tags: 'REPLACE' to replace tags, 'INSERT' to add tags or 'REMOVE' to remove tags.
1876     * @param contextualParameters The contextual parameters
1877     * @return the result
1878     */
1879    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
1880    public Map<String, Object> tag (List<String> pageIds, List<String> contentIds, List<String> tagNames, String mode, Map<String, Object> contextualParameters)
1881    {
1882        TagMode tagMode = TagMode.valueOf(mode);
1883        
1884        // Tag pages
1885        Map<String, Object> result = tag(pageIds, tagNames, tagMode, contextualParameters, false);
1886        
1887        // Tag contents
1888        result.putAll(_contentDAO.tag(contentIds, tagNames, tagMode, contextualParameters, false));
1889        
1890        // Invalid tags are ignored
1891        result.remove("invalid-tags");
1892        return result;
1893    }
1894    
1895    /**
1896     * Test if a tag is valid for a specific page
1897     * @param page The page
1898     * @param tagName The tag name
1899     * @return True if the tag is valid
1900     */
1901    public boolean _isTagValid (Page page, String tagName)
1902    {
1903        Map<String, Object> params = new HashMap<>();
1904        params.put("siteName", page.getSiteName());
1905        CMSTag tag = _tagProvider.getTag(tagName, params);
1906        
1907        return tag != null && tag.getTarget().getName().equals("PAGE");
1908    }
1909    
1910    /**
1911     * Returns the ids of the pages matching the tag
1912     * @param sitename The site id
1913     * @param lang The language code
1914     * @param tag The tag id
1915     * @return Array of pages ids
1916     */
1917    public List<String> findPagedIdsByTag(String sitename, String lang, String tag)
1918    {
1919        return _getMemoryPageTagCache().get(PageTagCacheKey.of(sitename, lang, tag), __ -> _computePagesIds(sitename, lang, tag))
1920                    .stream()
1921                    .filter(_resolver::hasAmetysObjectForId) // Cache remember for tag in default, we have to check if page exists in current workspace
1922                    .collect(Collectors.toList());
1923    }
1924    
1925    private List<String> _computePagesIds(String sitename, String lang, String tag)
1926    {
1927        
1928        Session defaultSession = null;
1929        try
1930        {
1931            List<String> pagesIds = new ArrayList<>();
1932            
1933            // Force default workspace to execute query
1934            defaultSession = _repository.login(RepositoryConstants.DEFAULT_WORKSPACE);
1935            
1936            String xpath = PageQueryHelper.getPageXPathQuery(sitename, lang, null, new TagExpression(Operator.EQ, tag), null);
1937            AmetysObjectIterable<Page> pages = _ametysObjectResolver.query(xpath, defaultSession);
1938            Iterator<Page> it = pages.iterator();
1939    
1940            while (it.hasNext())
1941            {
1942                pagesIds.add(it.next().getId());
1943            }
1944            
1945            return pagesIds;
1946        }
1947        catch (RepositoryException e)
1948        {
1949            throw new AmetysRepositoryException(e);
1950        }
1951        finally
1952        {
1953            if (defaultSession != null)
1954            {
1955                defaultSession.logout();
1956            }
1957        }
1958    }
1959
1960    private Cache<PageTagCacheKey, List<String>> _getMemoryPageTagCache()
1961    {
1962        return _cacheManager.get(MEMORY_PAGESTAGCACHE);
1963    }
1964    
1965    /**
1966     * Set the visible of pages
1967     * @param pageIds The id of pages
1968     * @param visible <code>true</code> to set pages as visible, <code>false</code> otherwise
1969     * @return The result map
1970     */
1971    @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION)
1972    public Map<String, Object> setVisibility (List<String> pageIds, boolean visible)
1973    {
1974        Map<String, Object> result = new HashMap<>();
1975        
1976        result.put("nomodifiable-pages", new ArrayList<>());
1977        result.put("noright-pages", new ArrayList<>());
1978        result.put("allright-pages", new ArrayList<>());
1979        result.put("locked-pages", new ArrayList<>());
1980        
1981        for (String id : pageIds)
1982        {
1983            Page page = _resolver.resolveById(id);
1984            
1985            Map<String, Object> page2json = new HashMap<>();
1986            page2json.put("id", page.getId());
1987            page2json.put("title", page.getTitle());
1988            
1989            if (_rightManager.currentUserHasRight("Web_Right_Page_Visibility", page) != RightResult.RIGHT_ALLOW)
1990            {
1991                @SuppressWarnings("unchecked")
1992                List<Map<String, Object>> norightPages = (List<Map<String, Object>>) result.get("noright-pages");
1993                norightPages.add(page2json);
1994            }
1995            else if (page instanceof LockablePage lockablePage && lockablePage.isLocked())
1996            {
1997                @SuppressWarnings("unchecked")
1998                List<Map<String, Object>> lockedPages = (List<Map<String, Object>>) result.get("nomodifiable-pages");
1999                lockedPages.add(page2json);
2000            }
2001            else if (page instanceof ModifiablePage mPage)
2002            {
2003                mPage.setVisible(visible);
2004                mPage.saveChanges();
2005                
2006                Map<String, Object> eventParams = new HashMap<>();
2007                eventParams.put(ObservationConstants.ARGS_PAGE, page);
2008                eventParams.put(ObservationConstants.ARGS_PAGE_ID, page.getId());
2009                _observationManager.notify(new Event(ObservationConstants.EVENT_PAGE_UPDATED, _currentUserProvider.getUser(), eventParams));
2010                
2011                @SuppressWarnings("unchecked")
2012                List<Map<String, Object>> allRightPages = (List<Map<String, Object>>) result.get("allright-pages");
2013                allRightPages.add(page2json);
2014            }
2015            else
2016            {
2017                @SuppressWarnings("unchecked")
2018                List<Map<String, Object>> nomodifiablePages = (List<Map<String, Object>>) result.get("nomodifiable-pages");
2019                nomodifiablePages.add(page2json);
2020            }
2021        }
2022        
2023        return result;
2024    }
2025    
2026    /**
2027     * Get the contents of a {@link Page} and its subpages which can be deleted.
2028     * A content is deleteable if user has right, the content is not locked and not referenced by other pages.
2029     * @param id The id of page
2030     * @return The list of deletable contents
2031     */
2032    @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION)
2033    public Map<String, Object> getDeleteablePageContents (String id)
2034    {
2035        Map<String, Object> results = new HashMap<>();
2036        
2037        results.put("deleteable-contents", new ArrayList<>());
2038        results.put("referenced-contents", new ArrayList<>());
2039        results.put("unauthorized-contents", new ArrayList<>());
2040        results.put("locked-contents", new ArrayList<>());
2041        
2042        Page page = _resolver.resolveById(id);
2043        PageHierarchy pageHierarchy = _getPageContents(page, true);
2044        
2045        for (Content content : pageHierarchy.contents())
2046        {
2047            Map<String, Object> contentParams = new HashMap<>();
2048            contentParams.put("id", content.getId());
2049            contentParams.put("title", content.getTitle(LocaleUtils.toLocale(page.getSitemapName())));
2050            contentParams.put("name", content.getName());
2051            
2052            if (_isReferenced(content, pageHierarchy.pages()))
2053            {
2054                // Content is referenced by at least another page
2055                @SuppressWarnings("unchecked")
2056                List<Map<String, Object>> referencedContents = (List<Map<String, Object>>) results.get("referenced-contents");
2057                referencedContents.add(contentParams);
2058            }
2059            else if (!_contentDAO.canDelete(content))
2060            {
2061                @SuppressWarnings("unchecked")
2062                List<Map<String, Object>> unauthorizedContents = (List<Map<String, Object>>) results.get("unauthorized-contents");
2063                unauthorizedContents.add(contentParams);
2064            }
2065            else if (_isLocked(content))
2066            {
2067                // If the content is locked by other
2068                @SuppressWarnings("unchecked")
2069                List<Map<String, Object>> lockedContents = (List<Map<String, Object>>) results.get("locked-contents");
2070                lockedContents.add(contentParams);
2071            }
2072            else
2073            {
2074                Map<String, Object> content2json = new HashMap<>();
2075                content2json.put("id", content.getId());
2076                content2json.put("name", content.getName());
2077                content2json.put("title", content.getTitle(LocaleUtils.toLocale(page.getSitemapName())));
2078                content2json.put("isNew", _isNew(content));
2079                content2json.put("isShared", content instanceof SharedContent);
2080                content2json.put("hasShared", _sharedContentManager.hasSharedContents(content));
2081                
2082                @SuppressWarnings("unchecked")
2083                List<Map<String, Object>> allrightContents = (List<Map<String, Object>>) results.get("deleteable-contents");
2084                allrightContents.add(content2json);
2085            }
2086        }
2087        
2088        return results;
2089    }
2090    
2091    /**
2092     * Get the contents that belong to the {@link Page} and its sub-pages and that can be deleted.
2093     * A content is deleteable if user has right, the content is not locked and it's not referenced by other pages.
2094     * If 'onlyNewlyCreatedContents' is set to 'true', only newly created contents will be returned
2095     * @param pageHierarchy Object containing the page and its sub pages, and all referencing contents in these pages
2096     * @param onlyNewlyCreatedContents true to return only the newly created contents
2097     * @return The ids of deleteable contents
2098     */
2099    private List<String> _getDeleteablePageContentIds (PageHierarchy pageHierarchy, boolean onlyNewlyCreatedContents)
2100    {
2101        return pageHierarchy.contents()
2102                .stream()
2103                .filter(_contentDAO::canDelete)
2104                .filter(Predicate.not(this::_isLocked))
2105                .filter(c -> !_isReferenced(c, pageHierarchy.pages()))
2106                .filter(c -> !onlyNewlyCreatedContents || _isNew(c))
2107                .map(Content::getId)
2108                .toList();
2109    }
2110    
2111    /**
2112     * Returns the page's attachments root node
2113     * @param id the page's id
2114     * @return The attachments' root node informations
2115     */
2116    @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION)
2117    public Map<String, Object> getAttachmentsRootNode (String id)
2118    {
2119        Map<String, Object> result = new HashMap<>();
2120        
2121        Page page = _resolver.resolveById(id);
2122        
2123        result.put("title", page.getTitle());
2124        result.put("pageId", page.getId());
2125        
2126        if (_rightManager.currentUserHasRight("Web_Rights_Page_Attachments", page) != RightResult.RIGHT_ALLOW
2127            && _rightManager.currentUserHasRight("Web_Rights_Page_AttachmentHandle", page) != RightResult.RIGHT_ALLOW)
2128        {
2129            return result;
2130        }
2131        
2132        TraversableAmetysObject attachments = page.getRootAttachments();
2133        
2134        if (attachments != null)
2135        {
2136            result.put("id", attachments.getId());
2137            if (attachments instanceof ModifiableAmetysObject)
2138            {
2139                result.put("isModifiable", true);
2140            }
2141            if (attachments instanceof ModifiableResourceCollection)
2142            {
2143                result.put("canCreateChild", true);
2144            }
2145            
2146            boolean hasChildNodes = false;
2147            boolean hasResources = false;
2148
2149            for (AmetysObject child : attachments.getChildren())
2150            {
2151                if (child instanceof Resource)
2152                {
2153                    hasResources = true;
2154                }
2155                else if (child instanceof ExplorerNode)
2156                {
2157                    hasChildNodes = true;
2158                }
2159            }
2160
2161            if (hasChildNodes)
2162            {
2163                result.put("hasChildNodes", true);
2164            }
2165
2166            if (hasResources)
2167            {
2168                result.put("hasResources", true);
2169            }
2170            
2171            return result;
2172        }
2173        
2174        throw new IllegalArgumentException("Page with id '" + id + "' does not support attachments.");
2175    }
2176    /**
2177     * Returns the page's parents ids
2178     * @param id the page's id
2179     * @return The attachments' root node informations
2180     */
2181    @Callable (rights = Callable.READ_ACCESS, rightContext = PageRightAssignmentContext.ID, paramIndex = 0)
2182    public Map<String, Object> getPageParents (String id)
2183    {
2184        Map<String, Object> result = new HashMap<>();
2185        List<Map<String, Object>> pages = new ArrayList<>();
2186        Page page = _resolver.resolveById(id);
2187        pages.add(_page2Json(page));
2188        while (page.getParent() != null && page.getParent() instanceof Page)
2189        {
2190            page = page.getParent();
2191            pages.add(_page2Json(page));
2192        }
2193        result.put("parents", pages);
2194        return result;
2195    }
2196    
2197    /**
2198     * Get the contents of a page and its child pages
2199     * @param sitemapElement The page
2200     * @return The list of contents
2201     */
2202    public Set<Content> getPageContents (SitemapElement sitemapElement)
2203    {
2204        return _getPageContents(sitemapElement, false).contents();
2205    }
2206    
2207    /**
2208     * Get the contents of a page and its child pages
2209     * @param sitemapElement The page
2210     * @param ignoreContentsOfNonRemovablePage true to ignore contents of non-removable pages (virtual pages)
2211     * @return The page hierarchy with all pages, sub pages and referencing contents
2212     */
2213    private PageHierarchy _getPageContents (SitemapElement sitemapElement, boolean ignoreContentsOfNonRemovablePage)
2214    {
2215        Set<Content> contents = new HashSet<>();
2216        
2217        if ((!ignoreContentsOfNonRemovablePage || sitemapElement instanceof RemovableAmetysObject)
2218                && sitemapElement.getTemplate() != null)
2219        {
2220            for (Zone zone : sitemapElement.getZones())
2221            {
2222                try (AmetysObjectIterable< ? extends ZoneItem> zoneItems = zone.getZoneItems())
2223                {
2224                    for (ZoneItem zoneItem : zoneItems)
2225                    {
2226                        if (zoneItem.getType() == ZoneItem.ZoneType.CONTENT)
2227                        {
2228                            contents.add(zoneItem.getContent());
2229                        }
2230                    }
2231                }
2232            }
2233        }
2234        
2235        Set<Page> pages = new HashSet<>();
2236        if (sitemapElement instanceof Page page)
2237        {
2238            pages.add(page);
2239        }
2240        
2241        AmetysObjectIterable< ? extends Page> childrenPages = sitemapElement.getChildrenPages();
2242        for (Page childPage : childrenPages)
2243        {
2244            PageHierarchy pageHierarchy = _getPageContents(childPage, ignoreContentsOfNonRemovablePage);
2245            pages.addAll(pageHierarchy.pages());
2246            contents.addAll(pageHierarchy.contents());
2247        }
2248        
2249        return new PageHierarchy(pages, contents);
2250    }
2251    
2252    /**
2253     * Get the user rights on sitemap element (page or sitemap)
2254     * @param pagesCt The sitemap element
2255     * @return The user's rights
2256     */
2257    protected Set<String> getUserRights (SitemapElement pagesCt)
2258    {
2259        UserIdentity user = _currentUserProvider.getUser();
2260        
2261        Set<String> userRights = _rightManager.getUserRights(user, pagesCt);
2262        
2263        userRights = new HashSet<>(userRights);
2264        // Do some specific stuff here, because the right 'Web_Rights_Page_Delete' is a right to delete child pages and not the page itself.
2265        // So the right should be checked on parent context.
2266        if (pagesCt instanceof Page)
2267        {
2268            SitemapElement parent = pagesCt.getParent();
2269            boolean canDelete = _rightManager.hasRight(user, "Web_Rights_Page_Delete", parent) == RightResult.RIGHT_ALLOW;
2270            if (!canDelete)
2271            {
2272                // No right on parent page, so remove the right if exists.
2273                userRights.remove("Web_Rights_Page_Delete");
2274            }
2275        }
2276        else
2277        {
2278            // There is no right of deletion on the sitemap
2279            userRights.remove("Web_Rights_Page_Delete");
2280        }
2281        
2282        // include the read access in the right list for convenience
2283        // use the reader profile Id even if its not an actual right id
2284        // nobody should ever defined a right with id "READER"
2285        if (_rightManager.hasReadAccess(user, pagesCt))
2286        {
2287            userRights.add(RightManager.READER_PROFILE_ID);
2288        }
2289        
2290        return userRights;
2291    }
2292    
2293    private boolean _isReferenced (Content content, Set<Page> pages)
2294    {
2295        if (content instanceof WebContent webContent)
2296        {
2297            // If only one page referencing the content is not inside the selected page hierarchy, it is a referenced content
2298            return webContent.getReferencingPages().stream().anyMatch(Predicate.not(pages::contains));
2299        }
2300        
2301        return false;
2302    }
2303    
2304    private boolean _isLocked (Content content)
2305    {
2306        if (content instanceof LockableAmetysObject lockableContent)
2307        {
2308            if (lockableContent.isLocked())
2309            {
2310                boolean canUnlockAll = _rightManager.hasRight(_currentUserProvider.getUser(), "CMS_Rights_UnlockAll", "/cms") == RightResult.RIGHT_ALLOW;
2311                if (!LockHelper.isLockOwner(lockableContent, _currentUserProvider.getUser()) && !canUnlockAll)
2312                {
2313                    return true;
2314                }
2315            }
2316        }
2317        
2318        return false;
2319    }
2320    
2321    private boolean _isNew (Content content)
2322    {
2323        boolean isNew = false;
2324        if (content instanceof WorkflowAwareContent waContent)
2325        {
2326            long workflowId = waContent.getWorkflowId();
2327            
2328            AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent);
2329            isNew = workflow.getHistorySteps(workflowId).isEmpty();
2330        }
2331        return isNew;
2332    }
2333
2334    private Map<String, Object> _page2Json (Page page)
2335    {
2336        Map<String, Object> page2json = new HashMap<>();
2337        page2json.put("id", page.getId());
2338        page2json.put("title", page.getTitle());
2339        page2json.put("siteName", page.getSiteName());
2340        page2json.put("path", page.getPathInSitemap());
2341        return page2json;
2342    }
2343    
2344    private Map<String, Object> _publication2Json (Page page)
2345    {
2346        Map<String, Object> pub2json = new HashMap<>();
2347        
2348        if (page.hasValue(DefaultPage.METADATA_PUBLICATION_START_DATE, ModelItemTypeConstants.DATETIME_TYPE_ID))
2349        {
2350            Object startDateAsJSON = page.dataToJSON(DefaultPage.METADATA_PUBLICATION_START_DATE);
2351            pub2json.put("startDate", startDateAsJSON);
2352        }
2353        
2354        if (page.hasValue(DefaultPage.METADATA_PUBLICATION_END_DATE, ModelItemTypeConstants.DATETIME_TYPE_ID))
2355        {
2356            Object endDateAsJSON = page.dataToJSON(DefaultPage.METADATA_PUBLICATION_END_DATE);
2357            pub2json.put("endDate", endDateAsJSON);
2358        }
2359        
2360        return pub2json;
2361        
2362    }
2363    
2364    private void _updateContentsAfterCopy(Page initialPage, Page createdPage) throws AmetysRepositoryException
2365    {
2366        for (Zone zone : createdPage.getZones())
2367        {
2368            Zone initialZone = initialPage.getZone(zone.getName());
2369            try (AmetysObjectIterable< ? extends ZoneItem> zoneItems = zone.getZoneItems(); AmetysObjectIterable< ? extends ZoneItem> initialZoneItems = initialZone.getZoneItems())
2370            {
2371                AmetysObjectIterator<? extends ZoneItem> iterator = zoneItems.iterator();
2372                AmetysObjectIterator< ? extends ZoneItem> initialIterator = initialZoneItems.iterator();
2373                if (iterator.getSize() != initialIterator.getSize())
2374                {
2375                    throw new IllegalStateException("An error occured during the copy of " + initialPage.getName() + " (" + initialPage.getId() + "). The resulting page have a different number of zoneItems.");
2376                }
2377                while (iterator.hasNext())
2378                {
2379                    ZoneItem zoneItem = iterator.next();
2380                    ZoneItem initialZoneItem = initialIterator.next();
2381                    if (zoneItem.getType().equals(ZoneType.CONTENT))
2382                    {
2383                        Content content = zoneItem.getContent();
2384                        WebContent initialContent = initialZoneItem.getContent();
2385                        
2386                        // Updaters
2387                        Set<String> ids = _copyUpdaterEP.getExtensionsIds();
2388                        for (String id : ids)
2389                        {
2390                            _copyUpdaterEP.getExtension(id).updateContent(initialPage.getSite(), createdPage.getSite(), initialContent, content);
2391                        }
2392                        
2393                        // Convert content language if necessary
2394                        if (content instanceof ModifiableContent modifiableContent && content.getLanguage() != null && content.getLanguage() != createdPage.getSitemapName())
2395                        {
2396                            modifiableContent.setLanguage(createdPage.getSitemapName());
2397                            modifiableContent.saveChanges();
2398                        }
2399                        
2400                        // Create the first version
2401                        if (content instanceof VersionableAmetysObject versionableContent)
2402                        {
2403                            versionableContent.checkpoint();
2404                        }
2405                    }
2406                }
2407            }
2408        }
2409        
2410        // Browse child pages
2411        for (Page childPage : createdPage.getChildrenPages())
2412        {
2413            Page initialChildPage = initialPage.getChild(childPage.getName());
2414            _updateContentsAfterCopy (initialChildPage, childPage);
2415        }
2416    }
2417    
2418    private List<String> _getChildrenPageIds (Page page)
2419    {
2420        List<String> childIds = new ArrayList<>();
2421        
2422        for (Page childPage : page.getChildrenPages())
2423        {
2424            childIds.add(childPage.getId());
2425            childIds.addAll(_getChildrenPageIds(childPage));
2426        }
2427        
2428        return childIds;
2429    }
2430
2431    /**
2432     * Check each parent and return true if one of them is invisible
2433     * @param page page to check
2434     * @return true if at least one parent is invisible
2435     */
2436    private boolean _isParentInvisible (Page page)
2437    {
2438        AmetysObject parent = page.getParent();
2439        while (parent != null && parent instanceof Page parentPage)
2440        {
2441            if (!parentPage.isVisible())
2442            {
2443                return true;
2444            }
2445            parent = parent.getParent();
2446        }
2447        return false;
2448    }
2449
2450    /* start of a group of methods for _isPreviewable */
2451    /**
2452     * Determine if this page is previewable
2453     * @param page The page to look at
2454     * @return true if the page can be previewed
2455     */
2456    private boolean _isPreviewable(Page page)
2457    {
2458        // Check for infinitive loop redirection
2459        ArrayList<String> pagesSequence = new ArrayList<>();
2460        pagesSequence.add(page.getId());
2461        if (_isInfiniteRedirection (page, pagesSequence))
2462        {
2463            getLogger().error("An infinite loop redirection was detected for page '" + page.getPathInSitemap() + "'");
2464            return false;
2465        }
2466
2467        if (page.getType() == PageType.LINK && LinkType.PAGE.equals(page.getURLType()))
2468        {
2469            return _isPageExist(page.getURL()) && _isPreviewable((Page) _resolver.resolveById(page.getURL()));
2470        }
2471
2472        if (page.getType() != PageType.NODE)
2473        {
2474            return true;
2475        }
2476        else
2477        {
2478            try (AmetysObjectIterable< ? extends Page> childrenPages = page.getChildrenPages())
2479            {
2480                for (Page subPage : childrenPages)
2481                {
2482                    if (_isPreviewable(subPage))
2483                    {
2484                        return true;
2485                    }
2486                }
2487            }
2488        }
2489        return false;
2490    }
2491
2492    private boolean _isPageExist (String id)
2493    {
2494        try
2495        {
2496            _resolver.resolveById(id);
2497            return true;
2498        }
2499        catch (UnknownAmetysObjectException e)
2500        {
2501            return false;
2502        }
2503    }
2504
2505    private boolean _isInfiniteRedirection (Page page, List<String> pagesSequence)
2506    {
2507        Page redirectPage = _getPageRedirection (page);
2508        if (redirectPage == null)
2509        {
2510            return false;
2511        }
2512
2513        if (pagesSequence.contains(redirectPage.getId()))
2514        {
2515            return true;
2516        }
2517
2518        pagesSequence.add(redirectPage.getId());
2519        return _isInfiniteRedirection (redirectPage, pagesSequence);
2520    }
2521
2522    private Page _getPageRedirection (Page page)
2523    {
2524        if (PageType.LINK.equals(page.getType()) && LinkType.PAGE.equals(page.getURLType()))
2525        {
2526            try
2527            {
2528                String pageId = page.getURL();
2529                return _resolver.resolveById(pageId);
2530            }
2531            catch (AmetysRepositoryException e)
2532            {
2533                return null;
2534            }
2535        }
2536        else if (PageType.NODE.equals(page.getType()))
2537        {
2538            AmetysObjectIterable<? extends Page> childPages = page.getChildrenPages();
2539            Iterator<? extends Page> it = childPages.iterator();
2540            if (it.hasNext())
2541            {
2542                return it.next();
2543            }
2544        }
2545
2546        return null;
2547    }
2548    /* end of a group of methods for _isPreviewable */
2549    
2550    private static final class PageTagCacheKey extends AbstractCacheKey
2551    {
2552        private PageTagCacheKey(String sitename, String lang, String tag)
2553        {
2554            super(sitename, lang, tag);
2555        }
2556        
2557        private PageTagCacheKey(String sitename, String lang)
2558        {
2559            super(sitename, lang);
2560        }
2561
2562        static PageTagCacheKey of(String sitename, String lang)
2563        {
2564            return new PageTagCacheKey(sitename, lang, null);
2565        }
2566
2567        static PageTagCacheKey of(String sitename, String lang, String tag)
2568        {
2569            return new PageTagCacheKey(sitename, lang, tag);
2570        }
2571    }
2572    
2573    private record PageHierarchy(Set<Page> pages, Set<Content> contents) { /* empty */ }
2574}