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.serverdirectory;
017
018import java.util.Arrays;
019import java.util.Comparator;
020import java.util.HashSet;
021import java.util.Set;
022import java.util.regex.Matcher;
023import java.util.regex.Pattern;
024
025import org.apache.cocoon.ProcessingException;
026import org.apache.commons.io.FilenameUtils;
027import org.apache.commons.lang.ArrayUtils;
028import org.apache.commons.lang.StringUtils;
029import org.apache.excalibur.source.Source;
030import org.apache.excalibur.source.SourceResolver;
031
032import org.ametys.core.user.UserIdentity;
033import org.ametys.runtime.authentication.AccessDeniedException;
034import org.ametys.runtime.authentication.AuthorizationRequiredException;
035import org.ametys.runtime.config.Config;
036
037/**
038 * Helper gathering utility methods for server directories
039 */
040public final class ServerDirectoryHelper
041{
042    // Pattern for the dynamic path variables : matches "${g1}" and "${g1[g2=g3]}", with g1, g2 and g3 the capturing groups
043    private static final Pattern DYNAMIC_PATH_PATTERN = Pattern.compile("\\$\\{([^}\\[]+)(?:\\[([^=\\[\\]\\}\\{]+)=([^=\\[\\]\\}\\{]+)\\])?\\}");
044    
045    private ServerDirectoryHelper()
046    {
047        // empty constructor
048    }
049    
050    /**
051     * Get the sources corresponding to the roots of the defined server directories
052     * @param sourceResolver the source resolver
053     * @return the sources of the server directories' roots
054     * @throws ProcessingException if the sever directory's location wasn't found
055     */
056    public static Set<Source> getRootServerSources(SourceResolver sourceResolver) throws ProcessingException
057    {
058        Set<String> locations = ServerDirectoryHelper.getRootServerDirectoryPaths();
059        
060        Set<Source> sources = new HashSet<>();
061        
062        for (String location : locations)
063        {
064            Source src = null;
065            try
066            {
067                src = sourceResolver.resolveURI(location.trim(), "file://", null);
068                if (src.exists())
069                {
070                    sources.add(src);
071                }
072            }
073            catch (Exception e)
074            {
075                throw new ProcessingException("Unable to retrieve server directory to location: <" + location + ">", e);
076            }
077            finally
078            {
079                sourceResolver.release(src);
080            }
081        }
082        
083        return sources;
084    }
085    
086    /**
087     * Get the paths of the server directories' roots
088     * @return the paths of the server directories' roots
089     */
090    public static Set<String> getRootServerDirectoryPaths ()
091    {
092        String authorizedDirectories = Config.getInstance().getValue("org.ametys.plugins.server.directory.authorized");
093        String[] locations = StringUtils.split(authorizedDirectories, "\n");     
094        
095        Comparator<String> c = new Comparator<>() 
096        {
097            @Override
098            public int compare(String s1, String s2) 
099            {
100                
101                int l1 = s1.length();
102                int l2 = s2.length();
103                
104                return ((Integer) l1).compareTo(l2);
105            }
106        };
107        Arrays.sort(locations, c);
108        
109        Set<String> distinctLocations = new HashSet<>();
110        for (String location : locations)
111        {
112            boolean found = false;
113            for (String distinctLocation : distinctLocations)
114            {
115                if (location.startsWith(distinctLocation))
116                {
117                    found = true;
118                    break;
119                }
120            }
121            
122            if (!found)
123            {
124                distinctLocations.add(location.trim());
125            }
126        }
127        
128        return distinctLocations;
129    }
130    
131    /**
132     * Normalizes a path, removing double and single dot path steps and replace '\' by '/'
133     * @param path The path
134     * @return The normalized path
135     */
136    public static String normalize (String path)
137    {
138        String normalizedPath = FilenameUtils.normalize(path);
139        return normalizedPath.replace("\\", "/");
140    }
141    
142    /**
143     * Check if the given path is a valid path for a root of a server directory
144     * @param path the path to check
145     * @param rootSources the sources corresponding to the server directories' roots
146     * @return true if the path is valid, false otherwise
147     */
148    public static boolean isValidPath(String path, Set<Source> rootSources)
149    {
150        String normalizedPath = normalize(path);
151        for (Source rootSource : rootSources)
152        {
153            if (normalizedPath.startsWith(rootSource.getURI()))
154            {
155                return true;
156            }
157        }
158        return false;
159    }
160    
161    /**
162     * Evaluate a dynamic path with contextual variables
163     * @param path The dynamic path
164     * @param siteName The current site name
165     * @param language The current site language
166     * @param currentUser The current user
167     * @return The path resolved
168     * @throws AuthorizationRequiredException If the current user is null and the path required the login
169     * @throws AccessDeniedException If the connected user does not belong to required population
170     * @throws IllegalArgumentException If a dynamic variable can not be evaluated
171     */
172    public static String evaluateDynamicPath(String path, String siteName, String language, UserIdentity currentUser) throws AuthorizationRequiredException, AccessDeniedException, IllegalArgumentException
173    {
174        Matcher matcher = DYNAMIC_PATH_PATTERN.matcher(path);
175        
176        StringBuffer newPath = new StringBuffer();
177        while (matcher.find())
178        {
179            String variable = matcher.group(1);
180            String param = matcher.group(2);
181            String paramValue = matcher.group(3);
182            
183            if ("login".equals(variable))
184            {
185                if (currentUser == null)
186                {
187                    throw new AuthorizationRequiredException(null);
188                }
189                if (param != null && paramValue != null)
190                {
191                    if ("population".equals(param))
192                    {
193                        String[] populations = paramValue.split(",");
194                        if (!ArrayUtils.contains(populations, currentUser.getPopulationId()))
195                        {
196                            throw new AccessDeniedException("The user " + currentUser + " is not authorized to access file " + path);
197                        }
198                    }
199                    else
200                    {
201                        // unknown parameter
202                        throw new IllegalArgumentException("Unable to evaluate the current site");
203                    }
204                }
205                
206                matcher.appendReplacement(newPath, currentUser.getLogin());
207            }
208            else if ("population".equals(variable))
209            {
210                if (currentUser == null)
211                {
212                    throw new AuthorizationRequiredException(null);
213                }
214                
215                matcher.appendReplacement(newPath, currentUser.getPopulationId());
216            }
217            else if ("site".equals(variable))
218            {
219                if (siteName == null)
220                {
221                    throw new IllegalArgumentException("Unable to evaluate the current site for service directory path " + path);
222                }
223                
224                matcher.appendReplacement(newPath, siteName);
225            }
226            else if ("lang".equals(variable))
227            {
228                if (language == null)
229                {
230                    throw new IllegalArgumentException("Unable to evaluate the current language for service directory path " + path);
231                }
232                
233                matcher.appendReplacement(newPath, language);
234            }
235            else
236            {
237                matcher.appendReplacement(newPath, "$0");
238            }
239        }
240        
241        matcher.appendTail(newPath);
242        
243        return newPath.toString();
244    }
245}