001/*
002 *  Copyright 2010 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.explorer.resources.readers;
017
018import java.io.IOException;
019import java.io.InputStream;
020import java.io.Serializable;
021import java.io.UnsupportedEncodingException;
022import java.net.URI;
023import java.net.URISyntaxException;
024import java.net.URLDecoder;
025import java.util.Arrays;
026import java.util.Collection;
027import java.util.Map;
028
029import org.apache.avalon.framework.parameters.Parameters;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.cocoon.ProcessingException;
033import org.apache.cocoon.ResourceNotFoundException;
034import org.apache.cocoon.caching.CacheableProcessingComponent;
035import org.apache.cocoon.environment.ObjectModelHelper;
036import org.apache.cocoon.environment.Response;
037import org.apache.cocoon.environment.SourceResolver;
038import org.apache.cocoon.reading.ServiceableReader;
039import org.apache.commons.io.IOUtils;
040import org.apache.commons.lang.StringUtils;
041import org.apache.excalibur.source.SourceValidity;
042import org.apache.excalibur.source.impl.validity.TimeStampValidity;
043import org.xml.sax.SAXException;
044
045import org.ametys.core.right.RightManager;
046import org.ametys.core.user.CurrentUserProvider;
047import org.ametys.core.user.UserIdentity;
048import org.ametys.core.util.ImageHelper;
049import org.ametys.plugins.explorer.resources.Resource;
050import org.ametys.plugins.repository.AmetysObjectResolver;
051import org.ametys.plugins.repository.UnknownAmetysObjectException;
052import org.ametys.plugins.repository.version.VersionableAmetysObject;
053import org.ametys.runtime.authentication.AccessDeniedException;
054import org.ametys.runtime.authentication.AuthorizationRequiredException;
055
056/**
057 * Reader for {@link Resource}
058 */
059public class AmetysResourceReader extends ServiceableReader implements CacheableProcessingComponent
060{
061    /** The Ametys object resolver */
062    protected AmetysObjectResolver _resolver;
063    /** The resource */
064    protected Resource _object;
065    /** The right manager */
066    protected RightManager _rightManager;
067    /** The current user provider */
068    protected CurrentUserProvider _currentUserProvider;
069    
070    private boolean _readForDownload;
071    
072    private Collection<String> _allowedFormats = Arrays.asList(new String[]{"png", "gif", "jpg", "jpeg"});
073    
074    private int _width;
075    private int _height;
076    private int _maxWidth;
077    private int _maxHeight;
078    
079    @Override
080    public void service(ServiceManager sManager) throws ServiceException
081    {
082        super.service(sManager);
083        _resolver = (AmetysObjectResolver) sManager.lookup(AmetysObjectResolver.ROLE);
084        _rightManager = (RightManager) sManager.lookup(RightManager.ROLE);
085        _currentUserProvider = (CurrentUserProvider) sManager.lookup(CurrentUserProvider.ROLE);
086    }
087    
088    @Override
089    public void setup(SourceResolver sResolver, Map objModel, String src, Parameters par) throws ProcessingException, SAXException, IOException
090    {
091        super.setup(sResolver, objModel, src, par);
092        
093        String id = par.getParameter("id", null);
094        String path = par.getParameter("path", null);
095        String version = par.getParameter("version", null);
096        
097        // parameters for image resizing
098        _width = par.getParameterAsInteger("width", 0);
099        _height = par.getParameterAsInteger("height", 0);
100        _maxWidth = par.getParameterAsInteger("maxWidth", 0);
101        _maxHeight = par.getParameterAsInteger("maxHeight", 0);
102        
103        _readForDownload = par.getParameterAsBoolean("download", false);
104        Response response = ObjectModelHelper.getResponse(objectModel);
105        
106        try
107        {
108            if (id != null)
109            {
110                _object = _resolver.resolveById(id);
111            }
112            else
113            {
114                _object = _resolver.resolveByPath(_decodePath(path.substring(1)));
115            }
116
117            // Check user access
118            checkUserAccess();
119            
120            if (!StringUtils.isEmpty(version) && _object instanceof VersionableAmetysObject)
121            {
122                ((VersionableAmetysObject) _object).switchToRevision(version);
123            }
124
125            if (_readForDownload)
126            {
127                String name = _object.getName();
128                String encodedName = null;
129                try
130                {
131                    URI uri = new URI(null, null, name, null);
132                    encodedName = uri.toASCIIString();
133                    // EXPLORER-358 : Fix for Chrome which does not support comma in filename
134                    encodedName = encodedName.replaceAll(",", "%2C");
135                }
136                catch (URISyntaxException e)
137                {
138                    // do nothing and send no encoded name
139                }
140                
141                name = name.replaceAll("\\\\", "\\\\\\\\");
142                name = name.replaceAll("\\\"", "\\\\\\\"");
143                response.setHeader("Content-Disposition", "attachment; filename=\"" + (encodedName != null ? encodedName : name) + "\"" + (encodedName != null ? ";filename*=UTF-8''" + encodedName : ""));
144            }
145        }
146        catch (UnknownAmetysObjectException e)
147        {
148            if (id != null)
149            {
150                throw new ResourceNotFoundException(String.format("The resource with id '%s' does not exist", id));
151            }
152            else
153            {
154                throw new ResourceNotFoundException(String.format("The resource at path '%s' does not exist", path));
155            }
156            
157        }
158    }
159    
160    /**
161     * Check the user access
162     * @throws AuthorizationRequiredException if authorization is required
163     * @throws AccessDeniedException if user has no access
164     */
165    protected void checkUserAccess() throws AuthorizationRequiredException, AccessDeniedException
166    {
167        UserIdentity user = _currentUserProvider.getUser();
168        
169        if (user == null)
170        {
171            // Check anonymous access
172            if (!_rightManager.hasAnonymousReadAccess(_object.getParent()))
173            {
174                throw new AuthorizationRequiredException(null);
175            }
176        }
177        else if (!_rightManager.hasReadAccess(user, _object.getParent()))
178        {
179            throw new AccessDeniedException("User " + user + " has no right to acesss the resource " + _object.getId());
180        }
181    }
182    
183    /**
184     * Decode the resource path
185     * @param path the resource path
186     * @return the decoded resource path
187     * @throws UnsupportedEncodingException if UTF-8 encoding is not supported
188     */
189    protected String _decodePath (String path) throws UnsupportedEncodingException
190    {
191        StringBuffer sb = new StringBuffer();
192        
193        String[] parts = path.split("/");
194        for (String part : parts)
195        {
196            sb.append("/");
197            sb.append(URLDecoder.decode(part, "utf-8"));
198        }
199        return sb.toString();
200    }
201
202    @Override
203    public Serializable getKey()
204    {
205        return _object.getId() + "#" + _height + "#" + _width + "#" + _maxHeight + "#" + _maxWidth;
206    }
207
208    @Override
209    public SourceValidity getValidity()
210    {
211        return new TimeStampValidity(getLastModified());
212    }
213    
214    @Override
215    public long getLastModified()
216    {
217        return _object.getLastModified().getTime();
218    }
219    
220    @Override
221    public String getMimeType()
222    {
223        return _object.getMimeType();
224    }
225    
226    @Override
227    public void generate() throws IOException, SAXException, ProcessingException
228    {
229        String name = _object.getName();
230        name = name.replaceAll("\\\\", "\\\\\\\\");
231        name = name.replaceAll("\\\"", "\\\\\\\"");
232        
233        try (InputStream is = _object.getInputStream())
234        {
235            int i = name.lastIndexOf('.');
236            String format = i != -1 ? name.substring(i + 1) : "png";
237            format = _allowedFormats.contains(format) ? format : "png";
238
239            if (_isImage())
240            {
241                ImageHelper.generateThumbnail(is, out, format, _height, _width, _maxHeight, _maxWidth);
242            }
243            else
244            {
245                IOUtils.copy(is, out);
246            }
247        }
248        catch (Exception e)
249        {
250            throw new ProcessingException("Unable to download file of id " + _object.getId(), e);
251        }
252        finally
253        {
254            IOUtils.closeQuietly(out);
255        }
256    }
257    
258    /**
259     * Determines if the file is an image
260     * @return <code>true</code> if file is a image
261     */
262    protected boolean _isImage()
263    {
264        if (_width > 0 || _height > 0 || _maxHeight > 0 || _maxWidth > 0)
265        {
266            // resize is required, assume this is a image
267            return true;
268        }
269        else
270        {
271            return getMimeType() != null && getMimeType().startsWith("image/");
272        }
273    }
274    
275    @Override
276    public void recycle()
277    {
278        super.recycle();
279        _object = null;
280    }
281}