/** * Copyright 2010 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.waveprotocol.box.server.rpc.render.web.text; import com.google.common.collect.ImmutableSet; import com.google.inject.Inject; import com.google.inject.Singleton; import org.waveprotocol.box.server.rpc.render.ClientAction; import org.waveprotocol.box.server.rpc.render.web.template.ProfileStore; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; /** * A utility class that converts raw wave documents into html. * * @author dhanji@gmail.com (Dhanji R. Prasanna) */ @Singleton public class Markup { private static final DateFormat TIME_MILLIS_FORMATTER = new SimpleDateFormat("s.SSS"); private static final DateFormat TIME_FORMATTER = new SimpleDateFormat("h:mm a"); private static final DateFormat MONTH_DAY_FORMATTER = new SimpleDateFormat("MMM dd"); private static final DateFormat MONTH_DAY_YEAR_FORMATTER = new SimpleDateFormat("MMM dd, yyyy"); private static final DateFormat FULL_FORMATTER = new SimpleDateFormat("h:mm MMM dd, yyyy"); private final ProfileStore profileStore; @Inject Markup(ProfileStore profileStore) { this.profileStore = profileStore; } /** * @return the participant's display name. */ // TODO: This doesn't belong here. public String getDisplayName(String participantId) { return profileStore.getProfiles(ImmutableSet.of(participantId)).get(participantId).getName(); } /** * @return the participant's image url. */ // TODO: This doesn't belong here. public String getImageUrl(String participantId) { return profileStore.getProfiles( ImmutableSet.of(participantId)).get(participantId).getImageUrl(); } /** * Helpful utility for templates. */ public String sanitizeHtml(String text) { return Markup.sanitize(text); } /** * Formats a given timestamp into a friendly date string. The output will * look differently depending on the day and year. If the time given is the * same day as "now", then it will only display the time (12:30 PM). If it's * in the same year, then it will display the month and day (Jun 01) otherwise * it will return the month, day and year (Jun 01, 2009). * * @param timestamp * @return the formatted date time string. */ public static String formatDateTime(long timestamp) { Date date = new Date(timestamp); Date now = new Date(); return FULL_FORMATTER.format(date); } public static String formatMillis(long millis) { return TIME_MILLIS_FORMATTER.format(new Date(millis)) + "s"; } public static String toDomId(String id) { //HACK to make blip ids work as DOM ids return id.replace('+', '-'); } public static String toBlipId(String id) { //HACK to make blip ids work as DOM ids return id.replace('-', '+'); } public static ClientAction measure(String action, long searchTime) { return new ClientAction("measure") .html(action + " completed in " + Markup.formatMillis(searchTime)); } public static String embedSnippet(String waveId) { // TODO(dhanji): Move to template String domain = System.getProperty("DOMAIN"); String port = System.getProperty("PORT"); return "<script type=\"text/javascript\">" + " function load() {" + " var targetDiv = document.getElementById('waveframe');" + " var wavePanel = new google.wave.WavePanel({" + " rootUrl: \"http://" + domain + ":" + port + "/\"," + " target: targetDiv," + " lite: true" + " }).loadWave(\""+ waveId +"\");" + " }" + " </script>"; } /** * Sanitizes untrusted text so that it does not emit HTML markup. This utility * is based on a similar one in GWT's SafeHtml.java. It eliminates all predefined * entities in HTML/XHTML, replacing them with escape codes: * http://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references * * @param text The untrusted text to sanitize * @return Escaped text that can safely be rendered in a web page. */ public static String sanitize(String text) { StringBuilder out = new StringBuilder(text.length()); char[] chars = text.toCharArray(); for (char c : chars) { switch (c) { case '&': out.append("&"); break; case '\'': out.append("'"); break; case '"': out.append("""); break; case '<': out.append("<"); break; case '>': out.append(">"); break; default: // allow all other characters. out.append(c); } } return out.toString(); } /** * Checks if the scheme is one of four simple types (see #isSafeUri), if not * disallows the URI by reducing it to '#' * * @param uri An untrusted URI string to sanitize * @return Returns a safe URI. */ private static String sanitizeUri(String uri) { return isSafeUri(uri) ? uri : "#"; } private static String extractScheme(String uri) { if (null == uri) { return null; } int colonPos = uri.indexOf(':'); if (colonPos < 0) { return null; } String scheme = uri.substring(0, colonPos); if (scheme.indexOf('/') >= 0 || scheme.indexOf('#') >= 0) { // The URI's prefix up to the first ':' contains other URI special // chars, and won't be interpreted as a scheme. return null; } return scheme; } // TODO(dhanji): Should we allow more schemes? Like im: private static boolean isSafeUri(String uri) { String scheme = extractScheme(uri); return (scheme == null || "http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme) || "mailto".equalsIgnoreCase(scheme) || "ftp".equalsIgnoreCase(scheme)); } /** * Simply converts a lowerCamelCased symbol into a dashed one: * <pre> * fontWeight -> font-weight * </pre> * TODO: perhaps we should intern all these strings to save memory * and remove the overhead of string allocation, they could also * be perfectly hashed. */ static String toDashedStyle(String name) { char[] nameChars = name.toCharArray(); // Pre allocate buffer, +1 for '-' (2-dashes are uncommon) StringBuilder builder = new StringBuilder(nameChars.length + 1); for (char nameChar : nameChars) { if (Character.isUpperCase(nameChar)) { builder.append('-'); builder.append(Character.toLowerCase(nameChar)); continue; } builder.append(nameChar); } return builder.toString(); } /** * Checks if an image URL is safe (i.e. hosted on Google) * @param imageUrl A string URL * @return True if this image URL is safe to use in a google-hosted page. */ public static boolean isTrustedImageUrl(String imageUrl) { String scheme = extractScheme(imageUrl); // NOTE(dhanji): the trailing slash is extremely important. return (scheme != null) && (imageUrl.startsWith(scheme + "://www.google.com/") || imageUrl.startsWith(scheme + "://google.com/")); } /** * First sanitizes the given URI and then encodes it using the UTF-8 character * set. * @param uri An untrusted URI string * @return Sanitized, URL-encoded URI for embedding in HTML */ static String sanitizeAndEncode(String uri) { try { return URLEncoder.encode(sanitizeUri(uri), "UTF-8"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } }