// @@@LICENSE // // Copyright (c) 2010-2012 Hewlett-Packard Development Company, L.P. // // 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. // // LICENSE@@@ enyo.kind({ name: "AbstractHtmlView", kind: "Control", events: { onViewReady: "", onViewSizeChange: "", onLoadFailed: "", onLinkClick: "", onInspectElement: "" }, published: { cidMap: {}, allowRemoteContent: true }, create: function () { this.inherited(arguments); }, // Returns true if the HtmlView is more or less ready and has content displayed // The accuracy of this method may depend on the implementation. isReady: function () { return false; }, activate: function () { }, deactivate: function () { } }); enyo.kind({ name: "WebviewHtmlView", kind: "AbstractHtmlView", create: function () { this.inherited(arguments); this.webviewBodyLoaded = false; }, destroy: function () { // FIXME workaround for bug in webview try { this.inherited(arguments); } catch (e) { this.error(e); } }, createWebview: function () { this.createComponent({ name: "webview", kind: "WebView", fitRender: false, acceptCookies: false, enableJavascript: false, ignoreMetaTags: true, onSingleTap: "webviewTap", onUrlRedirected: "doLinkClick", onLoadStarted: "webviewLoadStarted", onLoadComplete: "webviewLoadComplete", onLoadStopped: "webviewLoadStopped" }); this.$.webview.render(); }, initWebview: function () { if (!this.$.webview) { this.createWebview(); this.setupWebview(); } }, setupWebview: function () { if (this._webviewReady) { return; } this.$.webview.setRedirects([ {regex: "^file:.*", cookie: "", enable: false}, {regex: ".*", cookie: "", enable: true} ]); // Hack to prevent double url loads var origUrlChanged = viewControl.urlChanged.bind(viewControl); viewControl.urlChanged = function (old) { if (viewControl.url !== viewControl._pendingUrl) { console.log("calling orig"); viewControl._pendingUrl = viewControl.url; origUrlChanged(old); } }; var origLoadStarted = viewControl.loadStarted.bind(viewControl); viewControl.loadStarted = function () { viewControl._pendingUrl = undefined; origLoadStarted(); }; this._webviewReady = true; }, // [public] loadUrl: function (url) { this.initWebview(); this.$.webview.setUrl(url); }, // [public] activate: function () { this.initWebview(); this.log("activating webview"); this.$.webview.activate(); }, // [public] deactivate: function () { if (this.$.webview) { this.$.webview.activate(); } }, webviewLoadStarted: function () { console.log("load started"); this.webviewBodyLoaded = false; }, webviewLoadComplete: function () { this.loadTimer = window.setTimeout(this.webviewReady.bind(this), 1000); }, webviewLoadStopped: function () { console.log("load stopped"); this.webviewBodyLoaded = true; this.webviewReady(); }, webviewReady: function () { if (this.loadTimer) { window.clearTimeout(this.loadTimer); } this.doViewReady(); }, webviewTap: function () { // TODO return true; } }); enyo.kind({ name: "InlineHtmlView", kind: "AbstractHtmlView", CID_REGEX: /^cid:/i, CSS_RULE_REGEX: /\s*([\-\w]+)\s*:\s*([^:;]*)?(;|$)/g, CSS_UNSAFE_PROPERTY_REGEX: /(^-\w+)|behavior$/i, CSS_URL_NOTATION_REGEX: /url\s*\(((?:\\.|.)*?)\)/i, VALID_RESOURCE_URL_REGEX: /^https?:/i, VALID_LINK_URL_REGEX: /^(https?|mailto):/i, create: function () { this.inherited(arguments); }, destroy: function () { this.inherited(arguments); }, // [public] loadUrl: function (url, contentType) { if (!url) { this.log("no url provided"); return; } this.url = url; this.contentType = contentType || "text/plain"; if (window.palmGetResource) { enyo.asyncMethod(this, enyo.bind(this, "loadDirect")); } else { enyo.xhrGet({ url: url, load: enyo.bind(this, "handleXhrResponse") }); } }, // load using webOS's fast built-in file loader loadDirect: function () { var url = this.url.replace("file://", ""); var data = palmGetResource(url); if (data) { this.loadDone(this.sanitizeResponse(data, this.contentType)); } else { this.doLoadFailed(); } }, loadXhr: function () { enyo.xhrGet({ url: this.url, load: enyo.bind(this, "handleXhrResponse") }); }, handleXhrResponse: function (responseText, xhrObject) { if(xhrObject.status == 200 || xhrObject.status == 304) { this.loadDone(this.sanitizeResponse(data, this.contentType)); } else { this.doLoadFailed(); } }, // TODO: make this take a callback since both loading and sanitizing may be asynchronous later sanitizeResponse: function (unsafeData, contentType) { // This will get set to true if checkAllowExternalResource blocks a url this.remoteContentWasBlocked = false; var isHtml = (contentType.toLowerCase() == "text/html"); // FIXME currently very slow for some emails var cleanHtml = isHtml ? this.sanitizeHtml(unsafeData) : this.sanitizeText(unsafeData); return cleanHtml; }, // Returns true if a remote content url (e.g. external image) was blocked from being displayed wasRemoteContentBlocked: function () { return this.remoteContentWasBlocked; }, // Check whether a url can be loaded in an image tag or css style checkAllowExternalResource: function (url) { if (!this.allowRemoteContent) { this.remoteContentWasBlocked = true; return false; } return this.VALID_RESOURCE_URL_REGEX.test(url); }, // Check whether a link can be referenced checkAllowLink: function (url) { return this.VALID_LINK_URL_REGEX.test(url); }, // Check if this external url is allowed mapUrl: function (url, tagName, attribName) { if (this.CID_REGEX.test(url)) { // replace cid: reference with local file path return this.cidMap[url.substr(4)] || ""; } if (tagName === "a" && attribName === "href") { return this.checkAllowLink(url) ? url : null; } else if (tagName === "img" && attribName === "src") { return this.checkAllowExternalResource(url) ? url : null; } else { return null; } }, // Checks and possibly removes external urls in inline css styles // TODO: TEST mapInlineStyle: function (style) { /*jshint loopfunc:true */ var self = this; var cleanStyle = ""; // Extract css rules one-by-one to make sure there's nothing tricky var rule = new RegExp(this.CSS_RULE_REGEX); // copy for repeated matches var m; while (!!(m = rule.exec(style))) { var name = m[1]; var value = m[2]; if (!name || !value || this.CSS_UNSAFE_PROPERTY_REGEX.exec(name)) { // unsafe css property continue; } // Cleanup urls value = value.replace(self.CSS_URL_NOTATION_REGEX, function (s, match1) { return self.checkAllowExternalResource(match1) ? s : "url()"; }); // Append normalized css cleanStyle += name + ":" + value + ";"; } return cleanStyle; }, mapClasses: function(classes) { if (classes === "gmail_quote" || classes === "webos_quote") { return classes; } else { return null; } }, sanitizeHtml: function (unsafeHtml) { var self = this; return SimpleHtmlParser.parseHtml(unsafeHtml, function (tag) { var tagInfo = SimpleHtmlParser.tagWhitelist[tag.tagName]; if (tagInfo) { var newTag = { tagName: tag.tagName, attributes: {}, selfClosing: tag.selfClosing }; for (var attrName in tag.attributes) { var attribInfo = tagInfo[attrName] || SimpleHtmlParser.attributeWhitelist[attrName]; if (attribInfo) { var value = tag.attributes[attrName]; if (attribInfo === "uri") { value = self.mapUrl(value, tag.tagName, attrName); } else if (attrName === "style") { value = self.mapInlineStyle(value); } else if (attrName === "class") { value = self.mapClasses(value); } newTag.attributes[attrName] = value; } else { //console.log("stripping attribute " + tag.tagName + "." + attrName); } } return newTag; } else { //console.log("stripping tag " + tag.tagName); if (SimpleHtmlParser.tagBlacklist[tag.tagName] || tag.selfClosing) { return null; } else { return {tagName: "div"}; } } }); }, sanitizeText: function (unsafeText) { return EmailApp.Util.convertTextToHtml(unsafeText); }, makeExpander: function (quoteDiv, onContentChange) { var expander = document.createElement("div"); expander.textContent = "[show quoted text]"; expander.className = "quote-expander"; quoteDiv.style.display = "none"; quoteDiv.className += " -palm-quoted-text"; expander.onclick = function() { if (quoteDiv.style.display === "none") { quoteDiv.style.display = "block"; expander.textContent = "[hide quoted text]"; } else { quoteDiv.style.display = "none"; expander.textContent = "[show quoted text]"; } onContentChange(); }; quoteDiv.parentNode.insertBefore(expander, quoteDiv); }, // check if the given node is inside of a quoted text block isWithinQuote: function (node, rootNode) { while (node !== rootNode) { if (node.className && node.className.indexOf("-palm-quoted-text") >= 0) { return true; } node = node.parentNode; } return false; }, collapseQuotes: function (rootNode, nodes, onContentChange) { for (var i = 0; i < nodes.length; i += 1) { var quoteDiv = nodes.item(i); // only block off large-ish quotes if (quoteDiv.offsetHeight < 100) { continue; } // make sure we're not nested if (!this.isWithinQuote(quoteDiv, rootNode)) { this.makeExpander(quoteDiv, onContentChange); } } }, quoteTextNodes: function (rootNode, textNodes, onContentChange) { // don't bother with short quotes if (textNodes.length < 4) { return; } var element = textNodes[0].parentNode; // make sure we're not nested if (this.isWithinQuote(element, rootNode)) { return; } //console.log("quote candidates: " + enyo.map(textNodes, function (n) {return n.data;}).join("")); var node = textNodes[0]; var index = 0; var nodesToExtract = []; // Walk through the nodes and confirm that there's only text/br nodes do { if (node.nodeType === 1) { // element node if (node.localName.toLowerCase() === "br") { nodesToExtract.push(node); } else { console.log("non-br element" + node.localName); return; } } else if (node.nodeType === 3) { // text node if (textNodes[index] === node) { // matched up node nodesToExtract.push(node); } else { console.log("unexpected text node: " + node.data); return; } index += 1; if (index === textNodes.length) { // done break; } } else { console.log("node is not text/br: " + node.nodeType); return; } node = node.nextSibling; } while (node); //console.log("got " + nodesToExtract.length + " nodes to extract"); // Move nodes to new div var quoteDiv = document.createElement("div"); element.insertBefore(quoteDiv, textNodes[0]); for (var i = 0; i < nodesToExtract.length; i += 1) { node = nodesToExtract[i]; quoteDiv.appendChild(node); } this.makeExpander(quoteDiv, onContentChange); }, /* * Scan for