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