/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (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.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Google Safe Browsing. * * The Initial Developer of the Original Code is Google Inc. * Portions created by the Initial Developer are Copyright (C) 2006 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Fritz Schneider (original author) * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ // Implementation of the warning message we show users when we // notice navigation to a phishing page after it has loaded. The // browser view encapsulates all the hide/show logic, so the displayer // doesn't need to know when to display itself, only how. // // Displayers implement the following interface: // // start() -- fired to initialize the displayer (to make it active). When // called, this displayer starts listening for and responding to // events. At most one displayer per tab should be active at a // time, and start() should be called at most once. // declineAction() -- fired when the user declines the warning. // acceptAction() -- fired when the user declines the warning // explicitShow() -- fired when the user wants to see the warning again // browserSelected() -- the browser is the top tab // browserUnselected() -- the browser is no long the top tab // done() -- clean up. May be called once (even if the displayer wasn't // activated). // // At the moment, all displayers share access to the same xul in // safebrowsing-overlay.xul. Hence the need for at most one displayer // to be active per tab at a time. /** * Factory that knows how to create a displayer appropriate to the * user's platform. We use a clunky canvas-based displayer for all * platforms until such time as we can overlay semi-transparent * areas over browser content. * * See the base object for a description of the constructor args * * @constructor */ function PROT_PhishMsgDisplayer(msgDesc, browser, doc, url) { // TODO: Change this to return a PhishMsgDisplayerTransp on windows // (and maybe other platforms) when Firefox 2.0 hits. return new PROT_PhishMsgDisplayerCanvas(msgDesc, browser, doc, url); } /** * Base class that implements most of the plumbing required to hide * and show a phishing warning. Subclasses implement the actual * showMessage and hideMessage methods. * * This class is not meant to be instantiated directly. * * @param msgDesc String describing the kind of warning this is * @param browser Reference to the browser over which we display the msg * @param doc Reference to the document in which browser is found * @param url String containing url of the problem document * @constructor */ function PROT_PhishMsgDisplayerBase(msgDesc, browser, doc, url) { this.debugZone = "phishdisplayer"; this.msgDesc_ = msgDesc; // currently unused this.browser_ = browser; this.doc_ = doc; this.url_ = url; // We'll need to manipulate the XUL in safebrowsing-overlay.xul this.messageId_ = "safebrowsing-palm-message"; this.messageTailId_ = "safebrowsing-palm-message-tail-container"; this.messageContentId_ = "safebrowsing-palm-message-content"; this.extendedMessageId_ = "safebrowsing-palm-extended-message"; this.showmoreLinkId_ = "safebrowsing-palm-showmore-link"; this.faqLinkId_ = "safebrowsing-palm-faq-link"; this.urlbarIconId_ = "safebrowsing-urlbar-icon"; this.refElementId_ = this.urlbarIconId_; // We use this to report user actions to the server this.reporter_ = new PROT_Reporter(); // The active UI elements in our warning send these commands; bind them // to their handlers but don't register the commands until we start // (because another displayer might be active) this.commandHandlers_ = { "safebrowsing-palm-showmore": BindToObject(this.showMore_, this), }; this.windowWatcher_ = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] .getService(Components.interfaces.nsIWindowWatcher); } /** * @returns The default background color of the browser */ PROT_PhishMsgDisplayerBase.prototype.getBackgroundColor_ = function() { var pref = Components.classes["@mozilla.org/preferences-service;1"]. getService(Components.interfaces.nsIPrefBranch); return pref.getCharPref("browser.display.background_color"); } /** * Fired when the user declines our warning. Report it! */ PROT_PhishMsgDisplayerBase.prototype.declineAction = function() { G_Debug(this, "User declined warning."); G_Assert(this, this.started_, "Decline on a non-active displayer?"); this.reporter_.report("phishdecline", this.url_); this.messageShouldShow_ = false; if (this.messageShowing_) this.hideMessage_(); } /** * Fired when the user accepts our warning */ PROT_PhishMsgDisplayerBase.prototype.acceptAction = function() { G_Assert(this, this.started_, "Accept on an unstarted displayer?"); G_Assert(this, this.done_, "Accept on a finished displayer?"); G_Debug(this, "User accepted warning."); this.reporter_.report("phishaccept", this.url_); var url = this.getMeOutOfHereUrl_(); this.browser_.loadURI(url); } /** * Get the url for "Get me out of here." This is the browser's default home * page, or, about:blank. * @return String url */ PROT_PhishMsgDisplayerBase.prototype.getMeOutOfHereUrl_ = function() { // Try to get their homepage from prefs. var prefs = Cc["@mozilla.org/preferences-service;1"] .getService(Ci.nsIPrefService).getDefaultBranch(null); var url = "about:blank"; try { url = prefs.getComplexValue("browser.startup.homepage", Ci.nsIPrefLocalizedString).data; } catch(e) { G_Debug(this, "Couldn't get homepage pref: " + e); } return url; } /** * Invoked when the browser is resized */ PROT_PhishMsgDisplayerBase.prototype.onBrowserResized_ = function(event) { G_Debug(this, "Got resize for " + event.target); if (this.messageShowing_) { this.hideMessage_(); this.showMessage_(); } } /** * Invoked by the browser view when our browser is switched to */ PROT_PhishMsgDisplayerBase.prototype.browserSelected = function() { G_Assert(this, this.started_, "Displayer selected before being started???"); // If messageshowing hasn't been set, then this is the first time this // problematic browser tab has been on top, so do our setup and show // the warning. if (this.messageShowing_ === undefined) { this.messageShouldShow_ = true; } this.hideLockIcon_(); // Comes back when we are unselected or unloaded this.addWarningInUrlbar_(); // Goes away when we are unselected or unloaded // messageShouldShow might be false if the user dismissed the warning, // switched tabs, and then switched back. We're still active, but don't // want to show the warning again. The user can cause it to show by // clicking our icon in the urlbar. if (this.messageShouldShow_) this.showMessage_(); } /** * Invoked to display the warning message explicitly, for example if the user * clicked the url warning icon. */ PROT_PhishMsgDisplayerBase.prototype.explicitShow = function() { this.messageShouldShow_ = true; if (!this.messageShowing_) this.showMessage_(); } /** * Invoked by the browser view when our browser is switched away from */ PROT_PhishMsgDisplayerBase.prototype.browserUnselected = function() { this.removeWarningInUrlbar_(); this.unhideLockIcon_(); if (this.messageShowing_) this.hideMessage_(); } /** * Invoked to make this displayer active. The displayer will now start * responding to notifications such as commands and resize events. We * can't do this in the constructor because there might be many * displayers instantiated waiting in the problem queue for a particular * browser (e.g., a page has multiple framed problem pages), and we * don't want them all responding to commands! * * Invoked zero (the page we're a warning for was nav'd away from * before it reaches the head of the problem queue) or one (we're * displaying this warning) times by the browser view. */ PROT_PhishMsgDisplayerBase.prototype.start = function() { G_Assert(this, this.started_ == undefined, "Displayer started twice?"); this.started_ = true; this.commandController_ = new PROT_CommandController(this.commandHandlers_); this.doc_.defaultView.controllers.appendController(this.commandController_); // Add an event listener for when the browser resizes (e.g., user // shows/hides the sidebar). this.resizeHandler_ = BindToObject(this.onBrowserResized_, this); this.browser_.addEventListener("resize", this.resizeHandler_, false); } /** * @returns Boolean indicating whether this displayer is currently * active */ PROT_PhishMsgDisplayerBase.prototype.isActive = function() { return !!this.started_; } /** * Invoked by the browser view to clean up after the user is done * interacting with the message. Should be called once by the browser * view. */ PROT_PhishMsgDisplayerBase.prototype.done = function() { G_Assert(this, !this.done_, "Called done more than once?"); this.done_ = true; // If the Document we're showing the warning for was nav'd away from // before we had a chance to get started, we have nothing to do. if (this.started_) { // If we were started, we must be the current problem, so these things // must be showing this.removeWarningInUrlbar_(); this.unhideLockIcon_(); // Could be though that they've closed the warning dialog if (this.messageShowing_) this.hideMessage_(); if (this.resizeHandler_) { this.browser_.removeEventListener("resize", this.resizeHandler_, false); this.resizeHandler_ = null; } var win = this.doc_.defaultView; win.controllers.removeController(this.commandController_); this.commandController_ = null; } } /** * Helper function to remove a substring from inside a string. * * @param orig String to remove substring from * * @param toRemove String to remove (if it is present) * * @returns String with the substring removed */ PROT_PhishMsgDisplayerBase.prototype.removeIfExists_ = function(orig, toRemove) { var pos = orig.indexOf(toRemove); if (pos != -1) orig = orig.substring(0, pos) + orig.substring(pos + toRemove.length); return orig; } /** * We don't want to confuse users if they land on a phishy page that uses * SSL, so ensure that the lock icon never shows when we're showing our * warning. */ PROT_PhishMsgDisplayerBase.prototype.hideLockIcon_ = function() { var lockIcon = this.doc_.getElementById("lock-icon"); if (!lockIcon) return; lockIcon.hidden = true; } /** * Ensure they can see it after our warning is finished. */ PROT_PhishMsgDisplayerBase.prototype.unhideLockIcon_ = function() { var lockIcon = this.doc_.getElementById("lock-icon"); if (!lockIcon) return; lockIcon.hidden = false; } /** * This method makes our warning icon visible in the location bar. It will * be removed only when the problematic document is navigated awy from * (i.e., when done() is called), and not when the warning is dismissed. */ PROT_PhishMsgDisplayerBase.prototype.addWarningInUrlbar_ = function() { var urlbarIcon = this.doc_.getElementById(this.urlbarIconId_); if (!urlbarIcon) return; urlbarIcon.setAttribute('level', 'warn'); } /** * Hides our urlbar icon */ PROT_PhishMsgDisplayerBase.prototype.removeWarningInUrlbar_ = function() { var urlbarIcon = this.doc_.getElementById(this.urlbarIconId_); if (!urlbarIcon) return; urlbarIcon.setAttribute('level', 'safe'); } /** * VIRTUAL -- Displays the warning message */ PROT_PhishMsgDisplayerBase.prototype.showMessage_ = function() { }; /** * VIRTUAL -- Hide the warning message from the user. */ PROT_PhishMsgDisplayerBase.prototype.hideMessage_ = function() { }; /** * Reposition the message relative to refElement in the parent window * * @param message Reference to the element to position * @param tail Reference to the message tail * @param refElement Reference to element relative to which we position * ourselves */ PROT_PhishMsgDisplayerBase.prototype.adjustLocation_ = function(message, tail, refElement) { var refX = refElement.boxObject.x; var refY = refElement.boxObject.y; var refHeight = refElement.boxObject.height; var refWidth = refElement.boxObject.width; G_Debug(this, "Ref element is at [window-relative] (" + refX + ", " + refY + ")"); var pixelsIntoRefY = -2; var tailY = refY + refHeight - pixelsIntoRefY; var tailPixelsLeftOfRefX = tail.boxObject.width; var tailPixelsIntoRefX = Math.round(refWidth / 2); var tailX = refX - tailPixelsLeftOfRefX + tailPixelsIntoRefX; // Move message up a couple pixels so the tail overlaps it. var messageY = tailY + tail.boxObject.height - 2; var messagePixelsLeftOfRefX = 375; var messageX = refX - messagePixelsLeftOfRefX; G_Debug(this, "Message is at [window-relative] (" + messageX + ", " + messageY + ")"); G_Debug(this, "Tail is at [window-relative] (" + tailX + ", " + tailY + ")"); if (messageX < 0) { // We're hanging off the left edge, switch to floating mode tail.style.display = "none"; this.adjustLocationFloating_(message); return; } tail.style.top = tailY + "px"; tail.style.left = tailX + "px"; message.style.top = messageY + "px"; message.style.left = messageX + "px"; this.maybeAddScrollbars_(); } /** * Position the warning bubble with no reference element. In this case we * just center the warning bubble at the top of the users window. * @param message XULElement message bubble XUL container. */ PROT_PhishMsgDisplayerBase.prototype.adjustLocationFloating_ = function(message) { // Compute X offset var browserX = this.browser_.boxObject.x; var browserXCenter = browserX + this.browser_.boxObject.width / 2; var messageX = browserXCenter - (message.boxObject.width / 2); // Compute Y offset (top of the browser window) var messageY = this.browser_.boxObject.y; // Position message message.style.top = messageY + "px"; message.style.left = messageX + "px"; this.maybeAddScrollbars_(); } /** * Add a vertical scrollbar if we're falling out of the browser window. */ PROT_PhishMsgDisplayerBase.prototype.maybeAddScrollbars_ = function() { var message = this.doc_.getElementById(this.messageId_); var content = this.doc_.getElementById(this.messageContentId_); var bottom = content.boxObject.y + content.boxObject.height; var maxY = this.doc_.defaultView.innerHeight; G_Debug(this, "bottom: " + bottom + ", maxY: " + maxY + ", new height: " + (maxY - content.boxObject.y)); if (bottom > maxY) { var newHeight = maxY - content.boxObject.y; if (newHeight < 1) newHeight = 1; content.style.height = newHeight + "px"; content.style.overflow = "auto"; } } /** * Show the extended warning message */ PROT_PhishMsgDisplayerBase.prototype.showMore_ = function() { this.doc_.getElementById(this.extendedMessageId_).hidden = false; this.doc_.getElementById(this.showmoreLinkId_).style.display = "none"; // set FAQ URL var formatter = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"] .getService(Components.interfaces.nsIURLFormatter); var faqURL = formatter.formatURLPref("browser.safebrowsing.warning.infoURL"); var labelEl = this.doc_.getElementById(this.faqLinkId_); labelEl.setAttribute("href", faqURL); this.maybeAddScrollbars_(); } /** * The user clicked on one of the links in the buble. Display the * corresponding page in a new window with all the chrome enabled. * * @param url The URL to display in a new window */ PROT_PhishMsgDisplayerBase.prototype.showURL_ = function(url) { this.windowWatcher_.openWindow(this.windowWatcher_.activeWindow, url, "_blank", null, null); } /** * If the warning bubble came up in error, this url goes to a form * to notify the data provider. * @return url String */ PROT_PhishMsgDisplayerBase.prototype.getReportErrorURL_ = function() { var badUrl = this.url_; var url = gDataProvider.getReportErrorURL(); url += "&url=" + encodeURIComponent(badUrl); return url; } /** * URL for the user to report back to us. This is to provide the user * with an action after being warned. */ PROT_PhishMsgDisplayerBase.prototype.getReportGenericURL_ = function() { var badUrl = this.url_; var url = gDataProvider.getReportGenericURL(); url += "&url=" + encodeURIComponent(badUrl); return url; } /** * A specific implementation of the dislpayer using a canvas. This * class is meant for use on platforms that don't support transparent * elements over browser content (currently: all platforms). * * The main ugliness is the fact that we're hiding the content area and * painting the page to canvas. As a result, we must periodically * re-paint the canvas to reflect updates to the page. Otherwise if * the page was half-loaded when we showed our warning, it would * stay that way even though the page actually finished loading. * * See base constructor for full details of constructor args. * * @constructor */ function PROT_PhishMsgDisplayerCanvas(msgDesc, browser, doc, url) { PROT_PhishMsgDisplayerBase.call(this, msgDesc, browser, doc, url); this.dimAreaId_ = "safebrowsing-dim-area-canvas"; this.pageCanvasId_ = "safebrowsing-page-canvas"; this.xhtmlNS_ = "http://www.w3.org/1999/xhtml"; // we create html:canvas } PROT_PhishMsgDisplayerCanvas.inherits(PROT_PhishMsgDisplayerBase); /** * Displays the warning message. First we make sure the overlay is loaded * then call showMessageAfterOverlay_. */ PROT_PhishMsgDisplayerCanvas.prototype.showMessage_ = function() { G_Debug(this, "Showing message."); // Load the overlay if we haven't already. var dimmer = this.doc_.getElementById('safebrowsing-dim-area-canvas'); if (!dimmer) { var onOverlayMerged = BindToObject(this.showMessageAfterOverlay_, this); var observer = new G_ObserverWrapper("xul-overlay-merged", onOverlayMerged); this.doc_.loadOverlay( "chrome://browser/content/safebrowsing/warning-overlay.xul", observer); } else { // The overlay is already loaded so we go ahead and call // showMessageAfterOverlay_. this.showMessageAfterOverlay_(); } } /** * This does the actual work of showing the warning message. */ PROT_PhishMsgDisplayerCanvas.prototype.showMessageAfterOverlay_ = function() { this.messageShowing_ = true; // Position the canvas overlay. Order here is significant, but don't ask me // why for some of these. You need to: // 1. get browser dimensions // 2. add canvas to the document // 3. unhide the dimmer (gray out overlay) // 4. display to the canvas // 5. unhide the warning message // 6. update link targets in warning message // 7. focus the warning message // (1) var w = this.browser_.boxObject.width; G_Debug(this, "browser w=" + w); var h = this.browser_.boxObject.height; G_Debug(this, "browser h=" + h); var x = this.browser_.boxObject.x; G_Debug(this, "browser x=" + w); var y = this.browser_.boxObject.y; G_Debug(this, "browser y=" + h); var win = this.browser_.contentWindow; var scrollX = win.scrollX; G_Debug(this, "win scrollx=" + scrollX); var scrollY = win.scrollY; G_Debug(this, "win scrolly=" + scrollY); // (2) // We add the canvas dynamically and remove it when we're done because // leaving it hanging around consumes a lot of memory. var pageCanvas = this.doc_.createElementNS(this.xhtmlNS_, "html:canvas"); pageCanvas.id = this.pageCanvasId_; pageCanvas.style.left = x + 'px'; pageCanvas.style.top = y + 'px'; var dimarea = this.doc_.getElementById(this.dimAreaId_); this.doc_.getElementById('main-window').insertBefore(pageCanvas, dimarea); // (3) dimarea.style.left = x + 'px'; dimarea.style.top = y + 'px'; dimarea.style.width = w + 'px'; dimarea.style.height = h + 'px'; dimarea.hidden = false; // (4) pageCanvas.setAttribute("width", w); pageCanvas.setAttribute("height", h); var bgcolor = this.getBackgroundColor_(); var cx = pageCanvas.getContext("2d"); cx.drawWindow(win, scrollX, scrollY, w, h, bgcolor); // Now repaint the window every so often in case the content hasn't fully // loaded at this point. var debZone = this.debugZone; function repaint() { G_Debug(debZone, "Repainting canvas..."); cx.drawWindow(win, scrollX, scrollY, w, h, bgcolor); }; this.repainter_ = new PROT_PhishMsgCanvasRepainter(repaint); // (5) this.showAndPositionWarning_(); // (6) var link = this.doc_.getElementById('safebrowsing-palm-falsepositive-link'); link.href = this.getReportErrorURL_(); // (7) this.doc_.getElementById(this.messageContentId_).focus(); } /** * Show and position the warning message. We position the waring message * relative to the icon in the url bar, but if the element doesn't exist, * (e.g., the user remove the url bar from her/his chrome), we anchor at the * top of the window. */ PROT_PhishMsgDisplayerCanvas.prototype.showAndPositionWarning_ = function() { var refElement = this.doc_.getElementById(this.refElementId_); var message = this.doc_.getElementById(this.messageId_); var tail = this.doc_.getElementById(this.messageTailId_); message.hidden = false; message.style.display = "block"; // Determine if the refElement is visible. if (this.isVisibleElement_(refElement)) { // Show tail and position warning relative to refElement. tail.hidden = false; tail.style.display = "block"; this.adjustLocation_(message, tail, refElement); } else { // No ref element, position in the top center of window. tail.hidden = true; tail.style.display = "none"; this.adjustLocationFloating_(message); } } /** * @return Boolean true if elt is a visible XUL element. */ PROT_PhishMsgDisplayerCanvas.prototype.isVisibleElement_ = function(elt) { if (!elt) return false; // If it's on a collapsed/hidden toolbar, the x position is set to 0. if (elt.boxObject.x == 0) return false; return true; } /** * Hide the warning message from the user. */ PROT_PhishMsgDisplayerCanvas.prototype.hideMessage_ = function() { G_Debug(this, "Hiding phishing warning."); G_Assert(this, this.messageShowing_, "Hide message called but not showing?"); this.messageShowing_ = false; this.repainter_.cancel(); this.repainter_ = null; // Hide the warning popup. var message = this.doc_.getElementById(this.messageId_); message.hidden = true; message.style.display = "none"; var content = this.doc_.getElementById(this.messageContentId_); content.style.height = ""; content.style.overflow = ""; var tail = this.doc_.getElementById(this.messageTailId_); tail.hidden = true; tail.style.display = "none"; // Remove the canvas element from the chrome document. var pageCanvas = this.doc_.getElementById(this.pageCanvasId_); pageCanvas.parentNode.removeChild(pageCanvas); // Hide the dimmer. var dimarea = this.doc_.getElementById(this.dimAreaId_); dimarea.hidden = true; } /** * Helper class that periodically repaints the canvas. We repaint * frequently at first, and then back off to a less frequent schedule * at "steady state," and finally just stop altogether. We have to do * this because we're not sure if the page has finished loading when * we first paint the canvas, and because we want to reflect any * dynamically written content into the canvas as it appears in the * page after load. * * @param repaintFunc Function to call to repaint browser. * * @constructor */ function PROT_PhishMsgCanvasRepainter(repaintFunc) { this.count_ = 0; this.repaintFunc_ = repaintFunc; this.initPeriodMS_ = 500; // Initially repaint every 500ms this.steadyStateAtMS_ = 10 * 1000; // Go slowly after 10 seconds, this.steadyStatePeriodMS_ = 3 * 1000; // repainting every 3 seconds, and this.quitAtMS_ = 20 * 1000; // stop after 20 seconds this.startMS_ = (new Date).getTime(); this.alarm_ = new G_Alarm(BindToObject(this.repaint, this), this.initPeriodMS_); } /** * Called periodically to repaint the canvas */ PROT_PhishMsgCanvasRepainter.prototype.repaint = function() { this.repaintFunc_(); var nextRepaint; // If we're in "steady state", use the slow repaint rate, else fast if ((new Date).getTime() - this.startMS_ > this.steadyStateAtMS_) nextRepaint = this.steadyStatePeriodMS_; else nextRepaint = this.initPeriodMS_; if (!((new Date).getTime() - this.startMS_ > this.quitAtMS_)) this.alarm_ = new G_Alarm(BindToObject(this.repaint, this), nextRepaint); } /** * Called to stop repainting the canvas */ PROT_PhishMsgCanvasRepainter.prototype.cancel = function() { if (this.alarm_) { this.alarm_.cancel(); this.alarm_ = null; } this.repaintFunc_ = null; }