/* ***** 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 ***** */ // There is one BrowserView per browser window, and each BrowserView // is responsible for keeping track of problems (phishy documents) // within that window. The BrowserView is also responsible for // figuring out what to do about such problems, for example, whether // the tab with a phishy page is currently showing and therefore if we // should be showing a warning. // // The BrowserView receives information from three places: // // - from the phishing warden. When the phishing warden notices a // problem, it queries all browser views to see which one (if any) // has the Document that is problematic. It then hands the problem // off to the appropriate BrowserView. // // - from the controller. The controller responds to explicit user // actions (tab switches, requests to hide the warning message, // etc.) and let's the BrowserView know about any user action // having to do with the problems it is tracking. // // - from the TabbedBrowserWatcher. When the BrowserView is keeping // track of a problematic document it listens for interesting // events affecting it, for example pagehide (at which point // we presumably hide the warning if we're showing it). // // The BrowserView associates at most one "problem" with each Document // in the browser window. It keeps state about which Documents are // problematic by storing a "problem queue" on each browser (tab). // At most one problematic document per browser (tab) is active // at any time. That is, we show the warning for at most one phishy // document at any one time. If another phishy doc loads in that tab, // it goes onto the end of the queue to be activated only when the // currently active document goes away. // // If we had multiple types of warnings (one for after the page had // loaded, one for when the user clicked a link, etc) here's where // we'd select the appropate one to use. As it stands, we only have // one displayer (an "afterload" displayer). A displayer knows _how_ // to display a warning, whereas as the BrowserView knows _what_ and // _when_. // // To keep things (relatively) easy to reason about and efficient (the // phishwarden could be querying us inside a progresslistener // notification, or the controller inside an event handler), we have // the following rules: // // - at most one of a displayer's start() or stop() methods is called // in any iteration (if calling two is required, the second is run in // the next event loop) // - displayers should run their operations synchronously so we don't have // to look two places (here and in the displayer) to see what is happening // when // - displayer actions are run after cleaning up the browser view state // in case they have consequences // // TODO: this could use some redesign, but I don't have time. // TODO: the queue needs to be abstracted, but we want another release fast, // so I'm not going to touch it for the time being // TODO: IDN issues and canonical URLs? // TODO: Perhaps we should blur the page before showing a warning in order // to prevent stray keystrokes? /** * The BrowerView is responsible for keeping track of and figuring out * what to do with problems within a single browser window. * * TODO * Unify all browser-related state here. Currently it's split * between two objects, this object and the controller. We could have * this object be solely responsible for UI hide/show decisions, which * would probably make it easier to reason about what's going on. * * TODO * Investigate an alternative model. For example, we could factor out * the problem signaling stuff from the tab/UI logic into a * ProblemRegistry. Attach listeners to new docs/requests as they go * by and have these listeners periodically check in with a * ProblemRegistry to see if they're watching a problematic * doc/request. If so, then have them flag the browser view to be * aware of the problem. * * @constructor * @param tabWatcher Reference to the TabbedBrowserWatcher we'll use to query * for information about active tabs/browsers. * @param doc Reference to the XUL Document (browser window) in which the * tabwatcher is watching */ function PROT_BrowserView(tabWatcher, doc) { this.debugZone = "browserview"; this.tabWatcher_ = tabWatcher; this.doc_ = doc; } /** * See if we have any Documents with a given (problematic) URL that * haven't yet been marked as problems. Called as a subroutine by * tryToHandleProblemRequest(). * * @param url String containing the URL to look for * * @returns Reference to an unhandled Document with the problem URL or null */ PROT_BrowserView.prototype.getFirstUnhandledDocWithURL_ = function(url) { var docs = this.tabWatcher_.getDocumentsFromURL(url); if (!docs.length) return null; for (var i = 0; i < docs.length; i++) { // We only care about top level documents (i.e., we don't care about // frames). if (docs[i].defaultView.top != docs[i].defaultView) continue; var browser = this.tabWatcher_.getBrowserFromDocument(docs[i]); G_Assert(this, !!browser, "Found doc but can't find browser???"); var alreadyHandled = this.getProblem_(docs[i], browser); if (!alreadyHandled) return docs[i]; } return null; } /** * Invoked by the warden to give us the opportunity to handle a * problem. A problem is signaled once per request for a problem * Document and is handled at most once, so there's no issue with us * "losing" a problem due to multiple concurrently loading Documents * with the same URL. * * @param warden Reference to the warden signalling the problem. We'll * need him to instantiate one of his warning displayers * * @param request The nsIRequest that is problematic * * @returns Boolean indicating whether we handled problem */ PROT_BrowserView.prototype.tryToHandleProblemRequest = function(warden, request) { var doc = this.getFirstUnhandledDocWithURL_(request.name); if (doc) { var browser = this.tabWatcher_.getBrowserFromDocument(doc); G_Assert(this, !!browser, "Couldn't get browser from problem doc???"); G_Assert(this, !this.getProblem_(doc, browser), "Doc is supposedly unhandled, but has state?"); this.isProblemDocument_(browser, doc, warden); return true; } return false; } /** * We're sure a particular Document is problematic, so let's instantiate * a dispalyer for it and add it to the problem queue for the browser. * * @param browser Reference to the browser in which the problem doc resides * * @param doc Reference to the problematic document * * @param warden Reference to the warden signalling the problem. */ PROT_BrowserView.prototype.isProblemDocument_ = function(browser, doc, warden) { G_Debug(this, "Document is problem: " + doc.location.href); var url = doc.location.href; // We only have one type of displayer right now var displayer = new warden.displayers_["afterload"]("Phishing afterload", browser, this.doc_, url); // We listen for the problematic document being navigated away from // so we can remove it from the problem queue var hideHandler = BindToObject(this.onNavAwayFromProblem_, this, doc, browser); doc.defaultView.addEventListener("pagehide", hideHandler, true); // More info than we technically need, but it comes in handy for debugging var problem = { "browser_": browser, "doc_": doc, "displayer_": displayer, "url_": url, "hideHandler_": hideHandler, }; var numInQueue = this.queueProblem_(browser, problem); // If the queue was empty, schedule us to take something out if (numInQueue == 1) new G_Alarm(BindToObject(this.unqueueNextProblem_, this, browser), 0); } /** * Invoked when a problematic document is navigated away from. * * @param doc Reference to the problematic Document navigated away from * @param browser Reference to the browser in which the problem document * unloaded */ PROT_BrowserView.prototype.onNavAwayFromProblem_ = function(doc, browser) { G_Debug(this, "User nav'd away from problem."); var problem = this.getProblem_(doc, browser); (new PROT_Reporter).report("phishnavaway", problem.url_); G_Assert(this, doc === problem.doc_, "State doc not equal to nav away doc?"); G_Assert(this, browser === problem.browser_, "State browser not equal to nav away browser?"); this.problemResolved(browser, doc); } /** * @param browser Reference to a browser we'd like to know about * * @returns Boolean indicating if the browser in question has * problematic content */ PROT_BrowserView.prototype.hasProblem = function(browser) { return this.hasNonemptyProblemQueue_(browser); } /** * @param browser Reference to a browser we'd like to know about * * @returns Boolean indicating if the browser in question has a * problem (i.e., it has a non-empty problem queue) */ PROT_BrowserView.prototype.hasNonemptyProblemQueue_ = function(browser) { try { return !!browser.PROT_problemState__ && !!browser.PROT_problemState__.length; } catch(e) { // We could be checking a browser that has just been closed, in // which case its properties will not be valid, causing the above // statement to throw an error. Since this case handled elsewhere, // just return false. return false; } } /** * Invoked to indicate that the problem for a particular problematic * document in a browser has been resolved (e.g., by being navigated * away from). * * @param browser Reference to the browser in which resolution is happening * * @param opt_doc Reference to the problematic doc whose problem was resolved * (if absent, assumes the doc assocaited with the currently * active displayer) */ PROT_BrowserView.prototype.problemResolved = function(browser, opt_doc) { var problem; var doc; if (!!opt_doc) { doc = opt_doc; problem = this.getProblem_(doc, browser); } else { problem = this.getCurrentProblem_(browser); doc = problem.doc_; } problem.displayer_.done(); var wasHead = this.deleteProblemFromQueue_(doc, browser); // Peek at the next problem (if any) in the queue for this browser var queueNotEmpty = this.getCurrentProblem_(browser); if (wasHead && queueNotEmpty) { G_Debug(this, "More problems pending. Scheduling unqueue."); new G_Alarm(BindToObject(this.unqueueNextProblem_, this, browser), 0); } } /** * Peek at the top of the problem queue and if there's something there, * make it active. * * @param browser Reference to the browser we should activate a problem * displayer in if one is available */ PROT_BrowserView.prototype.unqueueNextProblem_ = function(browser) { var problem = this.getCurrentProblem_(browser); if (!problem) { G_Debug(this, "No problem in queue; doc nav'd away from? (shrug)"); return; } // Two problem docs that load in rapid succession could both schedule // themselves to be unqueued before this method is called. So ensure // that the problem at the head of the queue is not, in fact, active. if (!problem.displayer_.isActive()) { // It could be the case that the server is really slow to respond, // so there might not yet be anything in the problem Document. If // we show the warning when that's the case, the user will see a // blank document greyed out, and if they cancel the dialog // they'll see the page they're navigating away from because it // hasn't been painted over yet (b/c there's no content for the // problem page). So here we ensure that we have content for the // problem page before showing the dialog. var haveContent = false; try { // This will throw if there's no content yet var h = problem.doc_.defaultView.getComputedStyle(problem.doc_.body, "") .getPropertyValue("height"); G_Debug(this, "body height: " + h); if (Number(h.substring(0, h.length - 2))) haveContent = true; } catch (e) { G_Debug(this, "Masked in unqueuenextproblem: " + e); } if (!haveContent) { G_Debug(this, "Didn't get computed style. Re-queueing."); // One stuck problem document in a page shouldn't prevent us // warning on other problem frames that might be loading or // loaded. So stick the Document that doesn't have content // back at the end of the queue. var p = this.removeProblemFromQueue_(problem.doc_, browser); G_Assert(this, p === problem, "Unqueued wrong problem?"); this.queueProblem_(browser, problem); // Try again in a bit. This opens us up to a potential // vulnerability (put tons of hanging frames in a page // ahead of your real phishy frame), but the risk at the // moment is really low (plus it is outside our threat // model). new G_Alarm(BindToObject(this.unqueueNextProblem_, this, browser), 200 /*ms*/); return; } problem.displayer_.start(); // OK, we have content, but there there is an additional // issue. Due to a bfcache bug, if we show the warning during // paint suppression, the collapsing of the content pane affects // the doc we're naving from :( The symptom is a page with grey // screen on navigation to or from a phishing page (the // contentDocument will have width zero). // // Paint supression lasts at most 250ms from when the parser sees // the body, and the parser sees the body well before it has a // height. We err on the side of caution. // // Thanks to bryner for helping to track the bfcache bug down. // https://bugzilla.mozilla.org/show_bug.cgi?id=319646 if (this.tabWatcher_.getCurrentBrowser() === browser) new G_Alarm(BindToObject(this.problemBrowserMaybeSelected, this, browser), 350 /*ms*/); } } /** * Helper function that adds a new problem to the queue of problems pending * on this browser. * * @param browser Browser to which we should add state * * @param problem Object (structure, really) encapsulating the problem * * @returns Number indicating the number of items in the queue (and from * which you can infer whether the recently added item was * placed at the head, and hence should be active. */ PROT_BrowserView.prototype.queueProblem_ = function(browser, problem) { G_Debug(this, "Adding problem state for " + problem.url_); if (this.hasNonemptyProblemQueue_(browser)) G_Debug(this, "Already has problem state. Queueing this problem..."); // First problem ever signaled on this browser? Make a new queue! if (browser.PROT_problemState__ == undefined) browser.PROT_problemState__ = []; browser.PROT_problemState__.push(problem); return browser.PROT_problemState__.length; } /** * Helper function that removes a problem from the queue and deactivates * it. * * @param doc Reference to the doc for which we should remove state * * @param browser Reference to the browser from which we should remove * state * * @returns Boolean indicating if the remove problem was currently active * (that is, if it was at the head of the queue) */ PROT_BrowserView.prototype.deleteProblemFromQueue_ = function(doc, browser) { G_Debug(this, "Deleting problem state for " + browser); G_Assert(this, !!this.hasNonemptyProblemQueue_(browser), "Browser has no problem state"); var problem = this.getProblem_(doc, browser); G_Assert(this, !!problem, "Couldnt find state in removeproblemstate???"); var wasHead = browser.PROT_problemState__[0] === problem; this.removeProblemFromQueue_(doc, browser); var hideHandler = problem.hideHandler_; G_Assert(this, !!hideHandler, "No hidehandler in state?"); problem.doc_.defaultView.removeEventListener("pagehide", hideHandler, true); return wasHead; } /** * Helper function that removes a problem from the queue but does * NOT deactivate it. * * @param doc Reference to the doc for which we should remove state * * @param browser Reference to the browser from which we should remove * state * * @returns Boolean indicating if the remove problem was currently active * (that is, if it was at the head of the queue) */ PROT_BrowserView.prototype.removeProblemFromQueue_ = function(doc, browser) { G_Debug(this, "Removing problem state for " + browser); G_Assert(this, !!this.hasNonemptyProblemQueue_(browser), "Browser has no problem state"); var problem = null; // TODO Blech. Let's please have an abstraction here instead. for (var i = 0; i < browser.PROT_problemState__.length; i++) if (browser.PROT_problemState__[i].doc_ === doc) { problem = browser.PROT_problemState__.splice(i, 1)[0]; break; } return problem; } /** * Retrieve (but do not remove) the problem state for a particular * problematic Document in this browser * * @param doc Reference to the problematic doc to get state for * * @param browser Reference to the browser from which to get state * * @returns Object encapsulating the state we stored, or null if none */ PROT_BrowserView.prototype.getProblem_ = function(doc, browser) { if (!this.hasNonemptyProblemQueue_(browser)) return null; // TODO Blech. Let's please have an abstraction here instead. for (var i = 0; i < browser.PROT_problemState__.length; i++) if (browser.PROT_problemState__[i].doc_ === doc) return browser.PROT_problemState__[i]; return null; } /** * Retrieve the problem state for the currently active problem Document * in this browser * * @param browser Reference to the browser from which to get state * * @returns Object encapsulating the state we stored, or null if none */ PROT_BrowserView.prototype.getCurrentProblem_ = function(browser) { return browser.PROT_problemState__[0]; } /** * Invoked by the controller when the user switches tabs away from a problem * tab. * * @param browser Reference to the tab that was switched from */ PROT_BrowserView.prototype.problemBrowserUnselected = function(browser) { var problem = this.getCurrentProblem_(browser); G_Assert(this, !!problem, "Couldn't get state from browser"); problem.displayer_.browserUnselected(); } /** * Checks to see if the problem browser is selected, and if so, * tell it it to show its warning. * * @param browser Reference to the browser we wish to check */ PROT_BrowserView.prototype.problemBrowserMaybeSelected = function(browser) { var problem = this.getCurrentProblem_(browser); if (this.tabWatcher_.getCurrentBrowser() === browser && problem && problem.displayer_.isActive()) this.problemBrowserSelected(browser); } /** * Invoked by the controller when the user switches tabs to a problem tab * * @param browser Reference to the tab that was switched to */ PROT_BrowserView.prototype.problemBrowserSelected = function(browser) { G_Debug(this, "Problem browser selected"); var problem = this.getCurrentProblem_(browser); G_Assert(this, !!problem, "No state? But we're selected!"); problem.displayer_.browserSelected(); } /** * Invoked by the controller when the user accepts our warning. Passes * the accept through to the message displayer, which knows what to do * (it will be displayer-specific). * * @param browser Reference to the browser for which the user accepted * our warning */ PROT_BrowserView.prototype.acceptAction = function(browser) { var problem = this.getCurrentProblem_(browser); // We run the action only after we're completely through processing // this event. We do this because the action could cause state to be // cleared (e.g., by navigating the problem document) that we need // to finish processing the event. new G_Alarm(BindToObject(problem.displayer_.acceptAction, problem.displayer_), 0); } /** * Invoked by the controller when the user declines our * warning. Passes the decline through to the message displayer, which * knows what to do (it will be displayer-specific). * * @param browser Reference to the browser for which the user declined * our warning */ PROT_BrowserView.prototype.declineAction = function(browser) { var problem = this.getCurrentProblem_(browser); G_Assert(this, !!problem, "User declined but no state???"); // We run the action only after we're completely through processing // this event. We do this because the action could cause state to be // cleared (e.g., by navigating the problem document) that we need // to finish processing the event. new G_Alarm(BindToObject(problem.displayer_.declineAction, problem.displayer_), 0); } /** * The user wants to see the warning message. So let em! At some point when * we have multiple types of warnings, we'll have to mediate them here. * * @param browser Reference to the browser that has the warning the user * wants to see. */ PROT_BrowserView.prototype.explicitShow = function(browser) { var problem = this.getCurrentProblem_(browser); G_Assert(this, !!problem, "Explicit show on browser w/o problem state???"); problem.displayer_.explicitShow(); }