View Javadoc

1   /**
2    * This file Copyright (c) 2003-2012 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 freemarker.template.Template;
37  import info.magnolia.cms.core.Path;
38  import info.magnolia.cms.i18n.Messages;
39  import info.magnolia.cms.i18n.MessagesManager;
40  import info.magnolia.cms.security.AccessDeniedException;
41  import info.magnolia.cms.security.Security;
42  import info.magnolia.context.MgnlContext;
43  import info.magnolia.module.mail.MailTemplate;
44  import info.magnolia.module.mail.templates.MailAttachment;
45  
46  import org.apache.commons.httpclient.HttpClient;
47  import org.apache.commons.httpclient.NameValuePair;
48  import org.apache.commons.httpclient.cookie.CookiePolicy;
49  import org.apache.commons.httpclient.methods.GetMethod;
50  import org.apache.commons.httpclient.methods.PostMethod;
51  import org.apache.commons.lang.StringUtils;
52  import org.jdom.Attribute;
53  import org.jdom.Document;
54  import org.jdom.Element;
55  import org.jdom.filter.Filter;
56  import org.jdom.input.SAXBuilder;
57  import org.jdom.output.Format;
58  import org.jdom.output.XMLOutputter;
59  import org.w3c.tidy.Tidy;
60  import org.xml.sax.EntityResolver;
61  import org.xml.sax.InputSource;
62  import org.xml.sax.SAXException;
63  
64  import java.io.BufferedInputStream;
65  import java.io.File;
66  import java.io.FileOutputStream;
67  import java.io.IOException;
68  import java.io.InputStream;
69  import java.io.StringReader;
70  import java.io.StringWriter;
71  import java.net.MalformedURLException;
72  import java.net.URL;
73  import java.net.URLDecoder;
74  import java.util.ArrayList;
75  import java.util.HashMap;
76  import java.util.Iterator;
77  import java.util.Map;
78  
79  /**
80   * MgnlPageEmail.
81   * Date: Apr 6, 2006 Time: 9:24:29 PM
82   * @author <a href="mailto:niko@macnica.com">Nicolas Modrzyk</a>
83   *
84   */
85  public class MgnlPageEmail extends FreemarkerEmail {
86  
87      public static final String SUFFIX = "?mgnlIntercept=PREVIEW&mgnlPreview=true&mail=draw";
88  
89      private HttpClient client;
90  
91      private int cid = 0;
92  
93      private static final String UTF_8 = "UTF-8";
94  
95      private static final String MAGNOLIA = "magnolia";
96  
97      private static final String IMG = "img";
98  
99      private static final String SRC = "src";
100 
101     private static final String CID = "cid:";
102 
103     private static final String LINK = "link";
104 
105     private static final String HREF = "href";
106 
107     private static final String STYLE = "style";
108 
109     private static final String REL = "rel";
110 
111     private static final String ACTION = "action";
112 
113     private static final String LOGIN = "login";
114 
115     private static final String URL = "url";
116 
117     private static final String MGNL_USER_ID = "mgnlUserId";
118 
119     private static final String MGNL_USER_PSWD = "mgnlUserPSWD";
120 
121     private static final String SLASH = "/";
122 
123     private static final String HTTP = "http://";
124 
125     private static final String A_LINK = "a";
126 
127     private static final String FORM = "form";
128 
129     private static final String BODY = "body";
130 
131     private static final String FORM_ACTION = "action";
132 
133     private static final String DEFAULT_FORM_ACTION = "#";
134 
135 
136     public MgnlPageEmail(MailTemplate template) {
137         super(template);
138     }
139 
140     @Override
141     public void setBodyFromResourceFile() throws Exception {
142         String resourceFile = this.getTemplate().getTemplateFile();
143 
144         if(!StringUtils.contains(resourceFile, "http") ) {
145             String oriurl = MgnlContext.getAggregationState().getOriginalURL();
146             String temp = StringUtils.substring(oriurl, 0, StringUtils.indexOf(oriurl, MgnlContext.getContextPath()));
147             resourceFile = temp + MgnlContext.getContextPath() + resourceFile;
148         }
149 
150         URL url = new URL(resourceFile);
151         // retrieve the html content
152         String _content = retrieveContentFromMagnolia(resourceFile);
153         _content = cleanupHtmlCode(_content);
154         StringReader reader = new StringReader(_content);
155 
156         // filter the images
157         String urlBasePath = url.getProtocol() + "://" + url.getHost() + (url.getPort() > -1? ":" + url.getPort() : "" + "/");
158         String tmp = filterImages(urlBasePath, reader, url.toString());
159 
160         tmp = StringUtils.remove(tmp, "&#xD;");
161         super.setBody(tmp);
162     }
163 
164     protected String cleanupHtmlCode(String content) {
165         log.info("Cleaning html code");
166         content = content.replaceAll("<div ([.[^<>]]*)cms:edit([.[^<>]]*)>", "<div $1$2>");
167         content = cleanupHtmlCodeFromPageEditorCode(content);
168 
169         Tidy tidy = new Tidy();
170         tidy.setTidyMark(false);
171         tidy.setIndentContent(true);
172         tidy.setXmlTags(true);
173 
174         StringWriter writer = new StringWriter();
175 
176         tidy.parse(new StringReader(content), writer);
177 
178         return writer.toString();
179     }
180 
181     private String cleanupHtmlCodeFromPageEditorCode(String content) {
182         String cleanContent = StringUtils.substringBefore(content, "<!-- begin js and css added by @cms.init -->");
183         cleanContent += StringUtils.substringAfter(content, "<!-- end js and css added by @cms.init -->");
184         cleanContent = cleanContent.replaceAll("<!-- (/?)cms:(component|area|page) (.*)-->", "");
185         return cleanContent;
186     }
187 
188     // TODO : this is not used !
189     public void setBodyFromTemplate(Template template, Map _map) throws Exception {
190         final StringWriter writer = new StringWriter();
191         template.process(_map, writer);
192         writer.flush();
193         setBody(writer.toString());
194     }
195 
196 
197     private String filterImages(String urlBasePath, StringReader reader, String pageUrl) throws Exception {
198         log.info("Filtering images");
199         SAXBuilder parser = new SAXBuilder();
200         parser.setEntityResolver(new EntityResolver() {
201 
202             @Override
203             public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
204                 return new InputSource(new java.io.ByteArrayInputStream(new byte[0]));
205             }
206 
207         });
208         Document doc = parser.build(reader);
209         ArrayList toremove = new ArrayList();
210         ArrayList toadd = new ArrayList();
211         Element body = null;
212         // Filter content
213         Iterator iter = doc.getDescendants(new ContentFilter());
214         while (iter.hasNext()) {
215             Element elem = (Element) iter.next();
216             String name = elem.getName();
217             if (name.equalsIgnoreCase(IMG)) {
218                 // stream image and attach it to the email
219                 Attribute att = elem.getAttribute(SRC);
220                 if (log.isDebugEnabled()) {
221                     log.debug("Found new img:" + att.toString());
222                 }
223                 String value = att.getValue();
224                 this.cid++;
225                 att.setValue(CID + (this.cid));
226                 String url = getUrl(pageUrl, value);
227 
228                 if (log.isDebugEnabled()) {
229                     log.debug("Url is:" + url);
230                 }
231                 this.getTemplate().addAttachment(new MailAttachment(getAttachmentFile(url).toURL(), String.valueOf(this.cid), MailAttachment.MIME_RELATED));
232             }
233             else if (name.equalsIgnoreCase(LINK)) {
234                 // stream the css and put the content into a <style> tag and add to body tag
235                 Attribute att = elem.getAttribute(HREF);
236                 Element el = (Element) elem.clone();
237                 //Element el = elem;
238                 if (log.isDebugEnabled()) {
239                     log.debug("Found new css:" + att.toString());
240                 }
241                 String url = getUrl(pageUrl, att.getValue());
242                 el.setName(STYLE);
243                 el.removeAttribute(HREF);
244                 el.removeAttribute(REL);
245                 GetMethod streamCss = new GetMethod(url);
246                 getHttpClient(url).executeMethod(streamCss);
247                 String tmp = streamCss.getResponseBodyAsString();
248                 tmp = processUrls(tmp, url);
249                 el.setText(tmp);
250                 toremove.add(elem);
251                 toadd.add(el);
252 
253             } else if(name.equalsIgnoreCase(A_LINK)) {
254                 Attribute att = elem.getAttribute(HREF);
255 
256                 String url = getUrl(pageUrl, att.getValue());
257                 if(!att.getValue().startsWith(DEFAULT_FORM_ACTION)) {
258                     att.setValue(url);
259                 }
260 
261             } else if(name.equalsIgnoreCase(FORM)) {
262                 Attribute att = elem.getAttribute(FORM_ACTION);
263                 String url = att.getValue();
264                 if(att.getValue().equals(DEFAULT_FORM_ACTION)) {
265                     url = pageUrl;
266                 }
267                 att.setValue(url);
268 
269             } else if(name.equalsIgnoreCase(BODY)) {
270                 body = elem;
271             }
272         }
273 
274         // this is ugly but is there to
275         // avoid concurrent modification exception on the Document
276         for (int i = 0; i < toremove.size(); i++) {
277             Element elem = (Element) toremove.get(i);
278             Element parent = elem.getParentElement();
279 
280             body.addContent(0, (Element) toadd.get(i));
281             parent.removeContent(elem);
282 
283         }
284 
285         // create the return string reader with new document content
286         Format format = Format.getRawFormat();
287         format.setExpandEmptyElements(true);
288 
289 
290         XMLOutputter outputter = new XMLOutputter(format);
291         StringWriter writer = new StringWriter();
292         return outputter.outputString(doc);
293     }
294 
295 
296     private String getUrl(String currentPagePath, String path) {
297         String urlBasePath = currentPagePath.substring(0, currentPagePath.indexOf(MgnlContext.getContextPath()) );
298         if(!StringUtils.contains(path, HTTP) && !StringUtils.contains(path, MgnlContext.getContextPath())) {
299             return currentPagePath.substring(0, currentPagePath.lastIndexOf("/") + 1) + path;
300         } else if(!StringUtils.contains(path, HTTP)) {
301             return urlBasePath + path;
302         }
303         return path;
304     }
305 
306     private String processUrls(String responseBodyAsString, String cssPath) throws MalformedURLException, Exception {
307         String tmp = "";
308         Map<String, String> map = new HashMap<String, String>();
309 
310         int urlIndex = 0;
311         int closeIndex = 0;
312         int begin = 0;
313         int cid;
314 
315       //  List urls = new Array
316         while(StringUtils.indexOf(responseBodyAsString, "url(", begin) >= 0) {
317             urlIndex = StringUtils.indexOf(responseBodyAsString, "url(", begin) + "url(".length();
318             closeIndex = StringUtils.indexOf(responseBodyAsString, ")", urlIndex) ;
319 
320             String url = StringUtils.substring(responseBodyAsString, urlIndex, closeIndex);
321             url = getUrl(cssPath, url).replaceAll("\"", "");
322             url = "\"" + url + "\"";
323             if(!StringUtils.isEmpty(url) && !map.containsKey(url)) {
324                 map.put(url, url);
325 
326             } else if (map.containsKey(url)) {
327                 url = map.get(url);
328 
329             }
330             tmp += StringUtils.substring(responseBodyAsString, begin, urlIndex) + url;
331             begin = closeIndex;
332 
333         }
334 
335         tmp += StringUtils.substring(responseBodyAsString, closeIndex);
336         return tmp;
337     }
338 
339 
340     private File getAttachmentFile(String url) throws Exception {
341         log.info("Streaming content of url:" + url + " to a temporary file");
342 
343         // Execute an http get on the url
344         GetMethod redirect = new GetMethod(url);
345         getHttpClient(url).executeMethod(redirect);
346 
347         URL _url = new URL(url);
348         String file = URLDecoder.decode(_url.getFile(), "UTF-8");
349 
350         // create file in temp dir, with just the file name.
351         File tempFile = new File(Path.getTempDirectoryPath()
352             + File.separator
353             + file.substring(file.lastIndexOf(SLASH) + 1));
354         // if same file and same size, return, do not process
355         if (tempFile.exists() && redirect.getResponseContentLength() == tempFile.length()) {
356             redirect.releaseConnection();
357             return tempFile;
358         }
359 
360         // stream the content to the temp file
361         FileOutputStream out = new FileOutputStream(tempFile);
362         final int BUFFER_SIZE = 1 << 10 << 3; // 8KiB buffer
363         byte[] buffer = new byte[BUFFER_SIZE];
364         int bytesRead;
365         InputStream in = new BufferedInputStream(redirect.getResponseBodyAsStream());
366         while (true) {
367             bytesRead = in.read(buffer);
368             if (bytesRead > -1) {
369                 out.write(buffer, 0, bytesRead);
370             }
371             else {
372                 break;
373             }
374         }
375 
376         // cleanup
377         in.close();
378         out.close();
379         redirect.releaseConnection();
380 
381         return tempFile;
382     }
383 
384 
385     private HttpClient getHttpClient(String baseURL) throws Exception {
386         if (this.client == null) {
387             URL location = new URL(baseURL);
388             this.client = getHttpClientForUser(location);
389         }
390         return this.client;
391     }
392 
393 
394     private HttpClient getHttpClientForUser(URL location) throws IOException {
395         HttpClient _client = new HttpClient();
396         _client.getHostConfiguration().setHost(location.getHost(), location.getPort(), location.getProtocol());
397         _client.getParams().setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);
398         String user = getTemplate().getUsername();
399         String pass = getTemplate().getPassword();
400         log.info("Creating http client for user:" + user);
401         // login using the id and password of the current user
402         PostMethod authpost = new PostMethod(location.getPath());
403         NameValuePair action = new NameValuePair(ACTION, LOGIN);
404         NameValuePair url = new NameValuePair(URL, location.getPath());
405         NameValuePair userid = new NameValuePair(MGNL_USER_ID, user);
406         NameValuePair password = new NameValuePair(MGNL_USER_PSWD, pass);
407         authpost.setRequestBody(new NameValuePair[]{action, url, userid, password});
408         _client.executeMethod(authpost);
409         authpost.releaseConnection();
410         return _client;
411     }
412 
413     private String retrieveContentFromMagnolia(String _url) throws Exception {
414         log.info("Retrieving content from magnolia:" + _url);
415         GetMethod redirect = new GetMethod(_url + SUFFIX);
416         int code = getHttpClient(_url).executeMethod(redirect);
417         if(code == 403){
418             String user = getTemplate().getUsername().isEmpty() ? Security.getAnonymousUser().getName() : getTemplate().getUsername();
419             throw new AccessDeniedException(getMessages().get("page.form.page.message.accessdenied", new String[]{user, _url}));
420         }
421         String response = redirect.getResponseBodyAsString();
422         redirect.releaseConnection();
423         return response;
424     }
425 
426     /**
427      * Content filter.
428      *
429      */
430     static class ContentFilter implements Filter {
431 
432 
433         private static final long serialVersionUID = 1L;
434 
435         @Override
436         public boolean matches(Object object) {
437             if (object instanceof Element) {
438                 Element e = (Element) object;
439                 return e.getName().equalsIgnoreCase(LINK) || e.getName().equalsIgnoreCase(IMG)
440                 || e.getName().equalsIgnoreCase(A_LINK) || e.getName().equalsIgnoreCase(FORM)
441                 || e.getName().equalsIgnoreCase(BODY);
442             }
443 
444             return false;
445 
446         }
447     }
448 
449     public Messages getMessages() {
450         return MessagesManager.getMessages("info.magnolia.module.mail.messages");
451     }
452 }
453