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