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    private int _cropWidth;
079    private int _cropHeight;
080    
081    @Override
082    public void service(ServiceManager sManager) throws ServiceException
083    {
084        super.service(sManager);
085        _resolver = (AmetysObjectResolver) sManager.lookup(AmetysObjectResolver.ROLE);
086        _rightManager = (RightManager) sManager.lookup(RightManager.ROLE);
087        _currentUserProvider = (CurrentUserProvider) sManager.lookup(CurrentUserProvider.ROLE);
088    }
089    
090    @Override
091    public void setup(SourceResolver sResolver, Map objModel, String src, Parameters par) throws ProcessingException, SAXException, IOException
092    {
093        super.setup(sResolver, objModel, src, par);
094        
095        String id = par.getParameter("id", null);
096        String path = par.getParameter("path", null);
097        String version = par.getParameter("version", null);
098        
099        // parameters for image resizing
100        _width = par.getParameterAsInteger("width", 0);
101        _height = par.getParameterAsInteger("height", 0);
102        _maxWidth = par.getParameterAsInteger("maxWidth", 0);
103        _maxHeight = par.getParameterAsInteger("maxHeight", 0);
104        _cropWidth = par.getParameterAsInteger("cropWidth", 0);
105        _cropHeight = par.getParameterAsInteger("cropHeight", 0);
106        
107        _readForDownload = par.getParameterAsBoolean("download", false);
108        Response response = ObjectModelHelper.getResponse(objectModel);
109        
110        try
111        {
112            if (id != null)
113            {
114                _object = _resolver.resolveById(id);
115            }
116            else
117            {
118                _object = _resolver.resolveByPath(_decodePath(path.substring(1)));
119            }
120
121            // Check user access
122            checkUserAccess();
123            
124            if (!StringUtils.isEmpty(version) && _object instanceof VersionableAmetysObject)
125            {
126                ((VersionableAmetysObject) _object).switchToRevision(version);
127            }
128
129            if (_readForDownload)
130            {
131                String name = _object.getName();
132                String encodedName = null;
133                try
134                {
135                    URI uri = new URI(null, null, name, null);
136                    encodedName = uri.toASCIIString();
137                    // EXPLORER-358 : Fix for Chrome which does not support comma in filename
138                    encodedName = encodedName.replaceAll(",", "%2C");
139                }
140                catch (URISyntaxException e)
141                {
142                    // do nothing and send no encoded name
143                }
144                
145                name = name.replaceAll("\\\\", "\\\\\\\\");
146                name = name.replaceAll("\\\"", "\\\\\\\"");
147                response.setHeader("Content-Disposition", "attachment; filename=\"" + (encodedName != null ? encodedName : name) + "\"" + (encodedName != null ? ";filename*=UTF-8''" + encodedName : ""));
148            }
149        }
150        catch (UnknownAmetysObjectException e)
151        {
152            if (id != null)
153            {
154                throw new ResourceNotFoundException(String.format("The resource with id '%s' does not exist", id));
155            }
156            else
157            {
158                throw new ResourceNotFoundException(String.format("The resource at path '%s' does not exist", path));
159            }
160            
161        }
162    }
163    
164    /**
165     * Check the user access
166     * @throws AuthorizationRequiredException if authorization is required
167     * @throws AccessDeniedException if user has no access
168     */
169    protected void checkUserAccess() throws AuthorizationRequiredException, AccessDeniedException
170    {
171        UserIdentity user = _currentUserProvider.getUser();
172        
173        if (user == null)
174        {
175            // Check anonymous access
176            if (!_rightManager.hasAnonymousReadAccess(_object.getParent()))
177            {
178                throw new AuthorizationRequiredException(null);
179            }
180        }
181        else if (!_rightManager.hasReadAccess(user, _object.getParent()))
182        {
183            throw new AccessDeniedException("User " + user + " has no right to access the resource " + _object.getId());
184        }
185    }
186    
187    /**
188     * Decode the resource path
189     * @param path the resource path
190     * @return the decoded resource path
191     * @throws UnsupportedEncodingException if UTF-8 encoding is not supported
192     */
193    protected String _decodePath (String path) throws UnsupportedEncodingException
194    {
195        StringBuffer sb = new StringBuffer();
196        
197        String[] parts = path.split("/");
198        for (String part : parts)
199        {
200            sb.append("/");
201            sb.append(URLDecoder.decode(part, "utf-8"));
202        }
203        return sb.toString();
204    }
205
206    @Override
207    public Serializable getKey()
208    {
209        return _object.getId() + "#" + _height + "#" + _width + "#" + _maxHeight + "#" + _maxWidth + "#" + _cropHeight + "#" + _cropWidth;
210    }
211
212    @Override
213    public SourceValidity getValidity()
214    {
215        return new TimeStampValidity(getLastModified());
216    }
217    
218    @Override
219    public long getLastModified()
220    {
221        return _object.getLastModified().getTime();
222    }
223    
224    @Override
225    public String getMimeType()
226    {
227        return _object.getMimeType();
228    }
229    
230    @Override
231    public void generate() throws IOException, SAXException, ProcessingException
232    {
233        String name = _object.getName();
234        name = name.replaceAll("\\\\", "\\\\\\\\");
235        name = name.replaceAll("\\\"", "\\\\\\\"");
236        
237        try (InputStream is = _object.getInputStream())
238        {
239            int i = name.lastIndexOf('.');
240            String format = i != -1 ? name.substring(i + 1) : "png";
241            format = _allowedFormats.contains(format) ? format : "png";
242
243            if (_isImage())
244            {
245                ImageHelper.generateThumbnail(is, out, format, _height, _width, _maxHeight, _maxWidth, _cropHeight, _cropWidth);
246            }
247            else
248            {
249                IOUtils.copy(is, out);
250            }
251        }
252        catch (Exception e)
253        {
254            throw new ProcessingException("Unable to download file of id " + _object.getId(), e);
255        }
256        finally
257        {
258            IOUtils.closeQuietly(out);
259        }
260    }
261    
262    /**
263     * Determines if the file is an image
264     * @return <code>true</code> if file is a image
265     */
266    protected boolean _isImage()
267    {
268        if (_width > 0 || _height > 0 || _maxHeight > 0 || _maxWidth > 0 || _cropHeight > 0 || _cropWidth > 0)
269        {
270            // resize is required, assume this is a image
271            return true;
272        }
273        else
274        {
275            return getMimeType() != null && getMimeType().startsWith("image/");
276        }
277    }
278    
279    @Override
280    public void recycle()
281    {
282        super.recycle();
283        _object = null;
284    }
285}