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
18package com.android.mail.ui;
19
20import android.content.Context;
21import android.support.v4.text.TextUtilsCompat;
22import android.support.v4.view.ViewCompat;
23
24import com.android.mail.R;
25import com.android.mail.utils.LogTag;
26import com.android.mail.utils.LogUtils;
27import com.android.mail.utils.Utils;
28import com.google.common.annotations.VisibleForTesting;
29
30import java.util.Locale;
31import java.util.regex.Pattern;
32
33/**
34 * Renders data into very simple string-substitution HTML templates for conversation view.
35 */
36public 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
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
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
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     */
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
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
169    public String getMessageDomId(HtmlMessage msg) {
170        return MESSAGE_PREFIX + msg.getId();
171    }
172
173    public String getMessageIdForDomId(String domMessageId) {
174        return domMessageId.substring(MESSAGE_PREFIX_LENGTH);
175    }
176
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
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
209        mInProgress = false;
210
211        LogUtils.d(TAG, "rendered conversation of %d bytes, buffer capacity=%d",
212                mBuilder.length() << 1, mBuilder.capacity() << 1);
213
214        return emit();
215    }
216}
217