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