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