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