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