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