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