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}