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 string to be used as text, without interpreting HTML inside of it.
356     * @param str the string to escape.
357     * @return the escaped String.
358     */
359    public static String escapeHTML(String str)
360    {
361        return org.ametys.core.util.StringUtils.escapeHTML(str);
362    }
363    
364    /**
365     * Escape the given URL, meant to be used in eg. a background-image CSS style
366     * @param url the URL to escape
367     * @return the escaped URL
368     */
369    public static String escapeURLforCSS(String url)
370    {
371        Map<CharSequence, CharSequence> escapeMap = Map.of("'", "\\'",
372                                                           "\"", "\\\"",
373                                                           "(", "\\(",
374                                                           ")", "\\)");
375        
376        return new LookupTranslator(escapeMap).translate(url);
377    }
378    
379    /**
380     * Split the text.
381     * @param textToSplit the text to split.
382     * @param tokenizers the tokenizer characters.
383     * @param startIndex the minimum number of characters of the result string
384     * @return the split text.
385     */
386    public static String splitText(String textToSplit, String tokenizers, int startIndex)
387    {
388        String tokenizableText = textToSplit.substring(startIndex != 0 ? startIndex - 1 : 0, textToSplit.length());
389        
390        int tokenPlace = StringUtils.indexOfAny(tokenizableText, tokenizers);
391        
392        if (tokenPlace == -1)
393        {
394            return textToSplit;
395        }
396        else
397        {
398            return textToSplit.substring(0, startIndex - 1 + tokenPlace);
399        }
400    }
401    
402    /**
403     * Split the text.
404     * @param textToSplit the text to split.
405     * @param tokenizers the tokenizer characters.
406     * @param maxCharacters the maximum number of characters of the result string
407     * @param currentCharactersNumber the current character number.
408     * @return the split text.
409     */
410    @Deprecated
411    public static String splitText(String textToSplit, String tokenizers, int maxCharacters, int currentCharactersNumber)
412    {
413        int tokenStartIndex = maxCharacters - currentCharactersNumber - 1;
414        String tokenizableText = textToSplit.substring(tokenStartIndex, textToSplit.length());
415        
416        int tokenPlace = StringUtils.indexOfAny(tokenizableText, tokenizers);
417        
418        if (tokenPlace == -1)
419        {
420            return textToSplit;
421        }
422        else
423        {
424            return textToSplit.substring(0, tokenStartIndex + tokenPlace);
425        }
426    }
427    
428    /**
429     * Get the versions of the application.
430     * Default VersionsHandler impl will return Ametys and Application versions
431     * @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)
432     */
433    public static Node versions()
434    {
435        Map<String, Object> versionsMap = new HashMap<>();
436
437        List<Object> versionList = new ArrayList<>();
438
439        for (Version version : _versionHandler.getVersions())
440        {
441            Map<String, Object> versionMap = new HashMap<>();
442
443            String componentName = version.getName();
444            String componentVersion = version.getVersion();
445            String componentDate =  DateUtils.dateToString(version.getDate());
446            
447            if (StringUtils.isNotEmpty(componentName))
448            {
449                versionMap.put("Name", componentName);
450            }
451            if (StringUtils.isNotEmpty(componentVersion))
452            {
453                versionMap.put("Version", componentVersion);
454            }
455            if (StringUtils.isNotEmpty(componentDate))
456            {
457                versionMap.put("Date", componentDate);
458            }
459            
460            versionList.add(versionMap);
461        }
462        
463        versionsMap.put("Component", versionList);
464
465        return new MapElement("Versions", versionsMap);
466    }
467    
468    /**
469     * Get the current mode of the application for the current user.
470     * @return True if the application is in developer mode, false if in production mode.
471     */
472    public static boolean isDeveloperMode()
473    {
474        return isDeveloperMode(true);
475    }
476    
477    /**
478     * Get the current mode of the application for the current user.
479     * @param allowRequestOverride true to take the request into account when determining the mode
480     * @return True if the application is in developer mode, false if in production mode.
481     */
482    public static boolean isDeveloperMode(boolean allowRequestOverride)
483    {
484        Request request = allowRequestOverride ? ContextHelper.getRequest(_context) : null;
485        
486        DEVMODE developerMode = DevMode.getDeveloperMode(request);
487        return developerMode == DEVMODE.DEVELOPMENT
488                || developerMode == DEVMODE.SUPER_DEVELOPPMENT;
489    }
490    
491    /**
492     * Return the user
493     * @return The current connected user object or null
494     * @throws SAXException if a problem occured while getting the user
495     */
496    public static Node user() throws SAXException
497    {
498        UserIdentity userIdentity = _currentUserProvider.getUser();
499        if (userIdentity != null)
500        {
501            return user(userIdentity.getLogin(), userIdentity.getPopulationId());
502        }
503        
504        return null;
505    }
506    
507    /**
508     * Return the given user
509     * @param userIdentity the concerned user's login + population
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 userIdentity) throws SAXException
514    {
515        UserIdentity userIdentityObject = UserIdentity.stringToUserIdentity(userIdentity);
516        if (userIdentityObject == null)
517        {
518            return null;
519        }
520        else
521        {
522            return user(userIdentityObject.getLogin(), userIdentityObject.getPopulationId());
523        }
524    }
525    
526    /**
527     * Return the given user
528     * @param login the concerned user's login
529     * @param populationId the concerned user's population id
530     * @return The informations about the given user
531     * @throws SAXException If an error occurred while saxing the user
532     */
533    public static Node user(String login, String populationId) throws SAXException
534    {
535        DOMBuilder domBuilder = new DOMBuilder();
536        
537        UserIdentity userIdentity = new UserIdentity(login, populationId);
538        _userHelper.saxUserIdentity(userIdentity, domBuilder);
539        
540        return domBuilder.getDocument();
541    }
542    
543    /**
544     * Return the given user
545     * @param email the concerned user's email
546     * @param populationId the concerned user's population id
547     * @return The informations about the given user
548     * @throws SAXException If an error occurred while saxing the user
549     */
550    public static Node userByMail(String email, String populationId) throws SAXException
551    {
552        try
553        {
554            User user = _userManager.getUserByEmail(populationId, email);
555            if (user != null)
556            {
557                return user(UserIdentity.userIdentityToString(user.getIdentity()));
558            }
559        }
560        catch (NotUniqueUserException e)
561        {
562            return null;
563        }
564        return null;
565    }
566    
567    /**
568     * Returns the list of the current user's groups.
569     * @return the list of the current user's groups. Can be null if there is no connected user.
570     */
571    public static NodeList groups()
572    {
573        UserIdentity userIdentity = _currentUserProvider.getUser();
574        return userIdentity != null ? groups(userIdentity.getLogin(), userIdentity.getPopulationId()) : null;
575    }
576    
577    /**
578     * Returns the of the given user's group.
579     * @param userIdentity the concerned user's login + population
580     * @return the of the given user's group.
581     */
582    public static NodeList groups(String userIdentity)
583    {
584        UserIdentity userIdentityObject = UserIdentity.stringToUserIdentity(userIdentity);
585        if (userIdentityObject == null)
586        {
587            return null;
588        }
589        else
590        {
591            return groups(userIdentityObject.getLogin(), userIdentityObject.getPopulationId());
592        }
593    }
594    
595    /**
596     * Returns the of the given user's group.
597     * @param login the concerned user's login.
598     * @param populationId the concerned user's population.
599     * @return the of the given user's group.
600     */
601    public static NodeList groups(String login, String populationId)
602    {
603        ArrayList<Node> groups = new ArrayList<>();
604        
605        Set<GroupIdentity> userGroups = _groupManager.getUserGroups(new UserIdentity(login, populationId));
606        for (GroupIdentity groupId : userGroups)
607        {
608            Group group = _groupManager.getGroup(groupId);
609            if (group != null)
610            {
611                Map<String, String> attributes = new HashMap<>();
612                attributes.put("name", groupId.getId());
613                attributes.put("directory", groupId.getDirectoryId());
614                groups.add(new StringElement("group", attributes, group.getLabel()));
615            }
616        }
617        
618        return new AmetysNodeList(groups);
619    }
620
621    /**
622     * Parse a JSON string as a Map and return the value with the desired key
623     * @param jsonString the JSON representation of the object.
624     * @param key name of the value to return
625     * @return the value as a String, or empty string if an error occurred or the key was not found.
626     */
627    public static String getValueFromJsonObject(String jsonString, String key)
628    {
629        try
630        {
631            Map<String, Object> jsonMap = _jsonUtils.convertJsonToMap(jsonString);
632            if (jsonMap.containsKey(key))
633            {
634                Object value = jsonMap.get(key);
635                if (value instanceof Map || value instanceof Collection)
636                {
637                    _LOGGER.warn("Unable to get string value for key '{}' from json object {}: the value can not be a map nor collection", key, jsonString);
638                }
639                else if (value instanceof Date)
640                {
641                    return DateUtils.dateToString((Date) value);
642                }
643                else
644                {
645                    return value.toString();
646                }
647            }
648        }
649        catch (Exception e)
650        {
651            _LOGGER.warn("Unable to parse json object {}", jsonString, e);
652        }
653        
654        return "";
655    }
656    
657    /**
658     * Determines if the current logged user has right on a String context
659     * @param rightId The id of right
660     * @param objectCtx the context. Can be null to search on any context.
661     * @return true if the current user is allowed, false otherwise
662     */
663    public static boolean hasRight(String rightId, String objectCtx)
664    {
665        return _rightManager.currentUserHasRight(rightId, objectCtx) == RightResult.RIGHT_ALLOW;
666    }
667
668    /**
669     * Check if a given plugin is enabled
670     * @param plugin the plugin's name.
671     * @return the plugin is enabled
672     */
673    public static boolean isPluginEnabled(String plugin)
674    {
675        return PluginsManager.getInstance().getPluginNames().contains(plugin);
676    }
677    
678    /**
679     * Computes the base 64 representation of the image at the specified path in the given plugin.
680     * @param plugin the plugin's name.
681     * @param path the resource path.
682     * @return the base 64 encoding for the given resource.
683     * @throws IOException if an error occurs when trying to get the file
684     * @throws MalformedURLException if the url is invalid
685     */
686    public static String pluginImageBase64(String plugin, String path) throws MalformedURLException, IOException
687    {
688        Source source = null;
689        try
690        {
691            source = _sourceResolver.resolveURI("plugin:" + plugin + "://resources/" + path);
692            return _getResourceBase64(source);
693        }
694        finally
695        {
696            if (source != null)
697            {
698                _sourceResolver.release(source);
699            }
700        }
701    }
702    
703    /**
704     * Get the base 64 encoding for the given source
705     * @param source the source
706     * @return the base 64 encoding of the source
707     */
708    protected static String _getResourceBase64(Source source)
709    {
710        if (source.exists())
711        {
712            
713            try (InputStream dataIs = source.getInputStream())
714            {
715                return ImageResolverHelper.resolveImageAsBase64(dataIs, source.getMimeType(), 0, 0, 0, 0);
716            }
717            catch (Exception e)
718            {
719                throw new IllegalStateException(e);
720            }
721        }
722
723        return "";
724    }
725    
726    /**
727     * Determine if a plugin with the given name is available
728     * @param pluginName the plugin name
729     * @return true if the plugin is available
730     */
731    public boolean pluginAvailable(String pluginName)
732    {
733        return PluginsManager.getInstance().getPluginLocation(pluginName) != null;
734    }
735}