001/*
002 *  Copyright 2017 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.plugins.frontedition;
017
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.Comparator;
023import java.util.HashMap;
024import java.util.LinkedHashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.Set;
028import java.util.stream.Collectors;
029
030import javax.jcr.RepositoryException;
031
032import org.apache.avalon.framework.configuration.ConfigurationException;
033import org.apache.avalon.framework.context.Context;
034import org.apache.avalon.framework.context.ContextException;
035import org.apache.avalon.framework.context.Contextualizable;
036import org.apache.avalon.framework.logger.LogEnabled;
037import org.apache.avalon.framework.logger.Logger;
038import org.apache.avalon.framework.service.ServiceException;
039import org.apache.avalon.framework.service.ServiceManager;
040import org.apache.avalon.framework.service.Serviceable;
041import org.apache.cocoon.components.ContextHelper;
042import org.apache.cocoon.environment.Request;
043import org.apache.commons.lang3.ArrayUtils;
044import org.apache.commons.lang3.StringUtils;
045import org.apache.commons.lang3.tuple.Pair;
046import org.w3c.dom.NodeList;
047
048import org.ametys.cms.CmsConstants;
049import org.ametys.cms.content.RootContentHelper;
050import org.ametys.cms.contenttype.AttributeDefinition;
051import org.ametys.cms.contenttype.ContentType;
052import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
053import org.ametys.cms.contenttype.ContentTypesHelper;
054import org.ametys.cms.model.restrictions.RestrictedModelItem;
055import org.ametys.cms.repository.Content;
056import org.ametys.cms.repository.WorkflowAwareContent;
057import org.ametys.cms.workflow.ContentWorkflowHelper;
058import org.ametys.core.right.RightManager;
059import org.ametys.core.right.RightManager.RightResult;
060import org.ametys.core.ui.ClientSideElement;
061import org.ametys.core.ui.ClientSideElement.Script;
062import org.ametys.core.ui.ClientSideElement.ScriptFile;
063import org.ametys.core.ui.widgets.richtext.RichTextConfiguration;
064import org.ametys.core.ui.widgets.richtext.RichTextConfigurationExtensionPoint;
065import org.ametys.core.util.I18nizableSerializer;
066import org.ametys.core.util.JSONUtils;
067import org.ametys.plugins.core.ui.minimize.HashCache;
068import org.ametys.plugins.explorer.resources.actions.ExplorerResourcesDAO;
069import org.ametys.plugins.repository.AmetysObject;
070import org.ametys.plugins.repository.AmetysObjectIterable;
071import org.ametys.plugins.repository.AmetysObjectResolver;
072import org.ametys.plugins.repository.AmetysRepositoryException;
073import org.ametys.plugins.repository.RepositoryConstants;
074import org.ametys.plugins.repository.UnknownAmetysObjectException;
075import org.ametys.plugins.repository.jcr.DefaultTraversableAmetysObject;
076import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector;
077import org.ametys.plugins.repository.version.VersionableAmetysObject;
078import org.ametys.plugins.workflow.support.WorkflowHelper;
079import org.ametys.plugins.workflow.support.WorkflowProvider;
080import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow;
081import org.ametys.runtime.i18n.I18nizableText;
082import org.ametys.runtime.model.ModelItem;
083import org.ametys.runtime.model.ModelItemGroup;
084import org.ametys.runtime.plugin.PluginsManager;
085import org.ametys.web.cache.PageHelper;
086import org.ametys.web.renderingcontext.RenderingContext;
087import org.ametys.web.repository.page.ModifiablePage;
088import org.ametys.web.repository.page.Page;
089import org.ametys.web.repository.page.PagesContainer;
090import org.ametys.web.repository.page.Zone;
091import org.ametys.web.repository.page.ZoneItem;
092import org.ametys.web.repository.site.Site;
093import org.ametys.web.repository.site.SiteManager;
094import org.ametys.web.repository.sitemap.Sitemap;
095import org.ametys.web.transformation.xslt.AmetysXSLTHelper;
096
097import com.opensymphony.workflow.loader.ActionDescriptor;
098import com.opensymphony.workflow.loader.StepDescriptor;
099import com.opensymphony.workflow.loader.WorkflowDescriptor;
100import com.opensymphony.workflow.spi.Step;
101
102/**
103 * Helper for preparing the Ametys Edition
104 */
105public class AmetysFrontEditionHelper implements Serviceable, Contextualizable, LogEnabled
106{
107    /** The right's id from front-edition access */
108    public static final String FRONT_EDITION_RIGHT_ID = "Front_Edition_Access_Right";
109    
110    private static AmetysObjectResolver _ametysObjectResolver;
111    private static HashCache _hashCache;
112    private static RichTextConfigurationExtensionPoint _richTextConfigurationExtensionPoint;
113    private static RightManager _rightManager;
114    private static ContentWorkflowHelper _contentWorkflowHelper;
115    private static PageHelper _pageHelper;
116    private static WorkflowHelper _workflowHelper;
117    private static JSONUtils _jsonUtils;
118    private static SiteManager _siteManager;
119    private static WorkflowProvider _workflowProvider;
120    private static ContentTypesHelper _contentTypesHelper;
121    private static ContentTypeExtensionPoint _contentTypesEP;
122    private static RootContentHelper _rootContentHelper;
123    
124    private static Context _context;
125
126    private static Logger _logger;
127
128    public void service(ServiceManager manager) throws ServiceException
129    {
130        _ametysObjectResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
131        _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE);
132        _hashCache = (HashCache) manager.lookup(HashCache.ROLE);
133        _richTextConfigurationExtensionPoint = (RichTextConfigurationExtensionPoint) manager.lookup(RichTextConfigurationExtensionPoint.ROLE);
134        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
135        _contentWorkflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE);
136        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
137        _contentTypesEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
138        _pageHelper = (PageHelper) manager.lookup(PageHelper.ROLE);
139        _workflowHelper = (WorkflowHelper) manager.lookup(WorkflowHelper.ROLE);
140        _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE);
141        _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE);
142        _rootContentHelper = (RootContentHelper) manager.lookup(RootContentHelper.ROLE);
143    }
144    
145    @Override
146    public void contextualize(Context context) throws ContextException
147    {
148        _context = context;
149    }
150    
151    @Override
152    public void enableLogging(Logger logger)
153    {
154        _logger = logger;
155    }
156    
157    /**
158     * Prepare a hashcode for js files
159     * @param locale The language code to use
160     * @param rtlMode Is for right-to-left language?
161     * @param theme The url for the theme
162     * @return The new hashcode to read the minimified concatened file
163     */
164    public static String prepareJSFiles(String locale, boolean rtlMode, String theme)
165    {
166        String themeName = StringUtils.substringAfterLast(theme, "/");
167        
168        Map<String, String> filesInfos = new HashMap<>();
169        filesInfos.put("media", "");
170        filesInfos.put("tag", "script");
171        
172        Map<String, Map<String, String>> files = new LinkedHashMap<>();
173        files.put("/plugins/extjs7/resources/ext-all.js", filesInfos);
174        files.put("/plugins/extjs7/resources/classic/locale/locale-" + locale + ".js", filesInfos);
175        files.put(theme + "/" + themeName + ".js", filesInfos);
176
177        files.put("/plugins/core-ui/resources/js/Ext.fixes." + locale + ".js", filesInfos);
178        files.put("/plugins/core-ui/resources/js/Ext.enhancements." + locale + ".js", filesInfos);
179        
180        files.put("/plugins/core-ui/resources/js/Ametys." + locale + ".js", filesInfos);
181        files.put("/plugins/front-edition/resources/js/Ametys/override/Ametys." + locale + ".js", filesInfos);
182        files.put("/plugins/core-ui/resources/js/Ametys/mask/GlobalLoadMask." + locale + ".js", filesInfos);
183        
184        files.put("/plugins/core-ui/resources/js/Ametys/log/Logger." + locale + ".js", filesInfos);
185        files.put("/plugins/core-ui/resources/js/Ametys/log/Logger/Entry." + locale + ".js", filesInfos);
186        files.put("/plugins/core-ui/resources/js/Ametys/log/LoggerFactory." + locale + ".js", filesInfos);
187        files.put("/plugins/core-ui/resources/js/Ametys/log/ErrorDialog." + locale + ".js", filesInfos);
188        
189        files.put("/plugins/core-ui/resources/js/Ametys/window/DialogBox." + locale + ".js", filesInfos);
190        files.put("/plugins/front-edition/resources/js/Ametys/override/Ametys/window/DialogBox." + locale + ".js", filesInfos);
191        files.put("/plugins/core-ui/resources/js/Ametys/window/MessageBox." + locale + ".js", filesInfos);
192        
193        files.put("/plugins/core-ui/resources/js/Ametys/form/AbstractField." + locale + ".js", filesInfos);
194        files.put("/plugins/core-ui/resources/js/Ametys/form/AbstractFieldsWrapper." + locale + ".js", filesInfos);
195        files.put("/plugins/core-ui/resources/js/Ametys/form/AbstractQueryableComboBox." + locale + ".js", filesInfos);
196        files.put("/plugins/core-ui/resources/js/Ametys/form/AbstractQueryableComboBox/SplitterTracker." + locale + ".js", filesInfos);
197        files.put("/plugins/front-edition/resources/js/Ametys/override/Ametys/form/widget/AbstractQueryableComboBox." + locale + ".js", filesInfos);
198        files.put("/plugins/core-ui/resources/js/Ametys/form/field/DateTime." + locale + ".js", filesInfos);
199        files.put("/plugins/core-ui/resources/js/Ametys/form/field/StringTime." + locale + ".js", filesInfos);
200        files.put("/plugins/core-ui/resources/js/Ametys/form/field/Password." + locale + ".js", filesInfos);
201        files.put("/plugins/core-ui/resources/js/Ametys/form/field/ChangePassword." + locale + ".js", filesInfos);
202        files.put("/plugins/core-ui/resources/js/Ametys/form/field/ReferencedNumberField." + locale + ".js", filesInfos);
203        files.put("/plugins/core-ui/resources/js/Ametys/form/field/RichText." + locale + ".js", filesInfos);
204        files.put("/plugins/cms/resources/js/Ametys/cms/form/field/RichText." + locale + ".js", filesInfos);
205        files.put("/plugins/core-ui/resources/js/Ametys/form/field/RichText/SplitterTracker." + locale + ".js", filesInfos);
206        files.put("/plugins/core-ui/resources/js/Ametys/form/field/TextArea." + locale + ".js", filesInfos);
207        files.put("/plugins/core-ui/resources/js/Ametys/form/field/ColorSelector." + locale + ".js", filesInfos);
208        files.put("/plugins/core-ui/resources/js/Ametys/form/field/SelectUserDirectory." + locale + ".js", filesInfos);
209        files.put("/plugins/core-ui/resources/js/Ametys/form/field/SelectGroupDirectories." + locale + ".js", filesInfos);
210        files.put("/plugins/core-ui/resources/js/Ametys/form/field/Code." + locale + ".js", filesInfos);
211        files.put("/plugins/tiny_mce/resources/js/tinymce.js", filesInfos);
212        files.put("/plugins/codemirror/resources/js/codemirror.js", filesInfos);
213        files.put("/plugins/codemirror/resources/js/addon/edit/matchbrackets.js", filesInfos);
214        files.put("/plugins/codemirror/resources/js/addon/selection/active-line.js", filesInfos);
215        files.put("/plugins/codemirror/resources/js/mode/xml/xml.js", filesInfos);
216        files.put("/plugins/codemirror/resources/js/mode/javascript/javascript.js", filesInfos);
217        files.put("/plugins/codemirror/resources/js/mode/css/css.js", filesInfos);
218        files.put("/plugins/codemirror/resources/js/mode/htmlmixed/htmlmixed.js", filesInfos);
219        
220        files.put("/plugins/core-ui/resources/js/Ametys/data/ServerCaller." + locale + ".js", filesInfos);
221        files.put("/plugins/core-ui/resources/js/Ametys/data/ServerComm." + locale + ".js", filesInfos);
222        files.put("/plugins/front-edition/resources/js/Ametys/override/Ametys/data/ServerComm." + locale + ".js", filesInfos);
223        files.put("/plugins/core-ui/resources/js/Ametys/data/ServerCommProxy." + locale + ".js", filesInfos);
224        files.put("/plugins/core-ui/resources/js/Ametys/data/ServerComm/TimeoutDialog." + locale + ".js", filesInfos);
225
226        files.put("/plugins/core-ui/resources/js/Ametys/form/widget/RichText/RichTextConfiguration." + locale + ".js", filesInfos);
227        
228        files.put("/plugins/core-ui/resources/js/Ametys/plugins/coreui/system/devmode/StacktraceHelper." + locale + ".js", filesInfos);
229
230        files.put("/plugins/front-edition/resources/js/front-comm." + locale + ".js", filesInfos);
231        files.put("/plugins/front-edition/resources/js/front-widget." + locale + ".js", filesInfos);
232//        files.put("/plugins/front-edition/resources/js/front-page." + locale + ".js", filesInfos);
233        files.put("/plugins/core-ui/resources/js/Ametys/helper/EnterURL." + locale + ".js", filesInfos);
234        files.put("/plugins/core-ui/resources/js/Ametys/helper/FileUpload." + locale + ".js", filesInfos);
235        files.put("/plugins/web/resources/js/Ametys/web/helper/ChoosePage." + locale + ".js", filesInfos);
236        files.put("/plugins/web/resources/js/Ametys/web/helper/ContextToolbar." + locale + ".js", filesInfos);
237        files.put("/plugins/web/resources/js/Ametys/web/sitemap/SitemapTree." + locale + ".js", filesInfos);
238        files.put("/plugins/web/resources/js/Ametys/web/sitemap/SitemapTree/Page." + locale + ".js", filesInfos);
239        files.put("/plugins/web/resources/js/Ametys/web/sitemap/SitemapTree/Sitemap." + locale + ".js", filesInfos);
240        files.put("/plugins/web/resources/js/Ametys/web/site/SitesTree/Site." + locale + ".js", filesInfos);
241        files.put("/plugins/cms/resources/js/Ametys/cms/editor/LinkHandler." + locale + ".js", filesInfos);
242        files.put("/plugins/cms/resources/js/Ametys/plugins/cms/editor/Links." + locale + ".js", filesInfos);
243        files.put("/plugins/cms/resources/js/Ametys/plugins/cms/editor/Links/LinkHandler." + locale + ".js", filesInfos);
244        files.put("/plugins/web/resources/js/Ametys/plugins/web/editor/Links." + locale + ".js", filesInfos);
245        files.put("/plugins/web/resources/js/Ametys/plugins/web/editor/PageLinkHandler." + locale + ".js", filesInfos);
246        files.put("/plugins/cms/resources/js/Ametys/plugins/cms/editor/Images." + locale + ".js", filesInfos);
247        
248        files.put("/plugins/cms/resources/js/tinymce/lists." + locale + ".js", filesInfos);
249        files.put("/plugins/cms/resources/js/tinymce/links." + locale + ".js", filesInfos);
250        files.put("/plugins/web/resources/js/tinymce/links." + locale + ".js", filesInfos);
251        files.put("/plugins/front-edition/resources/js/tinymce/links." + locale + ".js", filesInfos);
252        files.put("/plugins/cms/resources/js/tinymce/styles." + locale + ".js", filesInfos);
253        files.put("/plugins/cms/resources/js/tinymce/images." + locale + ".js", filesInfos);
254        files.put("/plugins/front-edition/resources/js/tinymce/images." + locale + ".js", filesInfos);
255        
256        if (PluginsManager.getInstance().getPluginNames().contains("inlinemedia"))
257        {
258            
259            files.put("/plugins/inlinemedia/resources/js/Ametys/plugins/inlinemedia/Media." + locale + ".js", filesInfos);
260            files.put("/plugins/inlinemedia/resources/js/Ametys/plugins/inlinemedia/InsertMediaHelper." + locale + ".js", filesInfos);
261            files.put("/plugins/inlinemedia/resources/js/tinymce/images." + locale + ".js", filesInfos);
262        }
263        
264        files.put("/plugins/front-edition/resources/js/tinymce/save." + locale + ".js", filesInfos);
265        
266        //TinyMCE Tables
267        files.put("/plugins/cms/resources/js/tinymce/tables." + locale + ".js", filesInfos);
268        files.put("/plugins/front-edition/resources/js/tinymce/tables." + locale + ".js", filesInfos);
269        files.put("/plugins/cms/resources/js/Ametys/plugins/cms/editor/Tables." + locale + ".js", filesInfos);
270        files.put("/plugins/cms/resources/js/Ametys/plugins/cms/editor/BasicActions." + locale + ".js", filesInfos);
271        //TinyMCE Images
272        files.put("/plugins/cms/resources/js/Ametys/cms/uihelper/ChooseResource." + locale + ".js", filesInfos);
273        files.put("/plugins/explorer/resources/js/Ametys/explorer/tree/ExplorerTree." + locale + ".js", filesInfos);
274        files.put("/plugins/explorer/resources/js/Ametys/explorer/tree/ExplorerTree/NodeEntry." + locale + ".js", filesInfos);
275        files.put("/plugins/explorer/resources/js/Ametys/explorer/Resource." + locale + ".js", filesInfos);
276        files.put("/plugins/explorer/resources/js/Ametys/explorer/ExplorerNodeDAO." + locale + ".js", filesInfos);
277        files.put("/plugins/cms/resources/js/Ametys/cms/attach/AttachmentsExplorerTree." + locale + ".js", filesInfos);
278        files.put("/plugins/cms/resources/js/Ametys/cms/uihelper/ChooseAttachmentFile." + locale + ".js", filesInfos);
279        files.put("/plugins/front-edition/resources/js/Ametys/override/Ametys/cms/uihelper/ChooseAttachmentFile." + locale + ".js", filesInfos);
280        files.put("/plugins/explorer/resources/js/Ametys/explorer/resources/actions/FileActions." + locale + ".js", filesInfos);
281        files.put("/plugins/explorer/resources/js/Ametys/explorer/resources/helper/ResourceUpload." + locale + ".js", filesInfos);
282        files.put("/plugins/core-ui/resources/js/Ametys/file/AbstractFileExplorerTree/FileNode." + locale + ".js", filesInfos);
283        files.put("/plugins/core-ui/resources/js/Ametys/file/AbstractFileExplorerTree." + locale + ".js", filesInfos);
284
285        //Empty Message class
286        files.put("/plugins/core-ui/resources/js/Ametys/message/Message." + locale + ".js", filesInfos);
287        files.put("/plugins/core-ui/resources/js/Ametys/message/MessageTarget." + locale + ".js", filesInfos);
288        files.put("/plugins/core-ui/resources/js/Ametys/message/MessageTargetFactory." + locale + ".js", filesInfos);
289        files.put("/plugins/core-ui/resources/js/Ametys/message/factory/DefaultMessageTargetFactory." + locale + ".js", filesInfos);
290        files.put("/plugins/front-edition/resources/js/Ametys/message/MessageTargetHelper." + locale + ".js", filesInfos);
291        //add page
292        files.put("/plugins/web/resources/js/Ametys/plugins/web/page/AddPageWizard." + locale + ".js", filesInfos);
293        files.put("/plugins/front-edition/resources/js/Ametys/override/Ametys/plugins/web/page/AddPageWizard." + locale + ".js", filesInfos);
294        files.put("/plugins/web/resources/js/Ametys/web/sitemap/SitemapDAO." + locale + ".js", filesInfos);
295        files.put("/plugins/web/resources/js/Ametys/web/sitemap/Sitemap." + locale + ".js", filesInfos);
296        files.put("/plugins/web/resources/js/Ametys/web/page/PageDAO." + locale + ".js", filesInfos);
297        files.put("/plugins/front-edition/resources/js/Ametys/override/Ametys/web/page/PageDAO." + locale + ".js", filesInfos);
298        files.put("/plugins/web/resources/js/Ametys/plugins/web/page/AddPageWizard/Card." + locale + ".js", filesInfos);
299        files.put("/plugins/web/resources/js/Ametys/plugins/web/page/AddPageWizard/CreatePageCard." + locale + ".js", filesInfos);
300        files.put("/plugins/web/resources/js/Ametys/plugins/web/page/AddPageWizard/PageContentCard." + locale + ".js", filesInfos);
301        files.put("/plugins/web/resources/js/Ametys/plugins/web/page/AddPageWizard/ContentPropertiesCard." + locale + ".js", filesInfos);
302        files.put("/plugins/core-ui/resources/js/Ametys/form/ConfigurableFormPanel." + locale + ".js", filesInfos);
303        files.put("/plugins/front-edition/resources/js/Ametys/userprefs/UserPrefsDAO." + locale + ".js", filesInfos);
304        files.put("/plugins/core-ui/resources/js/Ametys/form/ConfigurableFormPanel/Repeater." + locale + ".js", filesInfos);
305        files.put("/plugins/core-ui/resources/js/Ametys/form/ConfigurableFormPanel/FieldCheckersManager." + locale + ".js", filesInfos);
306        files.put("/plugins/front-edition/resources/js/Ametys/message/MessageBus." + locale + ".js", filesInfos);
307        files.put("/plugins/core-ui/resources/js/Ametys/form/WidgetManager." + locale + ".js", filesInfos);
308        files.put("/plugins/core-ui/resources/js/Ametys/form/widget/DefaultWidgets." + locale + ".js", filesInfos);
309        files.put("/plugins/front-edition/resources/js/Ametys/override/Ametys/form/WidgetManager." + locale + ".js", filesInfos);
310        
311        files.put("/plugins/cms/resources/js/Ametys/cms/content/ContentDAO." + locale + ".js", filesInfos);
312        files.put("/plugins/web/resources/js/Ametys/web/content/ContentDAO." + locale + ".js", filesInfos);
313        files.put("/plugins/front-edition/resources/js/Ametys/override/Ametys/cms/content/ContentDAO." + locale + ".js", filesInfos);
314        files.put("/plugins/cms/resources/js/Ametys/cms/content/Content." + locale + ".js", filesInfos);
315        files.put("/plugins/core-ui/resources/js/Ametys/navhistory/HistoryDAO." + locale + ".js", filesInfos);
316        files.put("/plugins/core-ui/resources/js/Ametys/window/MessageBox." + locale + ".js", filesInfos);
317        files.put("/plugins/front-edition/resources/js/Ametys/tool/ToolsManager." + locale + ".js", filesInfos);
318
319        files.put("/plugins/cms/resources/js/Ametys/cms/form/widget/RichText." + locale + ".js", filesInfos);
320        files.put("/plugins/front-edition/resources/js/Ametys/override/Ametys/cms/form/widget/SelectContent/ContentEntry." + locale + ".js", filesInfos);
321        files.put("/plugins/cms/resources/js/Ametys/cms/form/widget/SelectContent." + locale + ".js", filesInfos);
322        files.put("/plugins/front-edition/resources/js/Ametys/override/Ametys/cms/form/widget/SelectContent." + locale + ".js", filesInfos);
323        files.put("/plugins/cms/resources/js/Ametys/cms/form/widget/SelectReferenceTableContent." + locale + ".js", filesInfos);
324        files.put("/plugins/front-edition/resources/js/Ametys/override/Ametys/cms/form/widget/SelectReferenceTableContent." + locale + ".js", filesInfos);
325        files.put("/plugins/web/resources/js/Ametys/plugins/web/page/AddPageWizard/PageTypeCard." + locale + ".js", filesInfos);
326        files.put("/plugins/front-edition/resources/js/Ametys/override/Ametys/plugins/web/page/AddPageWizard/PageTypeCard." + locale + ".js", filesInfos);
327        files.put("/plugins/web/resources/js/Ametys/web/form/widget/SelectPage." + locale + ".js", filesInfos);
328        files.put("/plugins/front-edition/resources/js/Ametys/override/Ametys/web/form/widget/SelectPage." + locale + ".js", filesInfos);
329        files.put("/plugins/web/resources/js/Ametys/web/form/widget/SelectSite." + locale + ".js", filesInfos);
330        files.put("/plugins/web/resources/js/Ametys/web/helper/ChooseSite." + locale + ".js", filesInfos);
331        files.put("/plugins/web/resources/js/Ametys/web/site/SitesTree." + locale + ".js", filesInfos);
332        files.put("/plugins/web/resources/js/Ametys/web/site/SitesTree/Site." + locale + ".js", filesInfos);
333        files.put("/plugins/web/resources/js/Ametys/web/helper/ContextToolbar." + locale + ".js", filesInfos);
334        files.put("/plugins/web/resources/js/Ametys/web/site/SitesTree/Site." + locale + ".js", filesInfos);
335        files.put("/plugins/web/resources/js/Ametys/web/form/widget/SelectPage/PageEntry." + locale + ".js", filesInfos);
336        files.put("/plugins/web/resources/js/Ametys/plugins/web/page/AddPageWizard/TagsCard." + locale + ".js", filesInfos);
337        files.put("/plugins/web/resources/js/Ametys/plugins/web/page/AddPageWizard/TagsCardLite." + locale + ".js", filesInfos);
338        files.put("/plugins/web/resources/js/Ametys/web/page/Page." + locale + ".js", filesInfos);
339        files.put("/plugins/cms/resources/js/Ametys/plugins/cms/tag/TagsTreePanel." + locale + ".js", filesInfos);
340        files.put("/plugins/cms/resources/js/Ametys/plugins/cms/tag/TagsTreePanel/TagNode." + locale + ".js", filesInfos);
341
342        //delete page
343        files.put("/plugins/web/resources/js/Ametys/plugins/web/sitemap/SitemapActions." + locale + ".js", filesInfos);
344
345        //page tags
346        files.put("/plugins/web/resources/js/Ametys/plugins/web/tag/AffectTagAction." + locale + ".js", filesInfos);
347        files.put("/plugins/cms/resources/js/Ametys/cms/uihelper/ChooseTagHelper." + locale + ".js", filesInfos);
348        files.put("/plugins/cms/resources/js/Ametys/cms/uihelper/ChooseTag." + locale + ".js", filesInfos);
349        files.put("/plugins/web/resources/js/Ametys/web/helper/ChooseTag." + locale + ".js", filesInfos);
350        files.put("/plugins/cms/resources/js/Ametys/plugins/cms/tag/TagActions." + locale + ".js", filesInfos);
351        files.put("/plugins/web/resources/js/Ametys/plugins/web/tag/TagsTreePanel." + locale + ".js", filesInfos);
352        
353        // scheduled publication
354        files.put("/plugins/web/resources/js/Ametys/plugins/web/page/SchedulePublication." + locale + ".js", filesInfos);
355        files.put("/plugins/front-edition/resources/js/Ametys/override/Ametys/plugins/web/page/SchedulePublication." + locale + ".js", filesInfos);
356
357        // ping
358        files.put("/plugins/core-ui/resources/js/Ametys/plugins/coreui/system/StartTimeChecker." + locale + ".js", filesInfos);
359
360        // actions on content
361        files.put("/plugins/cms/resources/js/Ametys/cms/uihelper/EditContent." + locale + ".js", filesInfos);
362        files.put("/plugins/web/resources/js/Ametys/plugins/web/zone/ZoneActions." + locale + ".js", filesInfos);
363        files.put("/plugins/web/resources/js/Ametys/plugins/web/zone/ZoneDAO." + locale + ".js", filesInfos);
364
365        // file widget (+cropping for image filter)
366        files.put("/plugins/core-ui/resources/js/Ametys/form/widget/File." + locale + ".js", filesInfos);
367        files.put("/plugins/front-edition/resources/js/Ametys/override/Ametys/form/widget/File." + locale + ".js", filesInfos);
368        files.put("/plugins/core-ui/resources/js/Ametys/form/widget/File/FileSource." + locale + ".js", filesInfos);
369        files.put("/plugins/core-ui/resources/js/Ametys/form/widget/File/External." + locale + ".js", filesInfos);
370        files.put("/plugins/cms/resources/js/Ametys/cms/form/widget/File/Resource." + locale + ".js", filesInfos);
371        files.put("/plugins/core-ui/resources/js/Ametys/helper/crop/CropDialog." + locale + ".js", filesInfos);
372        files.put("/plugins/core-ui/resources/js/Jcrop/jquery.Jcrop." + locale + ".js", filesInfos);
373
374        // Let's add all richtextconfiguration files
375        _addRichTextConfigurationFiles(files, filesInfos, locale, rtlMode, true);
376
377        return _hashCache.createHash(files, locale);
378    }
379
380    /**
381     * Prepare a hashcode for css files
382     * @param locale The language code to use
383     * @param rtlMode Is for right-to-left language?
384     * @param theme The url for the theme
385     * @return The new hashcode to read the minimified concatened file
386     */
387    public static String prepareCSSFiles(String locale, boolean rtlMode, String theme)
388    {
389        Map<String, String> filesInfos = new HashMap<>();
390        filesInfos.put("media", "");
391        filesInfos.put("tag", "link");
392        
393        Map<String, Map<String, String>> files = new LinkedHashMap<>();
394        String themeName = StringUtils.substringAfterLast(theme, "/");
395        files.put(theme + "/" + themeName + "-all.css", filesInfos); 
396        files.put("/plugins/front-edition/resources/css/theme-ametys-base-light.css", filesInfos);
397        files.put("/plugins/core-ui/resources/font/ametys/AmetysIcon.css", filesInfos);
398        files.put("/plugins/core-ui/resources/css/Jcrop/jquery.Jcrop.css", filesInfos);
399        files.put("/plugins/web/resources/css/pages/addpage.css", filesInfos);
400        files.put("/plugins/cms/resources/css/selectcontent.css", filesInfos);
401        
402        // Let's add all richtextconfiguration files
403        _addRichTextConfigurationFiles(files, filesInfos, locale, rtlMode, false);
404        
405        return _hashCache.createHash(files, locale);
406    }
407
408    private static List<ScriptFile> _getRichTextConfigurationFiles(boolean scripts, Map<String, Object> contextParameters)
409    {
410        List<ScriptFile> files = new ArrayList<>();
411        
412        for (String richTextConfigurationId : _richTextConfigurationExtensionPoint.getExtensionsIds())
413        {
414            RichTextConfiguration richTextConfiguration = _richTextConfigurationExtensionPoint.getExtension(richTextConfigurationId);
415            for (String category : richTextConfiguration.getCategories())
416            {
417                Set<ClientSideElement> convertors = richTextConfiguration.getConvertors(category, contextParameters);
418                if (convertors != null)
419                {
420                    for (ClientSideElement convertor : convertors)
421                    {
422                        List<Script> scriptsToAdd = convertor.getScripts(Collections.EMPTY_MAP);
423                        if (scriptsToAdd != null)
424                        {
425                            for (Script script : scriptsToAdd)
426                            {
427                                List<ScriptFile> scriptFiles = scripts ? script.getScriptFiles() : script.getCSSFiles();
428                                if (scriptFiles != null)
429                                {
430                                    files.addAll(scriptFiles);
431                                }
432                            }
433                        }
434                    }
435                }
436
437                Set<ClientSideElement> validators = richTextConfiguration.getValidators(category, contextParameters);
438                if (validators != null)
439                {
440                    for (ClientSideElement validator : validators)
441                    {
442                        List<Script> scriptsToAdd = validator.getScripts(Collections.EMPTY_MAP);
443                        if (scriptsToAdd != null)
444                        {
445                            for (Script script : scriptsToAdd)
446                            {
447                                List<ScriptFile> scriptFiles = scripts ? script.getScriptFiles() : script.getCSSFiles();
448                                if (scriptFiles != null)
449                                {
450                                    files.addAll(scriptFiles);
451                                }
452                            }
453                        }
454                    }
455                }
456            }
457        }
458
459        return files;
460    }
461
462    private static void _addRichTextConfigurationFiles(Map<String, Map<String, String>> files, Map<String, String> filesInfos, String locale, boolean rtlMode, boolean scripts)
463    {
464        Map<String, Object> contextParameters = new HashMap<>();
465        contextParameters.put("siteName", AmetysXSLTHelper.site());
466        
467        List<ScriptFile> richTextConfigurationFiles = _getRichTextConfigurationFiles(scripts, contextParameters);
468        for (ScriptFile file : richTextConfigurationFiles)
469        {
470            String uri = null;
471            if (file.isLangSpecific())
472            {
473                if (file.getLangPaths().containsKey(locale))
474                {
475                    uri = file.getLangPaths().get(locale);
476                }
477                else if (file.getLangPaths().containsKey(file.getDefaultLang()))
478                {
479                    uri = file.getLangPaths().get(file.getDefaultLang());
480                }
481            }
482            else
483            {
484                if (file.getRtlMode() == null || "all".equals(file.getRtlMode()) || Boolean.toString(rtlMode).equals(file.getRtlMode()))
485                {
486                    uri = file.getPath();
487                }
488            }
489            
490            if (uri != null)
491            {
492                files.put(uri, filesInfos);
493            }
494        }
495    }
496    /**
497     * Check if we can display the front edition button for a specific right
498     * Checks :
499     * * Rendering context == front
500     * * Edition mode
501     * * Front_Edition_Access_Right on current page
502     * * RightId available  on object
503     * @param rightId right to check (can be null)
504     * @param objectId id of page/content to check. Can be null or empty to get the current page
505     * @return true if all is OK, false if at least one is false
506     * @deprecated Use the version with three arguments
507     */
508    @Deprecated
509    public static boolean hasFrontEditionRight(String rightId, String objectId)
510    {
511        return hasFrontEditionRight(rightId, objectId, true);
512    }
513    /**
514     * Check if we can display the front edition button for a specific right
515     * Checks :
516     * * Rendering context == front
517     * * Edition mode
518     * * Front_Edition_Access_Right on current page
519     * * RightId available on object
520     * @param rightId right to check (can be null)
521     * @param objectId id of page/content to check. Can be null or empty to get the current page
522     * @param editionModeOnly Check if the user is in edition mode
523     * @return true if all is OK, false if at least one is false
524     */
525    public static boolean hasFrontEditionRight(String rightId, String objectId, boolean editionModeOnly)
526    {
527        String aoId = objectId;
528        if (StringUtils.isEmpty(aoId))
529        {
530            aoId = AmetysXSLTHelper.pageId();
531        }
532        
533        if (StringUtils.isEmpty(aoId)                                 // No page or content to check
534            || !RenderingContext.FRONT.toString().equals(AmetysXSLTHelper.renderingContext())     // We are no on front
535            || editionModeOnly && !AmetysXSLTHelper.isEditionMode()   // Not in edition mode
536            || !hasFrontEditionRight())                                 // The user has the front edition right on current page
537        {
538            
539            return false;
540        }
541        
542        Request request = ContextHelper.getRequest(_context);
543        
544        // Retrieve the current workspace.
545        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
546        
547        try
548        {
549            // Force default workspace (rights have to be checked on default workspace)
550            // The target objectId can be a node page, available of front only when a first subpage will be created
551            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, RepositoryConstants.DEFAULT_WORKSPACE);
552            
553            if (StringUtils.contains(aoId, ";"))
554            {
555                String[] pageIds = StringUtils.split(aoId, ";");
556                for (String pid : pageIds)
557                {
558                    if (_rightManager.currentUserHasRight(rightId, _ametysObjectResolver.resolveById(pid)) == RightResult.RIGHT_ALLOW)   // Check right on page (If this page does not exists an exception will be thrown... this is fine)
559                    {
560                        return true;
561                    }
562                    // else check the next one
563                }
564                return false; // No right inside the list
565            }
566            else
567            {
568                return StringUtils.isEmpty(rightId) // There is no right to check OR the user has the right
569                    || _rightManager.currentUserHasRight(rightId, _ametysObjectResolver.resolveById(aoId)) == RightResult.RIGHT_ALLOW;   // Check right on page (If this page does not exists an exception will be thrown... this is fine)
570            }
571        }
572        finally
573        {
574            // Restore workspace
575            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
576        }
577    }
578    
579    /**
580     * Check if current user has right on Ametys object for front edition
581     * Checks :
582     * * Rendering context == front
583     * * Edition mode
584     * * Front_Edition_Access_Right on current page
585     * * RightId available on object
586     * @param rightId right to check (can be null)
587     * @param ao The ametys object check. Cannot be null
588     * @param editionModeOnly Check if the user is in edition mode
589     * @return true if all is OK, false if at least one is false
590     */
591    public static boolean hasFrontEditionRight(String rightId, AmetysObject ao, boolean editionModeOnly)
592    {
593        if (!RenderingContext.FRONT.toString().equals(AmetysXSLTHelper.renderingContext())     // We are no on front
594            || editionModeOnly && !AmetysXSLTHelper.isEditionMode()   // Not in edition mode
595            || !hasFrontEditionRight())                                 // The user has the front edition right on current page
596        {
597            return false;
598        }
599        
600        Request request = ContextHelper.getRequest(_context);
601        
602        // Retrieve the current workspace.
603        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
604        
605        try
606        {
607            // Force default workspace (rights have to be checked on default workspace)
608            // The target objectId can be a node page, available of front only when a first subpage will be created
609            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, RepositoryConstants.DEFAULT_WORKSPACE);
610            
611            return StringUtils.isEmpty(rightId) // There is no right to check OR the user has the right
612                    || _rightManager.currentUserHasRight(rightId, ao) == RightResult.RIGHT_ALLOW;   // Check right on page (If this page does not exists an exception will be thrown... this is fine)
613        }
614        finally
615        {
616            // Restore workspace
617            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
618        }
619    }
620    
621    /**
622     * Check if the current user has the Front_Edition_Access_Right right
623     * @return true if right granted
624     */
625    public static boolean hasFrontEditionRight()
626    {
627        String pageId = AmetysXSLTHelper.pageId();
628        if (StringUtils.isNotEmpty(pageId))
629        {
630            Page page = _ametysObjectResolver.resolveById(pageId);
631            return _rightManager.currentUserHasRight(FRONT_EDITION_RIGHT_ID, page) == RightResult.RIGHT_ALLOW;
632        }
633        
634        String lang = AmetysXSLTHelper.lang();
635        if (StringUtils.isEmpty(lang))
636        {
637            Request request = ContextHelper.getRequest(_context);
638            lang = (String) request.getAttribute("lang");
639        }
640        String siteName = AmetysXSLTHelper.site();
641        
642        if (StringUtils.isNotEmpty(siteName) && StringUtils.isNoneEmpty(lang))
643        {
644            Site site = _siteManager.getSite(siteName);
645            Sitemap sitemap = site.getSitemap(lang);
646            return _rightManager.currentUserHasRight(FRONT_EDITION_RIGHT_ID, sitemap) == RightResult.RIGHT_ALLOW;
647        }
648        
649        return false;
650    }
651    /**
652     * Check if the current workflow action is available on this content
653     * @param actionId action to check
654     * @param contentId content to check
655     * @param checkEditionMode check also if we are in edition mode
656     * @return true if right is granted
657     */
658    public static boolean hasWorkflowRight(int actionId, String contentId, boolean checkEditionMode)
659    {
660        List<Integer> actionIds = new ArrayList<>();
661        actionIds.add(actionId);
662        return hasWorkflowRight(actionIds, contentId, checkEditionMode);
663    }
664    /**
665     * Check if a list of workflow actions are available on this content
666     * @param actionIds list of action ids
667     * @param contentId id of the content to check
668     * @param checkEditionMode check also if we are in edition mode
669     * @return true if all actions are available
670     */
671    public static boolean hasWorkflowRight(List<Integer> actionIds, String contentId, boolean checkEditionMode)
672    {
673        Content content = _ametysObjectResolver.resolveById(contentId);
674        return hasWorkflowRight(actionIds, content, checkEditionMode);
675    }
676    /**
677     * Check if a list of workflow actions are available on this content
678     * @param actionIds list of action ids
679     * @param content content to check
680     * @param checkEditionMode check also if we are in edition mode
681     * @return true if all actions are available
682     */
683    public static boolean hasWorkflowRight(List<Integer> actionIds, Content content, boolean checkEditionMode)
684    {
685        if (checkEditionMode)
686        {
687            boolean editionModeOk = AmetysXSLTHelper.isEditionMode();
688            if (!editionModeOk)
689            {
690                return false;
691            }
692        }
693        boolean result = false;
694        if (content instanceof WorkflowAwareContent)
695        {
696            WorkflowAwareContent wcontent = (WorkflowAwareContent) content;
697            int[] availableActions = _contentWorkflowHelper.getAvailableActions(wcontent);
698            for (int actionId : actionIds)
699            {
700                result = result || ArrayUtils.contains(availableActions, actionId);
701                if (result)
702                {
703                    break;
704                }
705            }
706        }
707        return result;
708    }
709    
710    /**
711     * Get the toolbar configuration for page: retrieve the user rights, the parent and page position
712     * @param pageId The page id
713     * @param addContentsConfigAsJson The items configuration for adding contents as JSON string
714     * @param editionModeOnly true to check edition mode
715     * @return the user's rights, parent and page position
716     * @throws UnknownAmetysObjectException if content is unknown
717     * @throws AmetysRepositoryException if an error occurred
718     * @throws RepositoryException if an error occurred 
719     */
720    public static String getPageToolbarConfig(String pageId, String addContentsConfigAsJson, boolean editionModeOnly) throws UnknownAmetysObjectException, AmetysRepositoryException, RepositoryException
721    {
722        Map<String, Object> result = new HashMap<>();
723        
724        Page page = _ametysObjectResolver.resolveById(pageId);
725        PagesContainer parent = page.getParent();
726        long position = _getPagePosition(page);
727        
728        List<Object> addContentsConfig = _jsonUtils.convertJsonToList(addContentsConfigAsJson);
729        
730        @SuppressWarnings("unchecked")
731        Set<Object> allowedContentTypes = addContentsConfig.stream()
732            .filter(Map.class::isInstance)
733            .filter(cfg -> _hasContentCreationRight((Map) cfg))
734            .map(cfg -> (String) ((Map) cfg).get("contentType"))
735            .collect(Collectors.toSet());
736        result.put("allowedContentTypes", allowedContentTypes);
737        
738        result.put("title", page.getTitle());
739        result.put("parentId", parent.getId());
740        result.put("position", position);
741
742        // Get the active status for each action depending on user's rights, page position, FO edition mode, ...
743        result.put("actionStatus", _getActionStatusOnPage(page, position, parent.getChildrenPages().getSize(), editionModeOnly));
744        
745        return _jsonUtils.convertObjectToJson(result);
746    }
747    
748    private static boolean _hasContentCreationRight(Map<String, Object> config)
749    {
750        if (config.containsKey("contentType"))
751        {
752            String cTypeId = (String) config.get("contentType");
753            
754            ContentType contentType = _contentTypesEP.getExtension(cTypeId);
755            if (contentType == null)
756            {
757                _logger.warn("Unknown content type '" + cTypeId + "'");
758                return false;
759            }
760            
761            String right = contentType.getRight();
762            if (StringUtils.isEmpty(right))
763            {
764                return true;
765            }
766            
767            return _rightManager.currentUserHasRight(right, "/cms") == RightResult.RIGHT_ALLOW || _rightManager.currentUserHasRight(right, _rootContentHelper.getRootContent()) == RightResult.RIGHT_ALLOW;
768        }
769        else
770        {
771            _logger.warn("Configuration to add content is invalid: missing 'contentType'. It will be ignored");
772            return false;
773        }
774    }
775    
776    private static long _getPagePosition(Page page) throws AmetysRepositoryException
777    {
778        PagesContainer parent = page.getParent();
779        AmetysObjectIterable< ? extends Page> children = parent.getChildrenPages();
780        
781        long count = 1;
782        for (Page child : children)
783        {
784            if (child.getId().equals(page.getId()))
785            {
786                return count;
787            }
788            count++;
789        }
790        
791        // Not found
792        return -1;
793    }
794    
795    private static Map<String, Boolean> _getActionStatusOnPage(Page page, long position, long size, boolean editionModeOnly) throws UnknownAmetysObjectException, AmetysRepositoryException
796    {
797        Map<String, Boolean> rights = new HashMap<>();
798        
799        if (page instanceof ModifiablePage)
800        {
801            rights.put("add-content", hasFrontEditionRight("Web_Rights_Page_AddContent", page, editionModeOnly));
802            rights.put("add-page", hasFrontEditionRight("Web_Rights_Page_Create", page, editionModeOnly));
803            rights.put("tag", hasFrontEditionRight("Web_Rights_Page_Tag", page, editionModeOnly));
804            rights.put("rename", hasFrontEditionRight("Web_Rights_Page_Rename", page, editionModeOnly));
805            rights.put("schedule-publication", hasFrontEditionRight("Web_Rights_Page_Schedule", page, editionModeOnly));
806            rights.put("delete", hasFrontEditionRight("Web_Rights_Page_Delete", page, editionModeOnly));
807            rights.put("delete", hasFrontEditionRight("Web_Rights_Page_Rename", page, editionModeOnly));
808            
809            boolean canMove = hasFrontEditionRight(StringUtils.EMPTY, page, editionModeOnly);
810            boolean moveUp = canMove && position > 1;
811            boolean moveDown = canMove && position < size;
812            
813            rights.put("move-up", moveUp);
814            rights.put("move-down", moveDown);
815            rights.put("move", moveDown || moveUp);
816        }
817        
818        return rights;
819    }
820    
821    /**
822     * Get the toolbar configuration for content: retrieve user rights, available workflow actions, content current step and status history
823     * @param contentId The content id
824     * @param zoneItemId The zone item id
825     * @param restrictedActions The actions ids to retrieve. Set to null or empty to get all available workflow actions with no restriction
826     * @param jsonStepConfig The step configuration
827     * @param locale the locale for translation
828     * @param editionModeOnly true to check edition mode
829     * @return the user's rights, available workflow actions and content current step
830     * @throws UnknownAmetysObjectException if content is unknown
831     * @throws AmetysRepositoryException if an error occurred
832     * @throws RepositoryException if an error occurred 
833     */
834    public static String getContentToolbarConfig(String contentId, String zoneItemId, String restrictedActions, String jsonStepConfig, String locale, boolean editionModeOnly) throws UnknownAmetysObjectException, AmetysRepositoryException, RepositoryException
835    {
836        Map<String, Object> result = new HashMap<>();
837        
838        Content content = _ametysObjectResolver.resolveById(contentId);
839
840        // Get the active status of each action depending on user rights, FO mode, content's position, ...
841        result.put("actionStatus", _getActionStatusOnContent(content, zoneItemId, editionModeOnly));
842        
843        if (content instanceof WorkflowAwareContent)
844        {
845            WorkflowDescriptor workflowDescriptor = _getWorkflowDescriptor((WorkflowAwareContent) content);
846            
847            WorkflowAwareContent wcontent = (WorkflowAwareContent) content;
848            
849            Map<String, Object> step = new HashMap<>();
850            long currentStepId = wcontent.getCurrentStepId();
851            
852            // Current step
853            StepDescriptor stepDescriptor = _getStepDescriptor(workflowDescriptor, currentStepId);
854            step.put("id", currentStepId);
855            String name = stepDescriptor.getName();
856            step.put("label", new I18nizableText(StringUtils.substringBefore(name, ":"), StringUtils.substringAfter(name, ":")));
857            result.put("step", step);
858            
859            Map<String, Object> stepConfig = StringUtils.isEmpty(jsonStepConfig) ? null : _jsonUtils.convertJsonToMap(jsonStepConfig);
860            result.put("statusHistory", _getStatusHistory(wcontent, currentStepId, stepConfig, workflowDescriptor));
861            
862            List<Integer> availableActions = Arrays.stream(_contentWorkflowHelper.getAvailableActions(wcontent)).boxed().collect(Collectors.toList());
863            result.put("availableWorkflowActions", availableActions);
864            
865            if (!editionModeOnly || AmetysXSLTHelper.isEditionMode())
866            {
867                List<Object> restrictedActionsAsList = StringUtils.isEmpty(restrictedActions) ? null : _jsonUtils.convertJsonToList(restrictedActions);
868                
869                List<Object> restrictedActionIds = restrictedActionsAsList == null ? null : restrictedActionsAsList.stream()
870                    .map(o -> o instanceof Map ? ((Map) o).get("id") : o)
871                    .collect(Collectors.toList());
872                
873                @SuppressWarnings("unchecked")
874                Map<Object, Map<String, Object>> workflowActions = availableActions.stream()
875                    .filter(actionId -> restrictedActionIds == null || restrictedActionIds.contains(actionId))
876                    .map(actionId -> {
877                        Map<String, Object> wa = new HashMap<>();
878                        
879                        _addDefaultWorkflowActionProperties(wa, workflowDescriptor, actionId);
880                        
881                        if (restrictedActionIds != null && restrictedActionsAsList != null && restrictedActionIds.contains(actionId))
882                        {
883                            for (Object object : restrictedActionsAsList)
884                            {
885                                if (object instanceof Map && ((Map) object).get("id") == actionId)
886                                {
887                                    // get or override properties from map sent by client side
888                                    wa.putAll((Map<String, Object>) object);
889                                    break;
890                                }
891                            }
892                            
893                        }
894                        return Pair.of(actionId, wa);
895                    })
896                    .collect(Collectors.toMap(Pair::getLeft, Pair::getRight));
897                
898                result.put("workflowActions", workflowActions);
899            }
900        }
901        
902        Request request = ContextHelper.getRequest(_context);
903        try
904        {
905            request.setAttribute(I18nizableSerializer.REQUEST_ATTR_LOCALE, locale);
906            return _jsonUtils.convertObjectToJson(result);
907        }
908        finally
909        {
910            request.removeAttribute(I18nizableSerializer.REQUEST_ATTR_LOCALE);
911        }
912    }
913    
914    private static Map<String, Boolean> _getActionStatusOnContent(Content content, String zoneItemId, boolean editionModeOnly) throws UnknownAmetysObjectException, AmetysRepositoryException
915    {
916        Map<String, Boolean> rights = new HashMap<>();
917        
918        ZoneItem zoneItem = _ametysObjectResolver.resolveById(zoneItemId);
919        Zone zone = zoneItem.getZone();
920        Page page = zone.getPage();
921        
922        rights.put("tag", hasFrontEditionRight("CMS_Rights_Content_Tag", content, editionModeOnly));
923        rights.put("remove", page instanceof ModifiablePage && hasFrontEditionRight("Web_Rights_Page_DeleteZoneItem", page, editionModeOnly));
924        rights.put("delete", hasFrontEditionRight("CMS_Rights_DeleteContent", content, editionModeOnly));
925        
926        boolean moveUp = false;
927        boolean moveDown = false;
928        
929        long position = getZoneItemPosition(zoneItem);
930        long size = getZoneSize(zone);
931        
932        if (position != -1 && page instanceof ModifiablePage && hasFrontEditionRight("Web_Rights_Page_OrganizeZoneItem", page, editionModeOnly))
933        {
934            if (position > 0) 
935            {
936                moveUp = true;
937            }
938            
939            if (position + 1 < size)
940            {
941                moveDown = true;
942            }
943        }
944        rights.put("move-up", moveUp);
945        rights.put("move-down", moveDown);
946        rights.put("move", moveDown || moveUp);
947        
948        return rights;
949    }
950    
951    private static Map<Integer, Map<String, Object>> _getStatusHistory(WorkflowAwareContent content, long currentStepId, Map<String, Object> stepsConfig, WorkflowDescriptor workflowDescriptor)
952    {
953        List<Step> historyStatus = new ArrayList<>();
954            
955        if (stepsConfig.containsKey(String.valueOf(currentStepId)))
956        {
957            AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(content);
958            
959            long workflowId = content.getWorkflowId();
960            List<Step> historySteps = workflow.getHistorySteps(workflowId);
961
962            // Sort by start date descendant 
963            Collections.sort(historySteps, new Comparator<Step>()
964            {
965                public int compare(Step s1, Step s2)
966                {
967                    return -s1.getStartDate().compareTo(s2.getStartDate());
968                }
969            });
970            
971            @SuppressWarnings("unchecked")
972            Map<String, Object> currentStepCfg = (Map<String, Object>) stepsConfig.get(String.valueOf(currentStepId));
973            String currentStepType = currentStepCfg != null ? (String) currentStepCfg.get("type") : null;
974            
975            boolean stop = false;
976            for (Step step : historySteps)
977            {
978                if (stop)
979                {
980                    break;
981                }
982
983                long stepId = step.getStepId();
984                @SuppressWarnings("unchecked")
985                Map<String, Object> stepCfg = (Map<String, Object>) stepsConfig.get(String.valueOf(stepId));
986                String stepType = stepCfg != null ? (String) stepCfg.get("type") : null;
987                
988                stop = "positive".equals(currentStepType) && !"positive".equals(stepType) 
989                        || ("negative".equals(currentStepType) || "neutral".equals(currentStepType)) && !"negative".equals(stepType)
990                        || currentStepId == stepId;
991                
992                if (!stop)
993                {
994                    historyStatus.add(step);
995                }
996            }
997        }
998        
999        Map<Integer, Map<String, Object>> jsonHistoryStatus = new LinkedHashMap<>();
1000        for (Step step : historyStatus)
1001        {
1002            if (!jsonHistoryStatus.containsKey(step.getStepId()))
1003            {
1004                StepDescriptor stepDescriptor = _getStepDescriptor(workflowDescriptor, step.getStepId());
1005                String name = stepDescriptor.getName();
1006                I18nizableText label = new I18nizableText(StringUtils.substringBefore(name, ":"), StringUtils.substringAfter(name, ":"));
1007                jsonHistoryStatus.put(step.getStepId(), Map.of("label", label));
1008            }
1009        }
1010        
1011        return jsonHistoryStatus;
1012    }
1013    
1014    private static void _addDefaultWorkflowActionProperties(Map<String, Object> wa, WorkflowDescriptor workflowDescriptor, int actionId)
1015    {
1016        // Add workflow action label
1017        ActionDescriptor wAction = workflowDescriptor.getAction(actionId);
1018        String name = wAction.getName();
1019        wa.put("label", new I18nizableText(StringUtils.substringBefore(name, ":"), StringUtils.substringAfter(name, ":")));
1020        
1021        // Default workflow icon
1022        switch (actionId)
1023        {
1024            case 3:
1025                wa.put("icon", "fas fa-hand-point-up"); // Propose
1026                break;
1027            case 4:
1028                wa.put("icon", "fas fa-check"); // Validate
1029                break;
1030            case 7:
1031                wa.put("icon", "fas fa-times"); // Refuse
1032                break;  
1033            case 10:
1034                wa.put("icon", "fas fa-ban"); // Unpublish
1035                break;  
1036            default:
1037                break;
1038        }
1039    }
1040    
1041    private static WorkflowDescriptor _getWorkflowDescriptor(WorkflowAwareContent content)
1042    {
1043        long workflowId = content.getWorkflowId();
1044        
1045        AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(content);
1046        
1047        String workflowName = workflow.getWorkflowName(workflowId);
1048        return workflow.getWorkflowDescriptor(workflowName);
1049    }
1050    
1051    private static StepDescriptor _getStepDescriptor(WorkflowDescriptor workflowDescriptor, long stepId)
1052    {
1053        if (workflowDescriptor != null)
1054        {
1055            StepDescriptor stepDescriptor = workflowDescriptor.getStep((int) stepId);
1056            if (stepDescriptor != null)
1057            {
1058                return stepDescriptor;
1059            }
1060        }
1061        
1062        return null;
1063    }
1064    
1065    /**
1066     * Get a string representing a json map of the contentIds with true/false if they can/can't be modified 
1067     * @param actionId action to check
1068     * @param inputPageId page (can be null)
1069     * @param checkEditionMode check also if we are in edition mode
1070     * @return {contentId : true/false, ....}
1071     */
1072    public static String getModifiableContents(int actionId, String inputPageId, boolean checkEditionMode)
1073    {
1074        Map<String, Object> jsonObject = new HashMap<>();
1075        String pageId = inputPageId;
1076        if (StringUtils.isEmpty(inputPageId))
1077        {
1078            pageId = AmetysXSLTHelper.pageId();
1079        }
1080        
1081        if (!StringUtils.isEmpty(pageId))
1082        {
1083            AmetysObject resolveById = _ametysObjectResolver.resolveById(pageId);
1084            if (resolveById instanceof Page)
1085            {
1086                Page page = (Page) resolveById;
1087                List<Content> contents = _pageHelper.getAllContents(page);
1088                for (Content content : contents)
1089                {
1090                    Map<String, Object> contentInfo = new HashMap<>();
1091                    contentInfo.put("unmodifiableAttributes", AmetysFrontEditionHelper.getUnmodifiableAttributes(content, List.of(actionId), checkEditionMode));
1092                    contentInfo.put("rights", AmetysFrontEditionHelper.getRightsForContent(content));
1093                    
1094                    jsonObject.put(content.getId(), contentInfo);
1095                }
1096            }
1097        }
1098
1099        return _jsonUtils.convertObjectToJson(jsonObject);
1100    }
1101    
1102    /**
1103     * Get path of unmodifiable attributes. Check if content can be modified and the restrictions on attributes
1104     * @param content the content
1105     * @param actionIds the workflow action ids to check
1106     * @param checkEditionMode to check edition mode
1107     * @return the list with the path of unmodifiable attributes (regarding of restrictions) or null if the content cannot be modified
1108     */
1109    @SuppressWarnings("unchecked")
1110    public static List<String> getUnmodifiableAttributes(Content content, List<Integer> actionIds, boolean checkEditionMode)
1111    {
1112        try
1113        {
1114            boolean hasWorkflowRight = hasWorkflowRight(actionIds, content, checkEditionMode);
1115            if (!hasWorkflowRight)
1116            {
1117                // content is not editable
1118                return null;
1119            }
1120            
1121            Map<String, ModelItem> modelItems = _contentTypesHelper.getModelItems(content.getTypes());
1122            List<AttributeDefinition> attributes = _getAttributeDefinitionsRecursively(modelItems.values());
1123            return attributes.stream()
1124                .filter(RestrictedModelItem.class::isInstance)
1125                .filter(mi -> !((RestrictedModelItem) mi).canWrite(content))
1126                .map(mi -> mi.getPath())
1127                .collect(Collectors.toList());
1128        }
1129        catch (ConfigurationException e)
1130        {
1131            _logger.error("Fail to get restricted model items for content " + content.getId(), e);
1132            return List.of();
1133        }
1134    }
1135    
1136    /**
1137     * Get a map of chosen rights for a content
1138     * @param content the content
1139     * @return the chosen rights for a content
1140     */
1141    public static Map<String, Object> getRightsForContent(Content content)
1142    {
1143        Map<String, Object> rights = new HashMap<>();
1144        rights.put("hasAddFileRight", _rightManager.currentUserHasRight(ExplorerResourcesDAO.RIGHTS_RESOURCE_ADD, content) == RightResult.RIGHT_ALLOW);
1145        return rights;
1146    }
1147    
1148    private static List<AttributeDefinition> _getAttributeDefinitionsRecursively(Collection<ModelItem> modelItems)
1149    {
1150        List<AttributeDefinition> attributeDefs = new ArrayList<>();
1151        
1152        for (ModelItem modelItem : modelItems)
1153        {
1154            if (modelItem instanceof AttributeDefinition)
1155            {
1156                attributeDefs.add((AttributeDefinition) modelItem);
1157            }
1158            else if (modelItem instanceof ModelItemGroup)
1159            {
1160                attributeDefs.addAll(_getAttributeDefinitionsRecursively(((ModelItemGroup) modelItem).getModelItems()));
1161            }
1162        }
1163        
1164        return attributeDefs;
1165    }
1166    
1167    /**
1168     * Get the name of a workflow action
1169     * @param workflowName workflow name
1170     * @param actionId action id in the workflow
1171     * @return name of the workflow action
1172     */
1173    public static String getWorkflowName(String workflowName, int actionId)
1174    {
1175        String workflowNameKey = _workflowHelper.getActionName(workflowName, actionId);
1176        String translatedName = org.ametys.core.util.AmetysXSLTHelper.translate(workflowNameKey);
1177        return translatedName;
1178    }
1179    /**
1180     * Get the status (validated/draft) of all contents in a page
1181     * @param pageId id of the page to check
1182     * @return "" if not a page, "draft" if all contents are drafts, "validated" if all contents are validated, and "mixed" if there are both
1183     */
1184    public static String getPageStatus(String pageId)
1185    {
1186        boolean hasDraft = false;
1187        boolean hasValidated = false;
1188        AmetysObject resolveById = _ametysObjectResolver.resolveById(pageId);
1189        if (resolveById instanceof Page)
1190        {
1191            Page page = (Page) resolveById;
1192            List<Content> contents = _pageHelper.getAllContents(page);
1193            for (Content content : contents)
1194            {
1195                if (isContentLive(content))
1196                {
1197                    hasValidated = true;
1198                }
1199                else
1200                {
1201                    hasDraft = true;
1202                }
1203            }
1204            String result = "none";
1205            if (hasDraft && !hasValidated)
1206            {
1207                result = "draft";
1208            }
1209            else if (!hasDraft && hasValidated)
1210            {
1211                result = "validated";
1212            }
1213            else if (hasDraft && hasValidated)
1214            {
1215                result = "mixed";
1216            }
1217            return result;
1218        }
1219        else
1220        {
1221            return "";
1222        }
1223    }
1224
1225    /**
1226     * Get the workflow step id of a content
1227     * @param contentId id of the content
1228     * @return the step Id
1229     */
1230    public static long getContentWorkflowId(String contentId)
1231    {
1232        AmetysObject resolvedById = _ametysObjectResolver.resolveById(contentId);
1233        if (resolvedById instanceof Content)
1234        {
1235            Content content = (Content) resolvedById;
1236            return getContentWorkflowId(content);
1237        }
1238        else
1239        {
1240            return 0;
1241        }
1242    }
1243    /**
1244     * Get the workflow step id of a content
1245     * @param content content
1246     * @return the step Id
1247     */
1248    public static long getContentWorkflowId(Content content)
1249    {
1250        WorkflowAwareContent wContent = (WorkflowAwareContent) content;
1251        return wContent.getCurrentStepId();
1252    }
1253
1254    /**
1255     * check if a content is published ot not
1256     * @param contentId id of the content
1257     * @return "validated" or "draft", "" if not found
1258     */
1259    public static String getContentStatus(String contentId)
1260    {
1261        AmetysObject resolvedById = _ametysObjectResolver.resolveById(contentId);
1262        if (resolvedById instanceof Content)
1263        {
1264            Content content = (Content) resolvedById;
1265            return getContentStatus(content);
1266        }
1267        else
1268        {
1269            return "";
1270        }
1271    }
1272    /**
1273     * check if a content is published ot not
1274     * @param content content
1275     * @return "validated" or "draft"
1276     */
1277    public static String getContentStatus(Content content)
1278    {
1279        return isContentLive(content) ? "validated" : "draft";
1280    }
1281    /**
1282     * check if a content is published ot not
1283     * @param contentId id of the content
1284     * @return true if validated
1285     */
1286    public static boolean isContentLive(String contentId)
1287    {
1288        AmetysObject resolvedById = _ametysObjectResolver.resolveById(contentId);
1289        if (resolvedById instanceof Content)
1290        {
1291            Content content = (Content) resolvedById;
1292            return isContentLive(content);
1293        }
1294        else
1295        {
1296            return false;
1297        }
1298    }
1299    /**
1300     * check if a content is published ot not
1301     * @param content content
1302     * @return true if validated
1303     */
1304    public static boolean isContentLive(Content content)
1305    {
1306        return Arrays.asList(((VersionableAmetysObject) content).getLabels()).contains(CmsConstants.LIVE_LABEL);
1307    }
1308    /**
1309     * Get the position of this zoneItem in the zone, or -1 if not found
1310     * @param zoneItemId zoneitem to search
1311     * @return zero based position, -1 if not found
1312     * @throws UnknownAmetysObjectException If zone item is not found
1313     * @throws AmetysRepositoryException If an error occurred
1314     */
1315    public static long getZoneItemPosition(String zoneItemId) throws UnknownAmetysObjectException, AmetysRepositoryException
1316    {
1317        ZoneItem zoneItem = _ametysObjectResolver.resolveById(zoneItemId);
1318        return getZoneItemPosition(zoneItem);
1319    }
1320    
1321    /**
1322     * Get the position of this zoneItem in its parent zone, or -1 if not found
1323     * @param zoneItem zoneitem
1324     * @return zero based position, -1 if not found
1325     */
1326    public static long getZoneItemPosition(ZoneItem zoneItem)
1327    {
1328        AmetysObject parent = zoneItem.getParent();
1329        if (parent instanceof DefaultTraversableAmetysObject)
1330        {
1331            return ((DefaultTraversableAmetysObject) parent).getChildPosition(zoneItem);
1332        }
1333        return -1; // virtual page
1334    }
1335    
1336    /**
1337     * Return the size of a zone
1338     * @param zoneName zone name
1339     * @param pageId page Id
1340     * @return size of the zone (-1 if not found)
1341     */
1342    public static long getZoneSize(String zoneName, String pageId)
1343    {
1344        Page page = _ametysObjectResolver.resolveById(pageId);
1345        Zone zone = page.getZone(zoneName);
1346        return getZoneSize(zone);
1347    }
1348    
1349    /**
1350     * Return the size of a zone
1351     * @param zone the zone
1352     * @return size of the zone or -1 is zone is null
1353     */
1354    public static long getZoneSize(Zone zone)
1355    {
1356        if (zone != null)
1357        {
1358            return zone.getZoneItems().getSize();
1359        }
1360        return -1;
1361    }
1362    /**
1363     * Test if a page is modifiable
1364     * @param pageId id of the page to check
1365     * @return true if the page is modifiable, false otherwise
1366     */
1367    public static boolean isPageModifiable(String pageId)
1368    {
1369        try
1370        {
1371            AmetysObject resolveById = _ametysObjectResolver.resolveById(pageId);
1372            return resolveById instanceof ModifiablePage;
1373        }
1374        catch (AmetysRepositoryException e)
1375        {
1376            // Nothing, this is not a page
1377        }
1378        
1379        return false;
1380    }
1381    
1382    /**
1383     * Returns the ids of the pages tagged with the specified tag in default workspace
1384     * @param tag The tag id
1385     * @return Array of pages ids
1386     */
1387    public static NodeList findPagesIdsByTagInDefaultWorkspace(String tag)
1388    {
1389        return findPagesIdsByTagInDefaultWorkspace(tag, false);
1390    }
1391    
1392    /**
1393     * Returns the ids of the pages tagged with the specified tag  in default workspace
1394     * @param tag The tag id
1395     * @param checkReadAccess true to return only pages with read access for current user
1396     * @return Array of pages ids
1397     */
1398    public static NodeList findPagesIdsByTagInDefaultWorkspace(String tag, boolean checkReadAccess)
1399    {
1400        Request request = ContextHelper.getRequest(_context);
1401        
1402        // Retrieve the current workspace.
1403        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
1404        
1405        try
1406        {
1407            // Force default workspace
1408            // The tagged page can be a node page, available of front only when a first subpage will be created
1409            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, RepositoryConstants.DEFAULT_WORKSPACE);
1410            return AmetysXSLTHelper.findPagesIdsByTag(tag, checkReadAccess);
1411        }
1412        finally
1413        {
1414         // Restore workspace
1415            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
1416        }
1417    }
1418    /**
1419     * Returns the ids of the pages tagged with the specified tag in default workspace
1420     * @param sitename The site id
1421     * @param lang The language code
1422     * @param tag The tag id
1423     * @return Array of pages ids
1424     */
1425    public static NodeList findPagesIdsByTagInDefaultWorkspace(String sitename, String lang, String tag)
1426    {
1427        return findPagesIdsByTagInDefaultWorkspace(sitename, lang, tag, false);
1428    }
1429    
1430    /**
1431     * Returns the ids of the pages tagged with the specified tag in default workspace
1432     * @param sitename The site id
1433     * @param lang The language code
1434     * @param tag The tag id
1435     * @param checkReadAccess true to return only pages with read access for current user
1436     * @return Array of pages ids
1437     */
1438    public static NodeList findPagesIdsByTagInDefaultWorkspace(String sitename, String lang, String tag, boolean checkReadAccess)
1439    {
1440        Request request = ContextHelper.getRequest(_context);
1441        
1442        // Retrieve the current workspace.
1443        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
1444        
1445        try
1446        {
1447            // Force default workspace
1448            // The tagged page can be a node page, available of front only when a first subpage will be created
1449            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, RepositoryConstants.DEFAULT_WORKSPACE);
1450            return AmetysXSLTHelper.findPagesIdsByTag(sitename, lang, tag, checkReadAccess);
1451        }
1452        finally
1453        {
1454         // Restore workspace
1455            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
1456        }
1457    }
1458}