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