001/*
002 *  Copyright 2012 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 */
016
017package org.ametys.core.util;
018
019import java.util.ArrayList;
020import java.util.Collection;
021import java.util.Date;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Set;
026
027import org.apache.avalon.framework.context.Context;
028import org.apache.avalon.framework.context.ContextException;
029import org.apache.avalon.framework.context.Contextualizable;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.avalon.framework.service.Serviceable;
033import org.apache.cocoon.components.ContextHelper;
034import org.apache.cocoon.environment.Request;
035import org.apache.cocoon.xml.dom.DOMBuilder;
036import org.apache.commons.lang3.StringUtils;
037import org.apache.commons.text.StringEscapeUtils;
038import org.apache.commons.text.translate.LookupTranslator;
039import org.slf4j.Logger;
040import org.slf4j.LoggerFactory;
041import org.w3c.dom.Node;
042import org.w3c.dom.NodeList;
043import org.xml.sax.SAXException;
044
045import org.ametys.core.DevMode;
046import org.ametys.core.DevMode.DEVMODE;
047import org.ametys.core.group.Group;
048import org.ametys.core.group.GroupIdentity;
049import org.ametys.core.group.GroupManager;
050import org.ametys.core.right.RightManager;
051import org.ametys.core.right.RightManager.RightResult;
052import org.ametys.core.user.CurrentUserProvider;
053import org.ametys.core.user.User;
054import org.ametys.core.user.UserIdentity;
055import org.ametys.core.user.UserManager;
056import org.ametys.core.user.directory.NotUniqueUserException;
057import org.ametys.core.util.dom.AmetysNodeList;
058import org.ametys.core.util.dom.MapElement;
059import org.ametys.core.util.dom.StringElement;
060import org.ametys.core.version.Version;
061import org.ametys.core.version.VersionsHandler;
062import org.ametys.plugins.core.user.UserHelper;
063import org.ametys.runtime.config.Config;
064import org.ametys.runtime.i18n.I18nizableText;
065import org.ametys.runtime.plugin.PluginsManager;
066import org.ametys.runtime.servlet.RuntimeConfig;
067import org.ametys.runtime.workspace.WorkspaceManager;
068import org.ametys.runtime.workspace.WorkspaceMatcher;
069
070/**
071 * Helper component to be used from XSL stylesheets.
072 */
073public class AmetysXSLTHelper implements Contextualizable, Serviceable
074{
075    /** The logger */
076    protected static final Logger _LOGGER = LoggerFactory.getLogger(AmetysXSLTHelper.class.getName());
077    
078    /** The i18n utils instance */
079    protected static I18nUtils _i18nUtils;
080    
081    /** The versions handler */
082    protected static VersionsHandler _versionHandler;
083
084    /** The current user provider */
085    protected static CurrentUserProvider _currentUserProvider;
086    /** The groups manager */
087    protected static GroupManager _groupManager;
088    /** The user helper */
089    protected static UserHelper _userHelper;
090    /** The json utils */
091    protected static JSONUtils _jsonUtils;
092    /** The right manager */
093    protected static RightManager _rightManager;
094    /** The user manager */
095    protected static UserManager _userManager;
096    
097    private static Context _context;
098
099    @Override
100    public void contextualize(Context context) throws ContextException
101    {
102        _context = context;
103    }
104    
105    public void service(ServiceManager manager) throws ServiceException
106    {
107        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
108        _versionHandler = (VersionsHandler) manager.lookup(VersionsHandler.ROLE);
109        
110        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
111        _groupManager = (GroupManager) manager.lookup(GroupManager.ROLE);
112        _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE);
113        _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE);
114        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
115        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
116    }
117    
118    /**
119     * Returns the current URI prefix.
120     * @return the current URI prefix.
121     */
122    public static String uriPrefix()
123    {
124        return uriPrefix(true);
125    }
126    
127    /**
128     * Returns the current URI prefix.
129     * @param withWorkspaceURI true to add the workspace URI (recommended)
130     * @return the current URI prefix.
131     */
132    public static String uriPrefix(boolean withWorkspaceURI)
133    {
134        return getUriPrefix(withWorkspaceURI);
135    }
136    
137    /**
138     * Returns the absolute URI prefix.
139     * @return the absolute URI prefix.
140     */
141    public static String absoluteUriPrefix()
142    {
143        return absoluteUriPrefix(true);
144    }
145    
146    /**
147     * Returns the absolute URI prefix.
148     * @param withWorkspaceURI true to add the workspace URI (recommended)
149     * @return the absolute URI prefix.
150     */
151    public static String absoluteUriPrefix(boolean withWorkspaceURI)
152    {
153        return getAbsoluteUriPrefix(withWorkspaceURI);
154    }
155    
156    /**
157     * Return the current workspace name
158     * @return The workspace name. Cannot be empty.
159     */
160    public static String workspaceName()
161    {
162        return getWorkspaceName();
163    }
164    
165    /**
166     * Return the current workspace URI
167     * @return The workspace name. Can be empty.
168     */
169    public static String workspacePrefix()
170    {
171        return getWorkspacePrefix();
172    }
173    
174    /**
175     * Return the current workspace theme name
176     * @return The name
177     */
178    public static String workspaceTheme()
179    {
180        Request request = ContextHelper.getRequest(_context);
181        return (String) request.getAttribute(WorkspaceMatcher.WORKSPACE_THEME);
182    }
183
184    /**
185     * Return the current workspace theme url
186     * @return The url without any prefix
187     */
188    public static String workspaceThemeURL()
189    {
190        Request request = ContextHelper.getRequest(_context);
191        
192        String workspaceThemeUrl = (String) request.getAttribute(WorkspaceMatcher.WORKSPACE_THEME_URL);
193        if (workspaceThemeUrl == null)
194        {
195            // fallback to the default workspace
196            String workspaceName = RuntimeConfig.getInstance().getDefaultWorkspace();
197            WorkspaceManager wm = WorkspaceManager.getInstance();
198            if (wm.getWorkspaceNames().contains(workspaceName))
199            {
200                workspaceThemeUrl = wm.getWorkspaces().get(workspaceName).getThemeURL();
201            }
202        }
203        
204        return workspaceThemeUrl;
205    }
206    
207    /**
208     * Get the application context path. Can be empty if the application
209     * resides in the root context. Use it to create a link beginning with
210     * the application root.
211     * @param withWorkspaceURI true to add the workspace URI (recommended)
212     * @return The application context path with workspace URI
213     * @see Request#getContextPath()
214     */
215    protected static String getUriPrefix(boolean withWorkspaceURI)
216    {
217        Request request = ContextHelper.getRequest(_context);
218        String workspaceURI = withWorkspaceURI ? getWorkspacePrefix() : "";
219        
220        return request.getContextPath() + workspaceURI;
221    }
222    
223    /**
224     * Get the absolutized version of the context path. Use it to create an absolute
225     * link beginning with the application root, for instance when sending a mail
226     * linking to the application.
227     * @param withWorkspaceURI true to add the workspace URI (recommended)
228     * @return The absolute context path.
229     */
230    protected static String getAbsoluteUriPrefix(boolean withWorkspaceURI)
231    {
232        Request request = ContextHelper.getRequest(_context);
233        
234        String uriPrefix = getUriPrefix(withWorkspaceURI);
235        
236        if (!uriPrefix.startsWith("http"))
237        {
238            uriPrefix = request.getScheme() + "://" + request.getServerName() + (request.getServerPort() != 80 ? ":" + request.getServerPort() : "") + uriPrefix;
239        }
240        
241        return uriPrefix;
242    }  
243    
244    /**
245     * Return the current workspace name
246     * @return The workspace name. Cannot be empty.
247     */
248    protected static String getWorkspaceName()
249    {
250        Request request = ContextHelper.getRequest(_context);
251        return (String) request.getAttribute(WorkspaceMatcher.WORKSPACE_NAME);
252    }
253    
254    /**
255     * Return the current workspace URI
256     * @return The workspace name. Can be empty for the default workspace.
257     */
258    protected static String getWorkspacePrefix()
259    {
260        Request request = ContextHelper.getRequest(_context);
261        return (String) request.getAttribute(WorkspaceMatcher.WORKSPACE_URI);
262    }
263
264    /**
265     * Returns the configuration value associated with the given parameter.
266     * @param id the configuration parameter.
267     * @return the configuration value associated with the given parameter.
268     */
269    public static Object config(String id)
270    {
271        if (Config.getInstance() != null)
272        {
273            return Config.getInstance().getValue(id);
274        }
275        else
276        {
277            return null;
278        }
279    }
280    
281    /**
282     * Return the value of a request parameter.
283     * @param parameter the parameter name.
284     * @return the request parameter.
285     */
286    public static String requestParameter(String parameter)
287    {
288        Request request = ContextHelper.getRequest(_context);
289        return request.getParameter(parameter);
290    }
291    
292    /**
293     * Translate an i18n key using current user language.
294     * @param key The key to translate. Specify the catalog this way: "catalogue:KEY"
295     * @return The translation or null.
296     */
297    public static String translate(String key)
298    {
299        return translate(key, null, null);
300    }
301    
302    /**
303     * Translate an i18n key
304     * @param key The key to translate. Specify the catalog this way: "catalogue:KEY"
305     * @param lang The language. Can be null to use current user language.
306     * @return The translation or null.
307     */
308    public static String translate(String key, String lang)
309    {
310        return translate(key, lang, null);
311    }
312    
313    /**
314     * Translate an i18n key
315     * @param key The key to translate. Specify the catalog this way: "catalogue:KEY"
316     * @param lang The language. Can be null to use current user language.
317     * @param parameters The key parameters. Can be empty.
318     * @return The translation or null.
319     */
320    public static String translate(String key, String lang, NodeList parameters)
321    {
322        List<String> i18nparams = new ArrayList<>();
323        if (parameters != null && parameters.getLength() == 1)
324        {
325            NodeList childNodes = parameters.item(0).getChildNodes();
326            for (int i = 0; i < childNodes.getLength(); i++) 
327            {  
328                i18nparams.add(childNodes.item(i).getTextContent());
329            }
330        }
331        
332        I18nizableText i18nKey = new I18nizableText(null, key, i18nparams);
333        return _i18nUtils.translate(i18nKey, lang);
334    }
335    
336    /**
337     * Escape the given string to be used as JS variable.
338     * @param str the string to escape.
339     * @return the escaped String.
340     */
341    public static String escapeJS(String str)
342    {
343        return StringEscapeUtils.escapeEcmaScript(str);
344    }
345    
346    /**
347     * Escape the given URL, meant to be used in eg. a background-image CSS style
348     * @param url the URL to escape
349     * @return the escaped URL
350     */
351    public static String escapeURLforCSS(String url)
352    {
353        Map<CharSequence, CharSequence> escapeMap = Map.of("'", "\\'",
354                                                           "\"", "\\\"",
355                                                           "(", "\\(",
356                                                           ")", "\\)");
357        
358        return new LookupTranslator(escapeMap).translate(url);
359    }
360    
361    /**
362     * Split the text.
363     * @param textToSplit the text to split.
364     * @param tokenizers the tokenizer characters.
365     * @param startIndex the minimum number of characters of the result string
366     * @return the split text.
367     */
368    public static String splitText(String textToSplit, String tokenizers, int startIndex)
369    {
370        String tokenizableText = textToSplit.substring(startIndex != 0 ? startIndex - 1 : 0, textToSplit.length());
371        
372        int tokenPlace = StringUtils.indexOfAny(tokenizableText, tokenizers);
373        
374        if (tokenPlace == -1)
375        {
376            return textToSplit;
377        }
378        else
379        {
380            return textToSplit.substring(0, startIndex - 1 + tokenPlace);
381        }
382    }
383    
384    /**
385     * Split the text.
386     * @param textToSplit the text to split.
387     * @param tokenizers the tokenizer characters.
388     * @param maxCharacters the maximum number of characters of the result string
389     * @param currentCharactersNumber the current character number.
390     * @return the split text.
391     */
392    @Deprecated
393    public static String splitText(String textToSplit, String tokenizers, int maxCharacters, int currentCharactersNumber)
394    {
395        int tokenStartIndex = maxCharacters - currentCharactersNumber - 1;
396        String tokenizableText = textToSplit.substring(tokenStartIndex, textToSplit.length());
397        
398        int tokenPlace = StringUtils.indexOfAny(tokenizableText, tokenizers);
399        
400        if (tokenPlace == -1)
401        {
402            return textToSplit;
403        }
404        else
405        {
406            return textToSplit.substring(0, tokenStartIndex + tokenPlace);
407        }
408    }
409    
410    /**
411     * Get the versions of the application.
412     * Default VersionsHandler impl will return Ametys and Application versions
413     * @return The versions &lt;Version&gt;&lt;Version&gt;&lt;Name&gt;X&lt;/Name&gt;&lt;Version&gt;X&lt;/Version&gt;&lt;Date&gt;X&lt;/Date&gt;&lt;/Version&gt;&lt;/Versions&gt; (empty tags are removed)
414     */
415    public static Node versions()
416    {
417        Map<String, Object> versionsMap = new HashMap<>();
418
419        List<Object> versionList = new ArrayList<>();
420
421        for (Version version : _versionHandler.getVersions())
422        {
423            Map<String, Object> versionMap = new HashMap<>();
424
425            String componentName = version.getName();
426            String componentVersion = version.getVersion();
427            String componentDate =  DateUtils.dateToString(version.getDate());
428            
429            if (StringUtils.isNotEmpty(componentName))
430            {
431                versionMap.put("Name", componentName);
432            }
433            if (StringUtils.isNotEmpty(componentVersion))
434            {
435                versionMap.put("Version", componentVersion);
436            }
437            if (StringUtils.isNotEmpty(componentDate))
438            {
439                versionMap.put("Date", componentDate);
440            }
441            
442            versionList.add(versionMap);
443        }
444        
445        versionsMap.put("Component", versionList);
446
447        return new MapElement("Versions", versionsMap);
448    }
449    
450    /**
451     * Get the current mode of the application for the current user.
452     * @return True if the application is in developer mode, false if in production mode.
453     */
454    public static boolean isDeveloperMode()
455    {
456        Request request = ContextHelper.getRequest(_context);
457        
458        DEVMODE developerMode = DevMode.getDeveloperMode(request);
459        return developerMode == DEVMODE.DEVELOPMENT 
460                || developerMode == DEVMODE.SUPER_DEVELOPPMENT;
461    }
462    
463    /**
464     * Return the user
465     * @return The current connected user object or null
466     * @throws SAXException if a problem occured while getting the user
467     */
468    public static Node user() throws SAXException
469    {
470        UserIdentity userIdentity = _currentUserProvider.getUser();
471        if (userIdentity != null)
472        {
473            return user(userIdentity.getLogin(), userIdentity.getPopulationId());
474        }
475        
476        return null;
477    }
478    
479    /**
480     * Return the given user
481     * @param userIdentity the concerned user's login + population
482     * @return The informations about the given user
483     * @throws SAXException If an error occurred while saxing the user
484     */
485    public static Node user(String userIdentity) throws SAXException
486    {
487        UserIdentity userIdentityObject = UserIdentity.stringToUserIdentity(userIdentity);
488        if (userIdentityObject == null)
489        {
490            return null;
491        }
492        else
493        {
494            return user(userIdentityObject.getLogin(), userIdentityObject.getPopulationId());
495        }
496    }
497    
498    /**
499     * Return the given user
500     * @param login the concerned user's login
501     * @param populationId the concerned user's population id
502     * @return The informations about the given user
503     * @throws SAXException If an error occurred while saxing the user
504     */
505    public static Node user(String login, String populationId) throws SAXException
506    {
507        DOMBuilder domBuilder = new DOMBuilder();
508        
509        UserIdentity userIdentity = new UserIdentity(login, populationId);
510        _userHelper.saxUserIdentity(userIdentity, domBuilder);
511        
512        return domBuilder.getDocument();
513    }
514    
515    /**
516     * Return the given user
517     * @param email the concerned user's email
518     * @param populationId the concerned user's population id
519     * @return The informations about the given user
520     * @throws SAXException If an error occurred while saxing the user
521     */
522    public static Node userByMail(String email, String populationId) throws SAXException
523    {
524        try
525        {
526            User user = _userManager.getUserByEmail(populationId, email);
527            if (user != null)
528            {
529                return user(UserIdentity.userIdentityToString(user.getIdentity()));
530            }
531        }
532        catch (NotUniqueUserException e)
533        {
534            return null;
535        }
536        return null;
537    }
538    
539    /**
540     * Returns the list of the current user's groups.
541     * @return the list of the current user's groups. Can be null if there is no connected user.
542     */
543    public static NodeList groups()
544    {
545        UserIdentity userIdentity = _currentUserProvider.getUser();
546        return userIdentity != null ? groups(userIdentity.getLogin(), userIdentity.getPopulationId()) : null;
547    }
548    
549    /**
550     * Returns the of the given user's group.
551     * @param userIdentity the concerned user's login + population
552     * @return the of the given user's group.
553     */
554    public static NodeList groups(String userIdentity)
555    {
556        UserIdentity userIdentityObject = UserIdentity.stringToUserIdentity(userIdentity);
557        if (userIdentityObject == null)
558        {
559            return null;
560        }
561        else
562        {
563            return groups(userIdentityObject.getLogin(), userIdentityObject.getPopulationId());
564        }
565    }
566    
567    /**
568     * Returns the of the given user's group.
569     * @param login the concerned user's login.
570     * @param populationId the concerned user's population.
571     * @return the of the given user's group.
572     */
573    public static NodeList groups(String login, String populationId)
574    {
575        ArrayList<Node> groups = new ArrayList<>();
576        
577        Set<GroupIdentity> userGroups = _groupManager.getUserGroups(new UserIdentity(login, populationId));
578        for (GroupIdentity groupId : userGroups)
579        {
580            Group group = _groupManager.getGroup(groupId);
581            if (group != null)
582            {
583                Map<String, String> attributes = new HashMap<>();
584                attributes.put("name", groupId.getId());
585                attributes.put("directory", groupId.getDirectoryId());
586                groups.add(new StringElement("group", attributes, group.getLabel()));
587            }
588        }
589        
590        return new AmetysNodeList(groups);
591    }
592
593    /**
594     * Parse a JSON string as a Map and return the value with the desired key
595     * @param jsonString the JSON representation of the object.
596     * @param key name of the value to return
597     * @return the value as a String, or empty string if an error occurred or the key was not found.
598     */
599    public static String getValueFromJsonObject(String jsonString, String key)
600    {
601        try
602        {
603            Map<String, Object> jsonMap = _jsonUtils.convertJsonToMap(jsonString);
604            if (jsonMap.containsKey(key))
605            {
606                Object value = jsonMap.get(key);
607                if (value instanceof Map || value instanceof Collection)
608                {
609                    _LOGGER.warn("Unable to get string value for key '{}' from json object {}: the value can not be a map nor collection", key, jsonString);
610                }
611                else if (value instanceof Date)
612                {
613                    return DateUtils.dateToString((Date) value);
614                }
615                else
616                {
617                    return value.toString();
618                }
619            }
620        }
621        catch (Exception e)
622        {
623            _LOGGER.warn("Unable to parse json object {}", jsonString, e);
624        }
625        
626        return "";
627    }
628    
629    /**
630     * Determines if the current logged user has right on a String context
631     * @param rightId The id of right
632     * @param objectCtx the context. Can be null to search on any context.
633     * @return true if the current user is allowed, false otherwise
634     */
635    public static boolean hasRight(String rightId, String objectCtx)
636    {
637        return _rightManager.currentUserHasRight(rightId, objectCtx) == RightResult.RIGHT_ALLOW;
638    }
639
640    /**
641     * Check if a given plugin is enabled
642     * @param plugin the plugin's name.
643     * @return the plugin is enabled
644     */
645    public static boolean isPluginEnabled(String plugin)
646    {
647        return PluginsManager.getInstance().getPluginNames().contains(plugin);
648    }
649}