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