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