View Javadoc
1   /**
2    * This file Copyright (c) 2003-2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.module.mail.templates.impl;
35  
36  import info.magnolia.cms.core.Path;
37  import info.magnolia.cms.i18n.Messages;
38  import info.magnolia.cms.i18n.MessagesManager;
39  import info.magnolia.cms.security.AccessDeniedException;
40  import info.magnolia.cms.security.Security;
41  import info.magnolia.context.MgnlContext;
42  import info.magnolia.module.mail.MailTemplate;
43  import info.magnolia.module.mail.templates.MailAttachment;
44  
45  import java.io.BufferedInputStream;
46  import java.io.File;
47  import java.io.FileOutputStream;
48  import java.io.IOException;
49  import java.io.InputStream;
50  import java.io.StringReader;
51  import java.io.StringWriter;
52  import java.net.URL;
53  import java.net.URLDecoder;
54  import java.util.Arrays;
55  import java.util.HashMap;
56  import java.util.Iterator;
57  import java.util.List;
58  import java.util.Map;
59  
60  import org.apache.commons.lang3.StringUtils;
61  import org.apache.http.HttpHost;
62  import org.apache.http.HttpResponse;
63  import org.apache.http.NameValuePair;
64  import org.apache.http.client.HttpClient;
65  import org.apache.http.client.config.CookieSpecs;
66  import org.apache.http.client.config.RequestConfig;
67  import org.apache.http.client.entity.UrlEncodedFormEntity;
68  import org.apache.http.client.methods.HttpGet;
69  import org.apache.http.client.methods.HttpPost;
70  import org.apache.http.impl.client.BasicResponseHandler;
71  import org.apache.http.impl.client.HttpClientBuilder;
72  import org.apache.http.message.BasicNameValuePair;
73  import org.jdom.Attribute;
74  import org.jdom.Document;
75  import org.jdom.Element;
76  import org.jdom.filter.Filter;
77  import org.jdom.input.SAXBuilder;
78  import org.jdom.output.Format;
79  import org.jdom.output.XMLOutputter;
80  import org.w3c.tidy.Tidy;
81  import org.xml.sax.EntityResolver;
82  import org.xml.sax.InputSource;
83  import org.xml.sax.SAXException;
84  
85  import com.google.common.collect.Lists;
86  
87  import freemarker.template.Template;
88  
89  /**
90   * MgnlPageEmail.
91   * Date: Apr 6, 2006 Time: 9:24:29 PM
92   * @author <a href="mailto:niko@macnica.com">Nicolas Modrzyk</a>
93   *
94   */
95  public class MgnlPageEmail extends FreemarkerEmail {
96  
97      public static final String SUFFIX = "?mgnlIntercept=PREVIEW&mgnlPreview=true&mail=draw";
98  
99      private HttpClient client;
100     private int cid = 0;
101 
102     private static final String IMG = "img";
103     private static final String SRC = "src";
104     private static final String CID = "cid:";
105     private static final String LINK = "link";
106     private static final String HREF = "href";
107     private static final String STYLE = "style";
108     private static final String REL = "rel";
109     private static final String ACTION = "action";
110     private static final String LOGIN = "login";
111     private static final String URL = "url";
112     private static final String MGNL_USER_ID = "mgnlUserId";
113     private static final String MGNL_USER_PSWD = "mgnlUserPSWD";
114     private static final String SLASH = "/";
115     private static final String HTTP = "http://";
116     private static final String A_LINK = "a";
117     private static final String FORM = "form";
118     private static final String BODY = "body";
119     private static final String FORM_ACTION = "action";
120     private static final String DEFAULT_FORM_ACTION = "#";
121 
122     public MgnlPageEmail(MailTemplate template) {
123         super(template);
124     }
125 
126     @Override
127     public void setBodyFromResourceFile() throws Exception {
128         String resourceFile = this.getTemplate().getTemplateFile();
129         if (!StringUtils.contains(resourceFile, "http")) {
130             String reqUrl = MgnlContext.getAggregationState().getOriginalURL();
131             resourceFile = reqUrl.substring(0, StringUtils.indexOf(reqUrl, MgnlContext.getAggregationState().getOriginalURI())) + resourceFile;
132         }
133 
134         URL url = new URL(resourceFile);
135         // retrieve the html content
136         String content = retrieveContentFromMagnolia(resourceFile);
137         // TODO: review below processing of HTML content processing
138         content = cleanupHtmlCode(content);
139         if (StringUtils.isNotBlank(content)) {
140             try {
141                 // filter the images
142                 String tmp = filterImages(new StringReader(content), url.toString());
143                 tmp = StringUtils.remove(content, "&#xD;");
144                 super.setBody(tmp);
145             } catch (Exception e) {
146                 log.warn("Can't filter image from email content.", e);
147                 super.setBody(StringUtils.EMPTY);
148             }
149         } else {
150             super.setBody("<a href='" + url.toString() + "'>" + url.toString() + "</a>");
151         }
152     }
153 
154     protected String cleanupHtmlCode(String content) {
155         log.info("Cleaning html code");
156         content = content.replaceAll("<div ([.[^<>]]*)cms:edit([.[^<>]]*)>", "<div $1$2>");
157         content = cleanupHtmlCodeFromPageEditorCode(content);
158 
159         Tidy tidy = new Tidy();
160         tidy.setTidyMark(false);
161         tidy.setIndentContent(true);
162         tidy.setXmlTags(true);
163 
164         StringWriter writer = new StringWriter();
165 
166         tidy.parse(new StringReader(content), writer);
167 
168         return writer.toString();
169     }
170 
171     private String cleanupHtmlCodeFromPageEditorCode(String content) {
172         String cleanContent = StringUtils.substringBefore(content, "<!-- begin js and css added by @cms.init -->");
173         cleanContent += StringUtils.substringAfter(content, "<!-- end js and css added by @cms.init -->");
174         cleanContent = cleanContent.replaceAll("<!-- (/?)cms:(component|area|page) (.*)-->", "");
175         return cleanContent;
176     }
177 
178     // TODO : this is not used !
179     public void setBodyFromTemplate(Template template, Map _map) throws Exception {
180         final StringWriter writer = new StringWriter();
181         template.process(_map, writer);
182         writer.flush();
183         setBody(writer.toString());
184     }
185 
186 
187     private String filterImages(StringReader reader, String pageUrl) throws Exception {
188         log.info("Filtering images");
189         SAXBuilder parser = new SAXBuilder();
190         parser.setEntityResolver(new EntityResolver() {
191 
192             @Override
193             public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
194                 return new InputSource(new java.io.ByteArrayInputStream(new byte[0]));
195             }
196 
197         });
198         Document doc = parser.build(reader);
199         List<Element> toremove = Lists.newArrayList();
200         List<Element> toadd = Lists.newArrayList();
201         Element body = null;
202         // Filter content
203         Iterator iter = doc.getDescendants(new ContentFilter());
204         while (iter.hasNext()) {
205             Element elem = (Element) iter.next();
206             String name = elem.getName();
207             if (name.equalsIgnoreCase(IMG)) {
208                 // stream image and attach it to the email
209                 Attribute att = elem.getAttribute(SRC);
210                 if (log.isDebugEnabled()) {
211                     log.debug("Found new img:" + att.toString());
212                 }
213                 String value = att.getValue();
214                 this.cid++;
215                 att.setValue(CID + (this.cid));
216                 String url = getUrl(pageUrl, value);
217 
218                 if (log.isDebugEnabled()) {
219                     log.debug("Url is:" + url);
220                 }
221                 this.getTemplate().addAttachment(new MailAttachment(getAttachmentFile(url).toURL(), String.valueOf(this.cid), MailAttachment.MIME_RELATED));
222             } else if (name.equalsIgnoreCase(LINK)) {
223                 // stream the css and put the content into a <style> tag and add to body tag
224                 Attribute att = elem.getAttribute(HREF);
225                 Element el = (Element) elem.clone();
226                 //Element el = elem;
227                 if (log.isDebugEnabled()) {
228                     log.debug("Found new css:" + att.toString());
229                 }
230                 String url = getUrl(pageUrl, att.getValue());
231                 el.setName(STYLE);
232                 el.removeAttribute(HREF);
233                 el.removeAttribute(REL);
234                 HttpGet streamCss = new HttpGet(url);
235                 HttpResponse response = getHttpClient(url).execute(streamCss);
236                 String tmp = new BasicResponseHandler().handleResponse(response);
237                 tmp = processUrls(tmp, url);
238                 el.setText(tmp);
239                 toremove.add(elem);
240                 toadd.add(el);
241 
242             } else if (name.equalsIgnoreCase(A_LINK)) {
243                 Attribute att = elem.getAttribute(HREF);
244 
245                 String url = getUrl(pageUrl, att.getValue());
246                 if (!att.getValue().startsWith(DEFAULT_FORM_ACTION)) {
247                     att.setValue(url);
248                 }
249 
250             } else if (name.equalsIgnoreCase(FORM)) {
251                 Attribute att = elem.getAttribute(FORM_ACTION);
252                 String url = att.getValue();
253                 if (att.getValue().equals(DEFAULT_FORM_ACTION)) {
254                     url = pageUrl;
255                 }
256                 att.setValue(url);
257 
258             } else if (name.equalsIgnoreCase(BODY)) {
259                 body = elem;
260             }
261         }
262 
263         // this is ugly but is there to
264         // avoid concurrent modification exception on the Document
265         for (int i = 0; i < toremove.size(); i++) {
266             Element elem = toremove.get(i);
267             Element parent = elem.getParentElement();
268 
269             body.addContent(0, toadd.get(i));
270             parent.removeContent(elem);
271 
272         }
273 
274         // create the return string reader with new document content
275         Format format = Format.getRawFormat();
276         format.setExpandEmptyElements(true);
277 
278 
279         XMLOutputter outputter = new XMLOutputter(format);
280         return outputter.outputString(doc);
281     }
282 
283 
284     private String getUrl(String currentPagePath, String path) {
285         String urlBasePath = currentPagePath.substring(0, currentPagePath.indexOf(MgnlContext.getContextPath()));
286         if (!StringUtils.contains(path, HTTP) && !StringUtils.contains(path, MgnlContext.getContextPath())) {
287             return currentPagePath.substring(0, currentPagePath.lastIndexOf("/") + 1) + path;
288         } else if (!StringUtils.contains(path, HTTP)) {
289             return urlBasePath + path;
290         }
291         return path;
292     }
293 
294     private String processUrls(String responseBodyAsString, String cssPath) throws Exception {
295         String tmp = "";
296         Map<String, String> map = new HashMap<>();
297 
298         int urlIndex;
299         int closeIndex = 0;
300         int begin = 0;
301 
302         //  List urls = new Array
303         while (StringUtils.indexOf(responseBodyAsString, "url(", begin) >= 0) {
304             urlIndex = StringUtils.indexOf(responseBodyAsString, "url(", begin) + "url(".length();
305             closeIndex = StringUtils.indexOf(responseBodyAsString, ")", urlIndex);
306 
307             String url = StringUtils.substring(responseBodyAsString, urlIndex, closeIndex);
308             url = getUrl(cssPath, url).replaceAll("\"", "");
309             url = "\"" + url + "\"";
310             if (!StringUtils.isEmpty(url) && !map.containsKey(url)) {
311                 map.put(url, url);
312 
313             } else if (map.containsKey(url)) {
314                 url = map.get(url);
315 
316             }
317             tmp += StringUtils.substring(responseBodyAsString, begin, urlIndex) + url;
318             begin = closeIndex;
319 
320         }
321 
322         tmp += StringUtils.substring(responseBodyAsString, closeIndex);
323         return tmp;
324     }
325 
326     private File getAttachmentFile(String urlString) throws Exception {
327         log.info("Streaming content of url:" + urlString + " to a temporary file");
328 
329         // Execute an http get on the url
330         HttpGet redirect = new HttpGet(urlString);
331         HttpResponse response = getHttpClient(urlString).execute(redirect);
332 
333         URL url = new URL(urlString);
334         String file = URLDecoder.decode(url.getFile(), "UTF-8");
335 
336         // create file in temp dir, with just the file name.
337         File tempFile = new File(Path.getTempDirectoryPath()
338                 + File.separator
339                 + file.substring(file.lastIndexOf(SLASH) + 1));
340         // if same file and same size, return, do not process
341         if (tempFile.exists() && response.getEntity().getContentLength() == tempFile.length()) {
342             redirect.releaseConnection();
343             return tempFile;
344         }
345 
346         // stream the content to the temp file
347         FileOutputStream out = new FileOutputStream(tempFile);
348         final int BUFFER_SIZE = 1 << 10 << 3; // 8KiB buffer
349         byte[] buffer = new byte[BUFFER_SIZE];
350         int bytesRead;
351         InputStream in = new BufferedInputStream(response.getEntity().getContent());
352         while (true) {
353             bytesRead = in.read(buffer);
354             if (bytesRead > -1) {
355                 out.write(buffer, 0, bytesRead);
356             } else {
357                 break;
358             }
359         }
360 
361         // cleanup
362         in.close();
363         out.close();
364         redirect.releaseConnection();
365 
366         return tempFile;
367     }
368 
369 
370     private HttpClient getHttpClient(String baseURL) throws Exception {
371         if (this.client == null) {
372             URL location = new URL(baseURL);
373             this.client = getHttpClientForUser(location);
374         }
375         return this.client;
376     }
377 
378 
379     private HttpClient getHttpClientForUser(URL location) throws IOException {
380         RequestConfig requestConfig = RequestConfig.custom()
381                 .setCookieSpec(CookieSpecs.BROWSER_COMPATIBILITY)
382                 .build();
383         HttpClient client = HttpClientBuilder.create()
384                 .setProxy(new HttpHost(location.getHost(), location.getPort(), location.getProtocol()))
385                 .setDefaultRequestConfig(requestConfig)
386                 .build();
387 
388 
389         String user = getTemplate().getUsername();
390         HttpPost authpost = new HttpPost(location.toString());
391         NameValuePair url = new BasicNameValuePair(URL, location.getPath());
392         List<NameValuePair> params = Arrays.asList(url);
393         if (user != null) {
394             // login using the id and password of the current user
395             log.info("Creating http client for user:" + user);
396             String pass = getTemplate().getPassword();
397             NameValuePair action = new BasicNameValuePair(ACTION, LOGIN);
398             NameValuePair userid = new BasicNameValuePair(MGNL_USER_ID, user);
399             NameValuePair password = new BasicNameValuePair(MGNL_USER_PSWD, pass);
400             params = Arrays.asList(action, url, userid, password);
401         }
402         authpost.setEntity(new UrlEncodedFormEntity(params));
403         client.execute(authpost);
404         authpost.releaseConnection();
405         return client;
406     }
407 
408     private String retrieveContentFromMagnolia(String _url) throws Exception {
409         log.info("Retrieving content from magnolia:" + _url);
410         HttpGet redirect = new HttpGet(_url + SUFFIX);
411         HttpResponse response = getHttpClient(_url).execute(redirect);
412         int code = response.getStatusLine().getStatusCode();
413         if (code == 403) {
414             String user = getTemplate().getUsername().isEmpty() ? Security.getAnonymousUser().getName() : getTemplate().getUsername();
415             throw new AccessDeniedException(getMessages().get("page.form.page.message.accessdenied", new String[]{user, _url}));
416         }
417         String responseBody = new BasicResponseHandler().handleResponse(response);
418         redirect.releaseConnection();
419         return responseBody;
420     }
421 
422     /**
423      * Content filter.
424      *
425      */
426     static class ContentFilter implements Filter {
427 
428 
429         private static final long serialVersionUID = 1L;
430 
431         @Override
432         public boolean matches(Object object) {
433             if (object instanceof Element) {
434                 Element e = (Element) object;
435                 return e.getName().equalsIgnoreCase(LINK) || e.getName().equalsIgnoreCase(IMG)
436                         || e.getName().equalsIgnoreCase(A_LINK) || e.getName().equalsIgnoreCase(FORM)
437                         || e.getName().equalsIgnoreCase(BODY);
438             }
439 
440             return false;
441 
442         }
443     }
444 
445     public Messages getMessages() {
446         return MessagesManager.getMessages("info.magnolia.module.mail.messages");
447     }
448 }