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