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