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