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