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