Home | History | Annotate | Download | only in ui
      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 
     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                 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