001/*
002 *  Copyright 2012 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.web.editor;
017
018import java.io.ByteArrayInputStream;
019import java.io.InputStream;
020import java.net.URL;
021import java.net.URLConnection;
022import java.util.Date;
023import java.util.HashSet;
024import java.util.Set;
025
026import org.apache.avalon.framework.activity.Initializable;
027import org.apache.avalon.framework.context.ContextException;
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.cocoon.Constants;
031import org.apache.cocoon.components.ContextHelper;
032import org.apache.cocoon.environment.Context;
033import org.apache.cocoon.environment.Request;
034import org.apache.cocoon.xml.AttributesImpl;
035import org.apache.commons.io.IOUtils;
036import org.apache.commons.io.output.ByteArrayOutputStream;
037import org.apache.commons.lang.StringUtils;
038import org.xml.sax.Attributes;
039import org.xml.sax.SAXException;
040
041import org.ametys.cms.repository.Content;
042import org.ametys.cms.transformation.htmledition.AbstractHTMLEditionHandler;
043import org.ametys.core.user.CurrentUserProvider;
044import org.ametys.core.user.UserIdentity;
045import org.ametys.plugins.explorer.resources.ModifiableResource;
046import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
047import org.ametys.plugins.explorer.resources.Resource;
048import org.ametys.plugins.explorer.resources.ResourceCollection;
049import org.ametys.runtime.config.Config;
050import org.ametys.web.repository.content.ModifiableWebContent;
051
052/**
053 * This transformer stores linked files as attachment from the incoming HTML.
054 */
055public class UploadedLinksHTMLEditionHandler extends AbstractHTMLEditionHandler implements Initializable
056{
057    
058    private CurrentUserProvider _userProvider;
059    
060    private Context _cocoonContext;
061
062    private boolean _tagToIgnore;
063    
064    private boolean _storeFileLinks;
065    
066    private Set<String> _storedFileExtensions;
067    
068    @Override
069    public void contextualize(org.apache.avalon.framework.context.Context context) throws ContextException
070    {
071        super.contextualize(context);
072        _cocoonContext = (Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
073    }
074    
075    @Override
076    public void service(ServiceManager serviceManager) throws ServiceException
077    {
078        super.service(serviceManager);
079        _userProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE);
080    }
081    
082    @Override
083    public void initialize() throws Exception
084    {
085        _storeFileLinks = Config.getInstance().getValueAsBoolean("store.external.files");
086        
087        _storedFileExtensions = new HashSet<>();
088        String extensions = Config.getInstance().getValueAsString("store.files.extensions");
089        for (String extension : StringUtils.split(extensions, ','))
090        {
091            _storedFileExtensions.add(extension.trim().toLowerCase());
092        }
093    }
094    
095    @Override
096    public void startDocument() throws SAXException
097    {
098        _tagToIgnore = false;
099        
100        super.startDocument();
101    }
102    
103    @Override
104    public void startElement(String uri, String loc, String raw, Attributes attrs) throws SAXException
105    {
106        if ("a".equals(raw) && _storeFileLinks)
107        {
108            boolean process = processLink(uri, loc, raw, attrs);
109            
110            if (!process)
111            {
112                return;
113            }
114        }
115        
116        super.startElement(uri, loc, raw, attrs);
117    }
118    
119    @Override
120    public void endElement(String uri, String loc, String raw) throws SAXException
121    {
122        if ("a".equals(raw) && _tagToIgnore)
123        {
124            // ignore a tag
125            _tagToIgnore = false;
126            return;
127        }
128        
129        super.endElement(uri, loc, raw);
130    }
131    
132    /**
133     * Process a link tag.
134     * @param uri the namespace URI.
135     * @param loc the local name (without prefix).
136     * @param raw the qualified name (with prefix).
137     * @param attrs the element attributes.
138     * @return true to process the link, false otherwise.
139     */
140    protected boolean processLink(String uri, String loc, String raw, Attributes attrs)
141    {
142        String type = attrs.getValue("data-ametys-type");
143        String ametysHref = attrs.getValue("data-ametys-href");
144        String ametysMeta = attrs.getValue("data-ametys-meta");
145        String href = attrs.getValue("href");
146        
147        if (type == null && ametysHref == null && href != null && ametysMeta != null)
148        {
149            try
150            {
151                URL url = new URL(href);
152                
153                int j = href.lastIndexOf('/');
154                String[] fileParts = StringUtils.split(href.substring(j + 1), "?#&");
155                if (fileParts.length > 0)
156                {
157                    String initialFileName = fileParts[0];
158                    
159                    if (StringUtils.isNotEmpty(initialFileName) && storeExternalFile(initialFileName))
160                    {
161                        URLConnection connection = url.openConnection();
162                        connection.setConnectTimeout(1000);
163                        connection.setReadTimeout(2000);
164                        
165                        try (InputStream is = connection.getInputStream())
166                        {
167                            ByteArrayOutputStream bos = new ByteArrayOutputStream();
168                            IOUtils.copy(is, bos);
169                            
170                            Resource resource = storeAttachment(initialFileName, new ByteArrayInputStream(bos.toByteArray()), null, null);
171                            
172                            AttributesImpl newAttrs = new AttributesImpl();
173                            
174                            _copyLinkAttributes(attrs, newAttrs);
175                            
176                            newAttrs.addCDATAAttribute("data-ametys-href", resource.getId());
177                            newAttrs.addCDATAAttribute("data-ametys-type", "attachment-content");
178                            
179                            super.startElement(uri, loc, raw, newAttrs);
180                            return false;
181                        }
182                    }
183                }
184            }
185            catch (Exception e)
186            {
187                // unable to fetch image, do not keep the img tag
188                getLogger().warn("Unable to fetch file from URL '" + href + "'. File is ignored.", e);
189                _tagToIgnore = true;
190                return false;
191            }
192        }
193        
194        return true;
195    }
196    
197    /**
198     * Test if the external file is to be stored as a local resource.
199     * @param fileName the file name.
200     * @return true if the external file is to be stored, false otherwise.
201     */
202    protected boolean storeExternalFile(String fileName)
203    {
204        String lowerFileName = fileName.toLowerCase();
205        
206        for (String extension : _storedFileExtensions)
207        {
208            if (lowerFileName.endsWith("." + extension))
209            {
210                return true;
211            }
212        }
213        
214        return false;
215    }
216    
217    /**
218     * Store a file as attachment.
219     * @param initialFileName the initial file name.
220     * @param is an input stream on the file.
221     * @param mimeType the file mime type.
222     * @param lastModified the last modification date.
223     * @return the final file name.
224     */
225    protected Resource storeAttachment(String initialFileName, InputStream is, String mimeType, Date lastModified)
226    {
227        Request request = ContextHelper.getRequest(_context);
228        Content content = (Content) request.getAttribute(Content.class.getName());
229        
230        String fileName = initialFileName;
231        
232        if (content instanceof ModifiableWebContent)
233        {
234            ModifiableWebContent webContent = (ModifiableWebContent) content;
235            int count = 2;
236            
237            ResourceCollection attachmentRoot = webContent.getRootAttachments();
238            
239            if (attachmentRoot != null && attachmentRoot instanceof ModifiableResourceCollection)
240            {
241                ModifiableResourceCollection root = (ModifiableResourceCollection) attachmentRoot;
242                
243                while (root.hasChild(fileName))
244                {
245                    int i = initialFileName.lastIndexOf('.');
246                    fileName = i == -1 ? initialFileName + '-' + (count++) : initialFileName.substring(0, i) + '-' + (count++) + initialFileName.substring(i);
247                }
248                
249                ModifiableResource resource = root.createChild(fileName, root.getResourceType());
250                Date finalLastModified = lastModified != null ? lastModified : new Date();
251                
252                String finalMimeType = mimeType != null ? mimeType : _cocoonContext.getMimeType(fileName.toLowerCase());
253                finalMimeType = finalMimeType != null ? finalMimeType : "application/unknown";
254                
255                UserIdentity creator = _userProvider.getUser();
256                
257                resource.setData(is, finalMimeType, finalLastModified, creator);
258                
259                return resource;
260            }
261        }
262        
263        return null;
264    }
265        
266    private void _copyLinkAttributes(Attributes attrs, AttributesImpl newAttrs)
267    {
268        for (int i = 0; i < attrs.getLength(); i++)
269        {
270            String name = attrs.getQName(i);
271            
272            if (!"data-ametys-href".equals(name) && !"data-ametys-type".equals(name) && !"data-ametys-meta".equals(name))
273            {
274                newAttrs.addAttribute(attrs.getURI(i), attrs.getLocalName(i), name, attrs.getType(i), attrs.getValue(i));
275            }
276        }
277    }
278    
279}