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