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