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