1 /**
2  * Copyright (C) 2012 Google Inc.
3  * Licensed to The Android Open Source Project.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package com.android.mail.ui;
19 
20 import android.content.Context;
21 import android.support.v4.text.TextUtilsCompat;
22 import android.support.v4.view.ViewCompat;
23 
24 import com.android.mail.R;
25 import com.android.mail.utils.LogTag;
26 import com.android.mail.utils.LogUtils;
27 import com.android.mail.utils.Utils;
28 import com.google.common.annotations.VisibleForTesting;
29 
30 import java.util.Locale;
31 import java.util.regex.Pattern;
32 
33 /**
34  * Renders data into very simple string-substitution HTML templates for conversation view.
35  */
36 public class HtmlConversationTemplates extends AbstractHtmlTemplates {
37 
38     /**
39      * Prefix applied to a message id for use as a div id
40      */
41     public static final String MESSAGE_PREFIX = "m";
42     public static final int MESSAGE_PREFIX_LENGTH = MESSAGE_PREFIX.length();
43 
44     private static final String TAG = LogTag.getLogTag();
45 
46     /**
47      * Pattern for HTML img tags with a "src" attribute where the value is an absolutely-specified
48      * HTTP or HTTPS URL. In other words, these are images with valid URLs that we should munge to
49      * prevent WebView from firing bad onload handlers for them. Part of the workaround for
50      * b/5522414.
51      *
52      * Pattern documentation:
53      * There are 3 top-level parts of the pattern:
54      * 1. required preceding string
55      * 2. the literal string "src"
56      * 3. required trailing string
57      *
58      * The preceding string must be an img tag "<img " with intermediate spaces allowed. The
59      * trailing whitespace is required.
60      * Non-whitespace chars are allowed before "src", but if they are present, they must be followed
61      * by another whitespace char. The idea is to allow other attributes, and avoid matching on
62      * "src" in a later attribute value as much as possible.
63      *
64      * The following string must contain "=" and "http", with intermediate whitespace and single-
65      * and double-quote allowed in between. The idea is to avoid matching Gmail-hosted relative URLs
66      * for inline attachment images of the form "?view=KEYVALUES".
67      *
68      */
69     private static final Pattern sAbsoluteImgUrlPattern = Pattern.compile(
70             "(<\\s*img\\s+(?:[^>]*\\s+)?)src(\\s*=[\\s'\"]*http)", Pattern.CASE_INSENSITIVE
71                     | Pattern.MULTILINE);
72     /**
73      * The text replacement for {@link #sAbsoluteImgUrlPattern}. The "src" attribute is set to
74      * something inert and not left unset to minimize interactions with existing JS.
75      */
76     private static final String IMG_URL_REPLACEMENT = "$1src='data:' blocked-src$2";
77 
78     private static final String LEFT_TO_RIGHT_TRIANGLE = "\u25B6 ";
79     private static final String RIGHT_TO_LEFT_TRIANGLE = "\u25C0 ";
80 
81     private static boolean sLoadedTemplates;
82     private static String sSuperCollapsed;
83     private static String sMessage;
84     private static String sConversationUpper;
85     private static String sConversationLower;
86 
HtmlConversationTemplates(Context context)87     public HtmlConversationTemplates(Context context) {
88         super(context);
89 
90         // The templates are small (~2KB total in ICS MR2), so it's okay to load them once and keep
91         // them in memory.
92         if (!sLoadedTemplates) {
93             sLoadedTemplates = true;
94             sSuperCollapsed = readTemplate(R.raw.template_super_collapsed);
95             sMessage = readTemplate(R.raw.template_message);
96             sConversationUpper = readTemplate(R.raw.template_conversation_upper);
97             sConversationLower = readTemplate(R.raw.template_conversation_lower);
98         }
99     }
100 
appendSuperCollapsedHtml(int firstCollapsed, int blockHeight)101     public void appendSuperCollapsedHtml(int firstCollapsed, int blockHeight) {
102         if (!mInProgress) {
103             throw new IllegalStateException("must call startConversation first");
104         }
105 
106         append(sSuperCollapsed, firstCollapsed, blockHeight);
107     }
108 
109     @VisibleForTesting
replaceAbsoluteImgUrls(final String html)110     static String replaceAbsoluteImgUrls(final String html) {
111         return sAbsoluteImgUrlPattern.matcher(html).replaceAll(IMG_URL_REPLACEMENT);
112     }
113 
114     /**
115      * Wrap a given message body string to prevent its contents from flowing out of the current DOM
116      * block context.
117      *
118      */
wrapMessageBody(String msgBody)119     public static String wrapMessageBody(String msgBody) {
120         // FIXME: this breaks RTL for an as-yet undetermined reason. b/13678928
121         // no-op for now.
122         return msgBody;
123 
124 //        final StringBuilder sb = new StringBuilder("<div style=\"display: table-cell;\">");
125 //        sb.append(msgBody);
126 //        sb.append("</div>");
127 //        return sb.toString();
128     }
129 
appendMessageHtml(HtmlMessage message, boolean isExpanded, boolean safeForImages, int headerHeight, int footerHeight)130     public void appendMessageHtml(HtmlMessage message, boolean isExpanded,
131             boolean safeForImages, int headerHeight, int footerHeight) {
132 
133         final String bodyDisplay = isExpanded ? "block" : "none";
134         final String expandedClass = isExpanded ? "expanded" : "";
135         final String showImagesClass = safeForImages ? "mail-show-images" : "";
136 
137         String body = message.getBodyAsHtml();
138 
139         /* Work around a WebView bug (5522414) in setBlockNetworkImage that causes img onload event
140          * handlers to fire before an image is loaded.
141          * WebView will report bad dimensions when revealing inline images with absolute URLs, but
142          * we can prevent WebView from ever seeing those images by changing all img "src" attributes
143          * into "gm-src" before loading the HTML. Parsing the potentially dirty HTML input is
144          * prohibitively expensive with TagSoup, so use a little regular expression instead.
145          *
146          * To limit the scope of this workaround, only use it on messages that the server claims to
147          * have external resources, and even then, only use it on img tags where the src is absolute
148          * (i.e. url does not begin with "?"). The existing JavaScript implementation of this
149          * attribute swap will continue to handle inline image attachments (they have relative
150          * URLs) and any false negatives that the regex misses. This maintains overall security
151          * level by not relying solely on the regex.
152          */
153         if (!safeForImages && message.embedsExternalResources()) {
154             body = replaceAbsoluteImgUrls(body);
155         }
156 
157         append(sMessage,
158                 getMessageDomId(message),
159                 expandedClass,
160                 headerHeight,
161                 showImagesClass,
162                 bodyDisplay,
163                 wrapMessageBody(body),
164                 bodyDisplay,
165                 footerHeight
166         );
167     }
168 
getMessageDomId(HtmlMessage msg)169     public String getMessageDomId(HtmlMessage msg) {
170         return MESSAGE_PREFIX + msg.getId();
171     }
172 
getMessageIdForDomId(String domMessageId)173     public String getMessageIdForDomId(String domMessageId) {
174         return domMessageId.substring(MESSAGE_PREFIX_LENGTH);
175     }
176 
startConversation(int viewportWidth, int sideMargin, int conversationHeaderHeight)177     public void startConversation(int viewportWidth, int sideMargin, int conversationHeaderHeight) {
178         if (mInProgress) {
179             throw new IllegalStateException(
180                     "Should not call start conversation until end conversation has been called");
181         }
182 
183         reset();
184         final String border = Utils.isRunningKitkatOrLater() ?
185                 "img[blocked-src] { border: 1px solid #CCCCCC; }" : "";
186         append(sConversationUpper, viewportWidth, border, sideMargin, conversationHeaderHeight);
187         mInProgress = true;
188     }
189 
endConversation(int convFooterPx, String docBaseUri, String conversationBaseUri, int viewportWidth, int webviewWidth, boolean enableContentReadySignal, boolean normalizeMessageWidths, boolean enableMungeTables, boolean enableMungeImages)190     public String endConversation(int convFooterPx, String docBaseUri, String conversationBaseUri,
191             int viewportWidth, int webviewWidth, boolean enableContentReadySignal,
192             boolean normalizeMessageWidths, boolean enableMungeTables, boolean enableMungeImages) {
193         if (!mInProgress) {
194             throw new IllegalStateException("must call startConversation first");
195         }
196 
197         final String contentReadyClass = enableContentReadySignal ? "initial-load" : "";
198 
199         final boolean isRtl = TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault())
200                 == ViewCompat.LAYOUT_DIRECTION_RTL;
201         final String showElided = (isRtl ? RIGHT_TO_LEFT_TRIANGLE : LEFT_TO_RIGHT_TRIANGLE) +
202                 mContext.getString(R.string.show_elided);
203         append(sConversationLower, convFooterPx, contentReadyClass,
204                 mContext.getString(R.string.hide_elided),
205                 showElided, docBaseUri, conversationBaseUri, viewportWidth, webviewWidth,
206                 enableContentReadySignal, normalizeMessageWidths,
207                 enableMungeTables, enableMungeImages, Utils.isRunningKitkatOrLater(),
208                 mContext.getString(R.string.forms_are_disabled));
209 
210         mInProgress = false;
211 
212         LogUtils.d(TAG, "rendered conversation of %d bytes, buffer capacity=%d",
213                 mBuilder.length() << 1, mBuilder.capacity() << 1);
214 
215         return emit();
216     }
217 }
218