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().getValue("store.external.files"); 086 087 _storedFileExtensions = new HashSet<>(); 088 String extensions = Config.getInstance().getValue("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}