001/*
002 *  Copyright 2015 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.plugins.linkdirectory;
017
018import java.text.Normalizer;
019import java.util.ArrayList;
020import java.util.HashMap;
021import java.util.Iterator;
022import java.util.List;
023import java.util.Map;
024import java.util.Set;
025import java.util.regex.Pattern;
026
027import org.apache.avalon.framework.component.Component;
028import org.apache.avalon.framework.context.Context;
029import org.apache.avalon.framework.context.ContextException;
030import org.apache.avalon.framework.context.Contextualizable;
031import org.apache.avalon.framework.service.ServiceException;
032import org.apache.avalon.framework.service.ServiceManager;
033import org.apache.avalon.framework.service.Serviceable;
034import org.apache.cocoon.components.ContextHelper;
035import org.apache.cocoon.environment.Request;
036import org.apache.cocoon.xml.AttributesImpl;
037import org.apache.cocoon.xml.XMLUtils;
038import org.apache.commons.collections.IteratorUtils;
039import org.apache.commons.lang3.ArrayUtils;
040import org.apache.commons.lang3.StringUtils;
041import org.apache.jackrabbit.util.ISO9075;
042import org.xml.sax.ContentHandler;
043import org.xml.sax.SAXException;
044
045import org.ametys.core.group.Group;
046import org.ametys.core.group.GroupIdentity;
047import org.ametys.core.group.GroupManager;
048import org.ametys.core.user.CurrentUserProvider;
049import org.ametys.core.user.User;
050import org.ametys.core.user.UserIdentity;
051import org.ametys.core.user.UserManager;
052import org.ametys.core.userpref.UserPreferencesException;
053import org.ametys.core.userpref.UserPreferencesManager;
054import org.ametys.plugins.explorer.resources.Resource;
055import org.ametys.plugins.linkdirectory.Link.LinkType;
056import org.ametys.plugins.linkdirectory.dynamic.DynamicInformationProviderExtensionPoint;
057import org.ametys.plugins.linkdirectory.repository.DefaultLink;
058import org.ametys.plugins.linkdirectory.repository.DefaultLinkFactory;
059import org.ametys.plugins.linkdirectory.repository.DefaultTheme;
060import org.ametys.plugins.linkdirectory.repository.DefaultThemeFactory;
061import org.ametys.plugins.linkdirectory.theme.ThemeExpression;
062import org.ametys.plugins.repository.AmetysObject;
063import org.ametys.plugins.repository.AmetysObjectIterable;
064import org.ametys.plugins.repository.AmetysObjectResolver;
065import org.ametys.plugins.repository.AmetysRepositoryException;
066import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
067import org.ametys.plugins.repository.TraversableAmetysObject;
068import org.ametys.plugins.repository.UnknownAmetysObjectException;
069import org.ametys.plugins.repository.metadata.BinaryMetadata;
070import org.ametys.plugins.repository.query.expression.Expression;
071import org.ametys.runtime.plugin.component.AbstractLogEnabled;
072import org.ametys.web.repository.page.Page;
073import org.ametys.web.repository.site.Site;
074import org.ametys.web.repository.site.SiteManager;
075import org.ametys.web.site.SiteConfigurationExtensionPoint;
076import org.ametys.web.userpref.FOUserPreferencesConstants;
077
078/**
079 * Link directory helper.
080 */
081public final class DirectoryHelper extends AbstractLogEnabled implements Component, Serviceable, Contextualizable
082{
083    /** The component role */
084    public static final String ROLE = DirectoryHelper.class.getName();
085
086    private static final String __PLUGIN_NODE_NAME = "linkdirectory";
087    
088    private static final String __LINKS_NODE_NAME = "ametys:directoryLinks";
089    
090    private static final String __THEMES_NODE_NAME = "ametys:themes";
091    
092    private static final String __USER_LINKS_NODE_NAME = "user-favorites";
093    
094    /** The Ametys object resolver */
095    private AmetysObjectResolver _ametysObjectResolver;
096    
097    /** The users manager */
098    private UserManager _userManager;
099
100    /** The groups manager */
101    private GroupManager _groupManager;
102    
103    /** The site manager */
104    private SiteManager _siteManager;
105    
106    /** The user preferences manager */
107    private UserPreferencesManager _userPreferencesManager;
108    
109    /** The current user provider */
110    private CurrentUserProvider _currentUserProvider;
111    
112    /** The site configuration extension point */
113    private SiteConfigurationExtensionPoint _siteConfiguration;
114
115    /** The context */
116    private Context _context;
117
118    private DynamicInformationProviderExtensionPoint _dynamicProviderEP;
119    
120    @Override
121    public void service(ServiceManager manager) throws ServiceException
122    {
123        _ametysObjectResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
124        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
125        _groupManager = (GroupManager) manager.lookup(GroupManager.ROLE);
126        _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE);
127        _userPreferencesManager = (UserPreferencesManager) manager.lookup(UserPreferencesManager.ROLE + ".FO");
128        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
129        _siteConfiguration = (SiteConfigurationExtensionPoint) manager.lookup(SiteConfigurationExtensionPoint.ROLE);
130        _dynamicProviderEP = (DynamicInformationProviderExtensionPoint) manager.lookup(DynamicInformationProviderExtensionPoint.ROLE);
131    }
132    
133    @Override
134    public void contextualize(Context context) throws ContextException
135    {
136        _context = context;
137    }
138    
139    /**
140     * Get the root plugin storage object.
141     * @param site the site.
142     * @return the root plugin storage object.
143     * @throws AmetysRepositoryException if a repository error occurs.
144     */
145    public ModifiableTraversableAmetysObject getPluginNode(Site site) throws AmetysRepositoryException
146    {
147        try
148        {
149            ModifiableTraversableAmetysObject pluginsNode = site.getRootPlugins();
150            
151            return getOrCreateNode(pluginsNode, __PLUGIN_NODE_NAME, "ametys:unstructured");
152        }
153        catch (AmetysRepositoryException e)
154        {
155            throw new AmetysRepositoryException("Error getting the link directory plugin node for site " + site.getName(), e);
156        }
157    }
158    
159    /**
160     * Get the links root node.
161     * @param site the site
162     * @param language the language.
163     * @return the links root node.
164     * @throws AmetysRepositoryException if a repository error occurs.
165     */
166    public ModifiableTraversableAmetysObject getLinksNode(Site site, String language) throws AmetysRepositoryException
167    {
168        try
169        {
170            // Get the root plugin node.
171            ModifiableTraversableAmetysObject pluginNode = getPluginNode(site);
172            
173            // Get or create the language node.
174            ModifiableTraversableAmetysObject langNode = getOrCreateNode(pluginNode, language, "ametys:unstructured");
175            
176            // Get or create the definitions container node in the language node and return it.
177            return getOrCreateNode(langNode, __LINKS_NODE_NAME, DefaultLinkFactory.LINK_ROOT_NODE_TYPE);
178        }
179        catch (AmetysRepositoryException e)
180        {
181            throw new AmetysRepositoryException("Error getting the link directory root node for site " + site.getName() + " and language " + language, e);
182        }
183    }
184    
185    /**
186     * Get the links root node for the given user.
187     * @param site the site
188     * @param language the language.
189     * @param user The user identity
190     * @return the links root node for the given user.
191     * @throws AmetysRepositoryException if a repository error occurs.
192     */
193    public ModifiableTraversableAmetysObject getLinksForUserNode(Site site, String language, UserIdentity user) throws AmetysRepositoryException
194    {
195        try
196        {
197            // Get the root plugin node.
198            ModifiableTraversableAmetysObject pluginNode = getPluginNode(site);
199            
200            // Get or create the user links node.
201            ModifiableTraversableAmetysObject userLinksNode = getOrCreateNode(pluginNode, __USER_LINKS_NODE_NAME, "ametys:unstructured");
202            // Get or create the population node.
203            ModifiableTraversableAmetysObject populationNode = getOrCreateNode(userLinksNode, user.getPopulationId(), "ametys:unstructured");
204            // Get or create the login node.
205            ModifiableTraversableAmetysObject loginNode = getOrCreateNode(populationNode, user.getLogin(), "ametys:unstructured");
206            // Get or create the language node.
207            ModifiableTraversableAmetysObject langNode = getOrCreateNode(loginNode, language, "ametys:unstructured");
208            
209            // Get or create the definitions container node in the language node and return it.
210            return getOrCreateNode(langNode, __LINKS_NODE_NAME, DefaultLinkFactory.LINK_ROOT_NODE_TYPE);
211        }
212        catch (AmetysRepositoryException e)
213        {
214            throw new AmetysRepositoryException("Error getting the link directory root node for user " + user + " and for site " + site.getName() + " and language " + language, e);
215        }
216    }
217
218    /**
219     * Get the themes storage node.
220     * @param site the site
221     * @param language the language.
222     * @return the themes storage node.
223     * @throws AmetysRepositoryException if a repository error occurs.
224     */
225    public ModifiableTraversableAmetysObject getThemesNode(Site site, String language) throws AmetysRepositoryException
226    {
227        try
228        {
229            // Get the root plugin node.
230            ModifiableTraversableAmetysObject pluginNode = getPluginNode(site);
231            
232            // Get or create the language node.
233            ModifiableTraversableAmetysObject langNode = getOrCreateNode(pluginNode, language, "ametys:unstructured");
234            
235            // Get or create the definitions container node in the language node and return it.
236            return getOrCreateNode(langNode, __THEMES_NODE_NAME, DefaultThemeFactory.THEME_ROOT_NODE_TYPE);
237        }
238        catch (AmetysRepositoryException e)
239        {
240            throw new AmetysRepositoryException("Error getting the themes node for site " + site.getName() + " and language " + language, e);
241        }
242    }
243    
244    /**
245     * Get the plugin node path
246     * @param siteName the site name.
247     * @return the plugin node path.
248     */
249    public String getPluginNodePath(String siteName)
250    {
251        return String.format("//element(%s, ametys:site)/ametys-internal:plugins/%s", siteName, __PLUGIN_NODE_NAME);
252    }
253    
254    /**
255     * Get the links root node path
256     * @param siteName the site name.
257     * @param language the language
258     * @return the links root node path.
259     */
260    public String getLinksNodePath(String siteName, String language)
261    {
262        return getPluginNodePath(siteName) + "/"  + language + "/" + __LINKS_NODE_NAME;
263    }
264    
265    /**
266     * Get the links root node path for the given user 
267     * @param siteName the site name.
268     * @param language the language
269     * @param user The user identity
270     * @return the links root node path for the given user.
271     */
272    public String getLinksForUserNodePath(String siteName, String language, UserIdentity user)
273    {
274        return getPluginNodePath(siteName) + "/" + __USER_LINKS_NODE_NAME + "/" + ISO9075.encode(user.getPopulationId()) + "/" + ISO9075.encode(user.getLogin()) + "/" + language + "/" + __LINKS_NODE_NAME;
275    }
276    
277    /**
278     * Get the themes node path
279     * @param siteName the site name.
280     * @param language the language
281     * @return the themes node path.
282     */
283    public String getThemesNodePath(String siteName, String language)
284    {
285        return getPluginNodePath(siteName) + "/"  + language + "/" + __THEMES_NODE_NAME;
286    }
287    
288    /**
289     * Get all the links
290     * @param siteName the site name.
291     * @param language the language.
292     * @return all the links' nodes
293     */
294    public String getAllLinksQuery(String siteName, String language)
295    {
296        return getLinksNodePath(siteName, language) + "/element(*, " + DefaultLinkFactory.LINK_NODE_TYPE + ")";
297    }
298    
299    /**
300     * Get the link query corresponding to the expression passed as a parameter
301     * @param siteName the site name.
302     * @param language the language.
303     * @param expression the {@link Expression} of the links retrieval query
304     * @return the link corresponding to the expression passed as a parameter
305     */
306    public String getLinksQuery(String siteName, String language, Expression expression)
307    {
308        return getLinksNodePath(siteName, language) + "/element(*, " + DefaultLinkFactory.LINK_NODE_TYPE + ")[" + expression.build() + "]";
309    }
310    
311    /**
312     * Get the theme query corresponding to the expression passed as a parameter
313     * @param siteName the site name.
314     * @param language the language.
315     * @param expression the {@link Expression} of the theme retrieval query
316     * @return the theme corresponding to the expression passed as a parameter
317     */
318    public String getThemeQuery(String siteName, String language, Expression expression)
319    {
320        return getThemesNodePath(siteName, language) + "/element(*, " + DefaultThemeFactory.THEME_NODE_TYPE + ")[" + expression.build() + "]";
321    }
322    
323    /**
324     * Get the query verifying the existence of an url
325     * @param siteName the site name.
326     * @param language the language.
327     * @param url the url to test. 
328     * @return the query verifying the existence of an url
329     */
330    public String getUrlExistsQuery(String siteName, String language, String url)
331    {
332        String lowerCaseUrl = StringUtils.replace(url, "'", "''").toLowerCase();
333        return getLinksNodePath(siteName, language) + "/element(*, " + DefaultLinkFactory.LINK_NODE_TYPE + ")[fn:lower-case(@ametys-internal:url) = '" + lowerCaseUrl + "' or fn:lower-case(@ametys-internal:internal-url) = '" + lowerCaseUrl + "']";
334    }
335    
336    /**
337     * Get the query verifying the existence of an url for the given user
338     * @param siteName the site name.
339     * @param language the language.
340     * @param url the url to test. 
341     * @param user The user identity
342     * @return the query verifying the existence of an url for the given user
343     */
344    public String getUrlExistsForUserQuery(String siteName, String language, String url, UserIdentity user)
345    {
346        String lowerCaseUrl = StringUtils.replace(url, "'", "''").toLowerCase();
347        return getLinksForUserNodePath(siteName, language, user) + "/element(*, " + DefaultLinkFactory.LINK_NODE_TYPE + ")[fn:lower-case(@ametys-internal:url) = '" + lowerCaseUrl + "' or fn:lower-case(@ametys-internal:internal-url) = '" + lowerCaseUrl + "']";
348    }
349    
350    /**
351     * Get the query verifying the existence of a theme
352     * @param siteName the site name.
353     * @param language the language.
354     * @param label the label of the theme to test. 
355     * @return the query verifying the existence of a theme
356     */
357    public String getThemeExistsQuery(String siteName, String language, String label)
358    {
359        String lowerCaseLabel = StringUtils.replace(label, "'", "''").toLowerCase();
360        return getThemesNodePath(siteName, language) + "/element(*, " + DefaultThemeFactory.THEME_NODE_TYPE + ")[fn:lower-case(@ametys-internal:label) = '" + lowerCaseLabel + "']";
361    }
362    
363    /**
364     * Normalizes an input string in order to capitalize it, remove accents, and replace whitespaces with underscores
365     * @param s the string to normalize
366     * @return the normalized string
367     */
368    public String normalizeString(String s)
369    {
370        // Strip accents
371        String normalizedLabel = Normalizer.normalize(s.toUpperCase(), Normalizer.Form.NFD).replaceAll("[^\\p{ASCII}]", "");
372        
373        // Upper case
374        String upperCaseLabel = normalizedLabel.replaceAll(" +", "_").replaceAll("[^\\w-]", "_").replaceAll("_+", "_").toUpperCase();
375        
376        return upperCaseLabel;
377    }
378    
379    /**
380     * Get links of a given site and language
381     * @param siteName the site name
382     * @param language the language
383     * @return the links
384     */
385    public AmetysObjectIterable<DefaultLink> getLinks(String siteName, String language)
386    {
387        Site site = _siteManager.getSite(siteName);
388        TraversableAmetysObject linksNode = getLinksNode(site, language);
389        return linksNode.getChildren();
390    }
391    
392    /**
393     * Get the list of links corresponding to the given theme ids
394     * @param themesIds the ids of the configured themes
395     * @param user the current user
396     * @param siteName the site's name
397     * @param language the site's language
398     * @return the list of default links corresponding to the given themes
399     */
400    public List<DefaultLink> getLinks(List<String> themesIds, UserIdentity user, String siteName, String language)
401    {
402        Site site = _siteManager.getSite(siteName);
403        TraversableAmetysObject linksNode = getLinksNode(site, language);
404        AmetysObjectIterable<AmetysObject> links = linksNode.getChildren();
405        Iterator<AmetysObject> it = links.iterator();
406        
407        if (themesIds.isEmpty())
408        {
409            return IteratorUtils.toList(it);
410        }
411        else
412        {
413            List<DefaultLink> configuredThemeLinks = new ArrayList<> ();
414
415            while (it.hasNext())
416            {
417                DefaultLink link = (DefaultLink) it.next();
418                String[] linkThemes = link.getThemes();
419                
420                for (String themeId : themesIds)
421                {
422                    if (ArrayUtils.contains(linkThemes, themeId))
423                    {
424                        configuredThemeLinks.add(link);
425                        break;
426                    }
427                }
428            }
429            
430            return configuredThemeLinks;
431        }
432    }
433    
434    /**
435     * Get links of a given site and language, for the given user
436     * @param siteName the site name
437     * @param language the language
438     * @param user The user identity
439     * @return the links for the given user
440     */
441    public AmetysObjectIterable<DefaultLink> getUserLinks(String siteName, String language, UserIdentity user)
442    {
443        Site site = _siteManager.getSite(siteName);
444        TraversableAmetysObject linksNode = getLinksForUserNode(site, language, user);
445        return linksNode.getChildren();
446    }
447    
448    /**
449     * Checks if the links displayed in a link directory service has access restrictions
450     * @param siteName the name of the site
451     * @param language the language
452     * @param themesIds the list of selected theme ids
453     * @return true if the links of the service have access restrictions, false otherwise
454     */
455    public boolean hasRestrictions(String siteName, String language, List<String> themesIds)
456    {
457        // No themes => we check all the links' access restrictions
458        if (themesIds.isEmpty())   
459        {
460            String allLinksQuery = getAllLinksQuery(siteName, language);
461            try (AmetysObjectIterable<AmetysObject> links = _ametysObjectResolver.query(allLinksQuery))
462            {
463                if (isAccessRestricted(links))
464                {
465                    return true;
466                }
467            }
468            
469            
470        }
471        // The service has themes specified => we solely check the corresponding links' access restrictions
472        else
473        {
474            for (String themeId : themesIds)
475            {
476                String xPathQuery = getLinksQuery(siteName, language, new ThemeExpression(themeId));
477                try (AmetysObjectIterable<AmetysObject> links = _ametysObjectResolver.query(xPathQuery))
478                {
479                    if (isAccessRestricted(links))
480                    {
481                        return true;
482                    }
483                }
484            }
485        }
486        
487        // All the tested links have no restricted access
488        return false;
489    }
490    
491    /**
492     * Checks if the links displayed in a link directory service has internal link
493     * @param siteName the name of the site
494     * @param language the language
495     * @param themesIds the list of selected theme ids
496     * @return true if the links of the service has internal link, false otherwise
497     */
498    public boolean hasInternalUrl(String siteName, String language, List<String> themesIds)
499    {
500        String allowedIdParameter = _siteConfiguration.getValueAsString(siteName, "allowed-ip");
501        if (StringUtils.isBlank(allowedIdParameter))
502        {
503            return false;
504        }
505        
506        UserIdentity user = _currentUserProvider.getUser();
507        
508        List<DefaultLink> links = getLinks(themesIds, user, siteName, language);
509        for (DefaultLink link : links)
510        {
511            if (StringUtils.isNotBlank(link.getInternalUrl()))
512            {
513                return true;
514            }
515        }
516        
517        return false;
518    }
519    
520    /**
521     * Check if the links' access is restricted or not
522     * @param links the links to be tested
523     * @return true if the link has a restricted access, false otherwise
524     */
525    public boolean isAccessRestricted(AmetysObjectIterable<AmetysObject> links)
526    {
527        Iterator<AmetysObject> it = links.iterator();
528        
529        while (it.hasNext())
530        {
531            DefaultLink link = (DefaultLink) it.next();
532            
533            // If any of the links has a limited access, the service declares itself non-cacheable
534            if (link.isAllowedAnyUser() || ArrayUtils.isNotEmpty(link.getGrantedGroups()) || ArrayUtils.isNotEmpty(link.getGrantedUsers()))
535            {
536                return true;
537            }
538        }
539        
540        return false;
541    }
542    
543    private ModifiableTraversableAmetysObject getOrCreateNode(ModifiableTraversableAmetysObject parentNode, String nodeName, String nodeType) throws AmetysRepositoryException
544    {
545        ModifiableTraversableAmetysObject node;
546        if (parentNode.hasChild(nodeName))
547        {
548            node = parentNode.getChild(nodeName);
549        }
550        else
551        {
552            node = parentNode.createChild(nodeName, nodeType);
553            parentNode.saveChanges();
554        }
555        return node;
556    }
557    
558    /**
559     * Sax the directory links
560     * @param siteName the site name
561     * @param contentHandler the content handler
562     * @param links the list of links to sax 
563     * @param storageContext the storage context, null if there is no connected user
564     * @param contextVars the context variables
565     * @param user the user
566     * @param saxThemesLabelOnly true to sax themes label only
567     * @param saxLinksOnly true if the only the links need to be saxed, false otherwise
568     * @param userLinks true if it is user links
569     * @throws SAXException If an error occurs while generating the SAX events
570     * @throws UserPreferencesException if an exception occurs while getting the user preferences
571     */
572    public void saxLinks(String siteName, ContentHandler contentHandler, List<DefaultLink> links, Map<String, String> contextVars, String storageContext, UserIdentity user, boolean saxThemesLabelOnly, boolean saxLinksOnly, boolean userLinks) throws SAXException, UserPreferencesException
573    {
574        String prefValues = null;
575        String[] prefLinksIdsArray = null; 
576        if (user != null)
577        {
578            prefValues =  _userPreferencesManager.getUserPreferenceAsString(user, storageContext, contextVars, "checked-links");
579            prefLinksIdsArray = StringUtils.split(prefValues, ",");
580        }
581        
582        String ipRegexp = _siteConfiguration.getValueAsString(siteName, "allowed-ip");
583        Pattern ipRestriction = null;
584        if (StringUtils.isNotBlank(ipRegexp))
585        {
586            ipRestriction = Pattern.compile(ipRegexp);
587        }
588        
589        boolean hasIPRestriction = ipRestriction != null;
590        boolean isIPAuthorized = _isIPAuthorized(ipRestriction);
591            
592        for (DefaultLink link : links)
593        {
594            if (_isCurrentUserGrantedAccess(link))
595            {
596                boolean selected = false;
597                if (ArrayUtils.isNotEmpty(prefLinksIdsArray))
598                {
599                    if (ArrayUtils.contains(prefLinksIdsArray, link.getId()))
600                    {
601                        selected = true;
602                    }
603                }
604                
605                saxLink(siteName, contentHandler, link, saxThemesLabelOnly, selected, saxLinksOnly, hasIPRestriction, isIPAuthorized, userLinks);
606            }
607        }
608    }
609    
610    /**
611     * Generate a directory link.
612     * @param siteName the site name
613     * @param contentHandler the content handler
614     * @param link the link to generate.
615     * @param saxThemesLabelOnly true to sax themes label only
616     * @param selected true if a front end user has checked this link as a user preference, false otherwise
617     * @param saxLinksOnly true if we want to sax a link for an input data
618     * @param hasIPRestriction true if we have IP restriction
619     * @param isIPAuthorized true if the IP is authorized
620     * @param userLink true if it is a user link
621     * @throws SAXException If an error occurs while generating the SAX events
622     */
623    public void saxLink (String siteName, ContentHandler contentHandler, DefaultLink link, boolean saxThemesLabelOnly, boolean selected, boolean saxLinksOnly, boolean hasIPRestriction, boolean isIPAuthorized, boolean userLink) throws SAXException
624    {
625        AttributesImpl attrs = new AttributesImpl();
626        attrs.addCDATAAttribute("id", link.getId());
627        attrs.addCDATAAttribute("lang", link.getLanguage());
628        
629        LinkType urlType = link.getUrlType();
630        
631        _addURLAttribute(link, hasIPRestriction, isIPAuthorized, attrs);
632        
633        attrs.addCDATAAttribute("urlType", StringUtils.defaultString(urlType.toString()));
634        
635        String dynInfoProviderId = StringUtils.defaultString(link.getDynamicInformationProvider());
636        // Check if provider exists
637        if (StringUtils.isNotEmpty(dynInfoProviderId) && _dynamicProviderEP.hasExtension(dynInfoProviderId))
638        {
639            attrs.addCDATAAttribute("dynamicInformationProvider", dynInfoProviderId);
640        }
641        attrs.addCDATAAttribute("title", StringUtils.defaultString(link.getTitle()));
642        attrs.addCDATAAttribute("content", StringUtils.defaultString(link.getContent()));
643        
644        if (urlType == LinkType.PAGE)
645        {
646            String pageId = link.getUrl();
647            try
648            {
649                Page page = _ametysObjectResolver.resolveById(pageId);
650                attrs.addCDATAAttribute("pageTitle", page.getTitle());
651            }
652            catch (UnknownAmetysObjectException e)
653            {
654                attrs.addCDATAAttribute("unknownPage", "true");
655            }
656        } 
657        
658        attrs.addCDATAAttribute("alternative", StringUtils.defaultString(link.getAlternative()));
659        attrs.addCDATAAttribute("pictureAlternative", StringUtils.defaultString(link.getPictureAlternative()));
660        
661        attrs.addCDATAAttribute("user-selected", selected ? "true" : "false");
662        
663        String pictureType = link.getPictureType();
664        attrs.addCDATAAttribute("pictureType", pictureType);
665        if (pictureType.equals("resource"))
666        {
667            String resourceId = link.getResourcePictureId();
668            try
669            {
670                Resource resource = _ametysObjectResolver.resolveById(resourceId);
671                attrs.addCDATAAttribute("pictureId", resourceId);
672                attrs.addCDATAAttribute("pictureName", resource.getName());
673                attrs.addCDATAAttribute("pictureSize", Long.toString(resource.getLength()));
674                attrs.addCDATAAttribute("imageType", "explorer");
675            }
676            catch (UnknownAmetysObjectException e)
677            {
678                getLogger().error("The resource of id'" + resourceId + "' does not exist anymore. The picture for link of id '" + link.getId() + "' will be ignored.", e);
679            }
680            
681        }
682        else if (pictureType.equals("external"))
683        {
684            BinaryMetadata picMeta = link.getExternalPicture();
685            attrs.addCDATAAttribute("picturePath", DefaultLink.PROPERTY_PICTURE);
686            attrs.addCDATAAttribute("pictureName", picMeta.getFilename());
687            attrs.addCDATAAttribute("pictureSize", Long.toString(picMeta.getLength()));
688            attrs.addCDATAAttribute("imageType", "link-metadata");
689        }
690        else if (pictureType.equals("glyph"))
691        {
692            attrs.addCDATAAttribute("pictureGlyph", link.getPictureGlyph());
693        }
694        
695        attrs.addCDATAAttribute("grantAnyUser", String.valueOf(link.isAllowedAnyUser()));
696        
697        boolean isAccessLimited = link.isAllowedAnyUser() || ArrayUtils.isNotEmpty(link.getGrantedUsers()) || ArrayUtils.isNotEmpty(link.getGrantedGroups());
698        attrs.addCDATAAttribute("limitedAccess", String.valueOf(isAccessLimited)); 
699        
700        attrs.addCDATAAttribute("userLink", String.valueOf(userLink));
701        
702        XMLUtils.startElement(contentHandler, "link", attrs);
703
704        // Sax the access limitations if necessary
705        if (!saxLinksOnly)
706        {
707            _saxGrantedUsers(contentHandler, link);
708            _saxGrantedGroups(contentHandler, link);
709        }
710        
711        // Themes
712        if (saxThemesLabelOnly)
713        {
714            _saxThemesLabel(contentHandler, link);
715        }
716        else
717        {
718            _saxThemes(contentHandler, link);
719        }
720        
721        XMLUtils.endElement(contentHandler, "link");
722    }
723    
724    /**
725     * Add the URL attribute to sax
726     * @param link the link
727     * @param hasIPRestriction true if we have IP restriction
728     * @param isIPAuthorized true if the IP is authorized
729     * @param attrs the attribute
730     */
731    private void _addURLAttribute(DefaultLink link, boolean hasIPRestriction, boolean isIPAuthorized, AttributesImpl attrs)
732    {
733        String internalUrl = link.getInternalUrl();
734        String externalUrl = link.getUrl();
735        
736        // If we have no internal URL or no IP restriction, just sax external URL
737        if (StringUtils.isBlank(internalUrl) || !hasIPRestriction)
738        {
739            attrs.addCDATAAttribute("url", StringUtils.defaultString(externalUrl));
740        }
741        else
742        {
743            // If the IP is authorized, sax internal URL
744            if (isIPAuthorized)
745            {
746                attrs.addCDATAAttribute("url", StringUtils.defaultString(internalUrl));
747            }
748            // else if we have external URL, we sax it
749            else if (StringUtils.isNotBlank(externalUrl))
750            {
751                attrs.addCDATAAttribute("url", StringUtils.defaultString(externalUrl));
752            }
753            // else we sax the internal URL and we disable it because the IP is not authorized
754            else
755            {
756                attrs.addCDATAAttribute("url", StringUtils.defaultString(internalUrl));
757                attrs.addCDATAAttribute("disabled", "true");
758            }
759        }
760    }
761    
762    /**
763     * Get the actual ids of the themes configured properly, their names if they were not 
764     * @param configuredThemesNames the normalized ids of the configured themes
765     * @param siteName the site's name
766     * @param language the site's language
767     * @return the actual ids of the configured themes
768     */
769    public Map<String, List<String>> getThemesMap(List<String> configuredThemesNames, String siteName, String language)
770    {
771        Map<String, List<String>> themesMap = new HashMap<> ();
772        List<String> correctThemesList = new ArrayList<> ();
773        List<String> wrongThemesList = new ArrayList<> ();
774        
775        for (int i = 0; i < configuredThemesNames.size(); i++)
776        {
777            ModifiableTraversableAmetysObject themesNode = this.getThemesNode(_siteManager.getSite(siteName), language);
778            String configuredThemeName = configuredThemesNames.get(i);
779            
780            if (!themesNode.hasChild(configuredThemeName))
781            {
782                getLogger().warn("The theme '" + configuredThemeName + "' was not found. It will be ignored.");
783                wrongThemesList.add(configuredThemeName);
784            }
785            else
786            {
787                AmetysObject configuredThemeNode = themesNode.getChild(configuredThemesNames.get(i));
788                correctThemesList.add(configuredThemeNode.getId());
789            }
790        }
791        
792        themesMap.put("themes", correctThemesList);
793        themesMap.put("unknown-themes", wrongThemesList);
794        return themesMap;
795    }
796    
797    /**
798     * Retrieve theme ids from theme names
799     * @param site the site
800     * @param language the language
801     * @param configuredThemesNames the names of the configured themes
802     * @return the themes ids matching the given theme names
803     */
804    public List<String> getThemesIdsFromThemesNames(Site site, String language, List<String> configuredThemesNames)
805    {
806        List<String> configuredThemesIds = new ArrayList<>();
807        
808        if (configuredThemesNames != null)
809        {
810            for (int i = 0; i < configuredThemesNames.size(); i++)
811            {
812                ModifiableTraversableAmetysObject themesNode = this.getThemesNode(site, language);
813                String configuredThemeName = configuredThemesNames.get(i);
814
815                if (themesNode.hasChild(configuredThemeName))
816                {
817                    AmetysObject configuredThemeNode = themesNode.getChild(configuredThemeName);
818                    configuredThemesIds.add(configuredThemeNode.getId());
819                }
820            }
821        }
822        
823        return configuredThemesIds;
824    }
825    
826    /**
827     * Verify the existence of a theme
828     * @param themeId the id of the theme to verify
829     * @return true if the theme exists, false otherwise
830     */
831    public boolean themeExists(String themeId)
832    {
833        return _ametysObjectResolver.hasAmetysObjectForId(themeId);
834    }
835
836    /**
837     * Get the site's name
838     * @param request the request
839     * @return the site's name
840     */
841    public String getSiteName(Request request)
842    {
843        Page page = (Page) request.getAttribute(Page.class.getName());
844        String siteName = null;
845        if (page != null)
846        {
847            siteName = page.getSiteName();
848        }
849        else
850        {
851            siteName = (String) request.getAttribute("site");
852        }
853        return siteName;
854    }
855
856    /**
857     * Get the site's language
858     * @param request the request
859     * @return the site's language
860     */
861    public String getLanguage(Request request)
862    {
863        String language = (String) request.getAttribute("sitemapLanguage");
864        if (StringUtils.isEmpty(language))
865        {
866            language = request.getParameter("language");
867        }
868        
869        return language;
870    }
871    
872    /**
873     * Retrieve the context variables from the front
874     * @param request the request
875     * @return the map of context variables
876     */
877    public Map<String, String> getContextVars(Request request)
878    {
879        Map<String, String> contextVars = new HashMap<> ();
880        
881        contextVars.put(FOUserPreferencesConstants.CONTEXT_VAR_SITENAME, getSiteName(request));
882        contextVars.put(FOUserPreferencesConstants.CONTEXT_VAR_LANGUAGE, getLanguage(request));
883    
884        return contextVars;
885    }
886    
887    /**
888     * Sax the themes
889     * @param contentHandler the content handler 
890     * @param link the link 
891     * @throws SAXException If an error occurs while generating the SAX events
892     */
893    private void _saxThemes (ContentHandler contentHandler, DefaultLink link) throws SAXException
894    {
895        XMLUtils.startElement(contentHandler, "themes");
896        
897        for (String themeId : link.getThemes())
898        {
899            try
900            {
901                DefaultTheme theme = _ametysObjectResolver.resolveById(themeId);
902                
903                AttributesImpl attrs = new AttributesImpl();
904                attrs.addCDATAAttribute("id", themeId);
905                attrs.addCDATAAttribute("label", theme.getLabel());
906                
907                XMLUtils.createElement(contentHandler, "theme", attrs);
908            }
909            catch (UnknownAmetysObjectException e)
910            {
911                // Theme does not exist anymore
912            }
913        }
914            
915        
916        XMLUtils.endElement(contentHandler, "themes");
917    }
918    
919    /**
920     * Sax the label of the themes
921     * @param contentHandler the content handler
922     * @param link the link
923     * @throws SAXException if an error occurs while saxing
924     */
925    private void _saxThemesLabel (ContentHandler contentHandler, DefaultLink link) throws SAXException
926    {
927        List<String> themesLabel = new ArrayList<>();
928        
929        for (String themeId : link.getThemes())
930        {
931            try
932            {
933                DefaultTheme theme = _ametysObjectResolver.resolveById(themeId);
934                themesLabel.add(theme.getLabel());
935            }
936            catch (UnknownAmetysObjectException e)
937            {
938                // Theme does not exist anymore
939            }
940        }
941        
942        XMLUtils.createElement(contentHandler, "themes", StringUtils.join(themesLabel, ", "));
943    }
944    
945    /**
946     * Sax the granted users
947     * @param contentHandler the content handler
948     * @param link the link
949     * @throws SAXException if an exception occurs while saxing
950     */
951    private void _saxGrantedUsers(ContentHandler contentHandler, DefaultLink link) throws SAXException
952    {
953        XMLUtils.startElement(contentHandler, "granted-users");
954        
955        for (UserIdentity grantedUser : link.getGrantedUsers())
956        {
957            User user = _userManager.getUser(grantedUser.getPopulationId(), grantedUser.getLogin());
958            if (user != null)
959            {
960                AttributesImpl attrs = new AttributesImpl();
961                attrs.addCDATAAttribute("login", grantedUser.getLogin());
962                attrs.addCDATAAttribute("populationId", grantedUser.getPopulationId());
963                attrs.addCDATAAttribute("fullName", user.getFullName());
964                XMLUtils.createElement(contentHandler, "granted-user", attrs);
965            }
966        }
967        XMLUtils.endElement(contentHandler, "granted-users");
968    }
969    
970    /**
971     * Sax the granted groups
972     * @param contentHandler the content handler
973     * @param link the link
974     * @throws SAXException if an exception occurs while saxing
975     */
976    private void _saxGrantedGroups(ContentHandler contentHandler, DefaultLink link) throws SAXException
977    {
978        XMLUtils.startElement(contentHandler, "granted-groups");
979        
980        for (GroupIdentity grantedGroup : link.getGrantedGroups())
981        {
982            Group group = _groupManager.getGroup(grantedGroup.getDirectoryId(), grantedGroup.getId());
983            if (group != null)
984            {
985                AttributesImpl attrs = new AttributesImpl();
986                attrs.addCDATAAttribute("id", grantedGroup.getId());
987                attrs.addCDATAAttribute("groupDirectory", grantedGroup.getDirectoryId());
988                attrs.addCDATAAttribute("label", group.getLabel());
989                XMLUtils.createElement(contentHandler, "granted-group", attrs);
990            }
991        }
992        XMLUtils.endElement(contentHandler, "granted-groups");
993    }
994    
995    /**
996     * Determines if the current user is allowed to see the link or not
997     * @param link the link 
998     * @return true if the current user is allowed to see the link, false otherwise
999     */
1000    private boolean _isCurrentUserGrantedAccess(DefaultLink link)
1001    {
1002        UserIdentity user = _currentUserProvider.getUser();
1003        
1004        boolean isAllowedAnyUser = link.isAllowedAnyUser();
1005        UserIdentity[] grantedUsers = link.getGrantedUsers();
1006        GroupIdentity[] grantedGroups = link.getGrantedGroups();
1007        
1008        // There is an access restriction on users or groups
1009        if (ArrayUtils.isNotEmpty(grantedUsers) || ArrayUtils.isNotEmpty(grantedGroups))
1010        {
1011            return _isGrantedAccess(user, grantedUsers, grantedGroups);
1012        }
1013        
1014        // There is an access restriction on connected users
1015        if (isAllowedAnyUser)
1016        {
1017            return user != null ? true : false;
1018        }
1019        
1020        // There is no access restriction
1021        return true;
1022    }
1023    
1024    /**
1025     * Checks if the user has access to the page or not
1026     * @param user the identity of the user
1027     * @param grantedUsers the list of granted users
1028     * @param grantedGroups the list of granted groups
1029     * @return true if the user is granted access, false otherwise
1030     */
1031    private boolean _isGrantedAccess(UserIdentity user, UserIdentity[] grantedUsers, GroupIdentity[] grantedGroups)
1032    {
1033        if (user == null)
1034        {
1035            return false;
1036        }
1037        
1038        Set<GroupIdentity> groups  = _groupManager.getUserGroups(user);
1039
1040        // Check the authorizations
1041        if (ArrayUtils.contains(grantedUsers, user))
1042        {
1043            return true;
1044        }
1045        
1046        for (GroupIdentity group : grantedGroups)
1047        {
1048            if (groups.contains(group))
1049            {
1050                return true;
1051            }
1052        }
1053        
1054        // The user doesn't belong to the granted users
1055        return false;
1056    }
1057    
1058    /**
1059     * Checks if the IP is authorized for use link internal URL
1060     * @param ipRestriction The ip restriction pattern
1061     * @return true the IP is authorized for use link internal URL, false otherwise
1062     */
1063    private boolean _isIPAuthorized(Pattern ipRestriction)
1064    {
1065        if (ipRestriction == null)
1066        {
1067            return true;
1068        }
1069        
1070        Request request = ContextHelper.getRequest(_context);
1071        
1072        // The real client IP may have been put in the non-standard "X-Forwarded-For" request header, in case of reverse proxy
1073        String xff = request.getHeader("X-Forwarded-For");
1074        String ip = null;
1075        
1076        if (xff != null)
1077        {
1078            ip = xff.split(",")[0];
1079        }
1080        else
1081        {
1082            ip = request.getRemoteAddr();
1083        }
1084        
1085        boolean result = ipRestriction.matcher(ip).matches();
1086        
1087        if (getLogger().isDebugEnabled())
1088        {
1089            getLogger().debug("Ip '{}' is considered {} with pattern {}", ip, result ? "internal" : "external", ipRestriction.pattern());
1090        }
1091        
1092        return result;
1093    }
1094}