// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- /* * ***** 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 Mozilla Mobile Browser. * * The Initial Developer of the Original Code is * Mozilla Corporation. * Portions created by the Initial Developer are Copyright (C) 2011 * the Initial Developer. All Rights Reserved. * * Contributor(s): * * 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 ***** */ "use strict"; let Cc = Components.classes; let Ci = Components.interfaces; let Cu = Components.utils; let Cr = Components.results; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm") Cu.import("resource://gre/modules/AddonManager.jsm"); Cu.import("resource://gre/modules/FileUtils.jsm"); XPCOMUtils.defineLazyGetter(this, "PluralForm", function() { Cu.import("resource://gre/modules/PluralForm.jsm"); return PluralForm; }); // Lazily-loaded browser scripts: [ ["SelectHelper", "chrome://browser/content/SelectHelper.js"], ].forEach(function (aScript) { let [name, script] = aScript; XPCOMUtils.defineLazyGetter(window, name, function() { let sandbox = {}; Services.scriptloader.loadSubScript(script, sandbox); return sandbox[name]; }); }); XPCOMUtils.defineLazyServiceGetter(this, "Haptic", "@mozilla.org/widget/hapticfeedback;1", "nsIHapticFeedback"); XPCOMUtils.defineLazyServiceGetter(this, "DOMUtils", "@mozilla.org/inspector/dom-utils;1", "inIDOMUtils"); const kStateActive = 0x00000001; // :active pseudoclass for elements const kXLinkNamespace = "http://www.w3.org/1999/xlink"; // The element tag names that are considered to receive input. Mouse-down // events directed to one of these are allowed to go through. const kElementsReceivingInput = { applet: true, audio: true, button: true, embed: true, input: true, map: true, select: true, textarea: true, video: true }; const kDefaultCSSViewportWidth = 980; const kDefaultCSSViewportHeight = 480; function dump(a) { Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).logStringMessage(a); } function getBridge() { return Cc["@mozilla.org/android/bridge;1"].getService(Ci.nsIAndroidBridge); } function sendMessageToJava(aMessage) { return getBridge().handleGeckoMessage(JSON.stringify(aMessage)); } #ifdef MOZ_CRASHREPORTER Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter", "@mozilla.org/xre/app-info;1", "nsICrashReporter"); #endif XPCOMUtils.defineLazyGetter(this, "ContentAreaUtils", function() { let ContentAreaUtils = {}; Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js", ContentAreaUtils); return ContentAreaUtils; }); XPCOMUtils.defineLazyGetter(this, "Rect", function() { Cu.import("resource://gre/modules/Geometry.jsm"); return Rect; }); function resolveGeckoURI(aURI) { if (aURI.indexOf("chrome://") == 0) { let registry = Cc['@mozilla.org/chrome/chrome-registry;1'].getService(Ci["nsIChromeRegistry"]); return registry.convertChromeURL(Services.io.newURI(aURI, null, null)).spec; } else if (aURI.indexOf("resource://") == 0) { let handler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler); return handler.resolveURI(Services.io.newURI(aURI, null, null)); } return aURI; } /** * Cache of commonly used string bundles. */ var Strings = {}; [ ["brand", "chrome://branding/locale/brand.properties"], ["browser", "chrome://browser/locale/browser.properties"], ["charset", "chrome://global/locale/charsetTitles.properties"] ].forEach(function (aStringBundle) { let [name, bundle] = aStringBundle; XPCOMUtils.defineLazyGetter(Strings, name, function() { return Services.strings.createBundle(bundle); }); }); var MetadataProvider = { getDrawMetadata: function getDrawMetadata() { return JSON.stringify(BrowserApp.selectedTab.getViewport()); }, paintingSuppressed: function paintingSuppressed() { return false; } }; var BrowserApp = { _tabs: [], _selectedTab: null, deck: null, startup: function startup() { window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = new nsBrowserAccess(); dump("zerdatime " + Date.now() + " - browser chrome startup finished."); this.deck = document.getElementById("browsers"); BrowserEventHandler.init(); ViewportHandler.init(); getBridge().setDrawMetadataProvider(MetadataProvider); getBridge().browserApp = this; Services.obs.addObserver(this, "Tab:Add", false); Services.obs.addObserver(this, "Tab:Load", false); Services.obs.addObserver(this, "Tab:Selected", false); Services.obs.addObserver(this, "Tab:Closed", false); Services.obs.addObserver(this, "Session:Back", false); Services.obs.addObserver(this, "Session:Forward", false); Services.obs.addObserver(this, "Session:Reload", false); Services.obs.addObserver(this, "Session:Stop", false); Services.obs.addObserver(this, "SaveAs:PDF", false); Services.obs.addObserver(this, "Browser:Quit", false); Services.obs.addObserver(this, "Preferences:Get", false); Services.obs.addObserver(this, "Preferences:Set", false); Services.obs.addObserver(this, "ScrollTo:FocusedInput", false); Services.obs.addObserver(this, "Sanitize:ClearAll", false); Services.obs.addObserver(this, "PanZoom:PanZoom", false); Services.obs.addObserver(this, "FullScreen:Exit", false); Services.obs.addObserver(this, "Viewport:Change", false); Services.obs.addObserver(this, "Passwords:Init", false); Services.obs.addObserver(this, "FormHistory:Init", false); Services.obs.addObserver(this, "sessionstore-state-purge-complete", false); function showFullScreenWarning() { NativeWindow.toast.show(Strings.browser.GetStringFromName("alertFullScreenToast"), "short"); } window.addEventListener("fullscreen", function() { sendMessageToJava({ gecko: { type: window.fullScreen ? "ToggleChrome:Show" : "ToggleChrome:Hide" } }); }, false); window.addEventListener("mozfullscreenchange", function() { sendMessageToJava({ gecko: { type: document.mozFullScreen ? "DOMFullScreen:Start" : "DOMFullScreen:Stop" } }); if (document.mozFullScreen) showFullScreenWarning(); }, false); // When a restricted key is pressed in DOM full-screen mode, we should display // the "Press ESC to exit" warning message. window.addEventListener("MozShowFullScreenWarning", showFullScreenWarning, true); NativeWindow.init(); Downloads.init(); FormAssistant.init(); OfflineApps.init(); IndexedDB.init(); XPInstallObserver.init(); ConsoleAPI.init(); ClipboardHelper.init(); PermissionsHelper.init(); CharacterEncoding.init(); SearchEngines.init(); // Init LoginManager Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); // Init FormHistory Cc["@mozilla.org/satchel/form-history;1"].getService(Ci.nsIFormHistory2); let url = "about:home"; let forceRestore = false; if ("arguments" in window) { if (window.arguments[0]) url = window.arguments[0]; if (window.arguments[1]) forceRestore = window.arguments[1]; if (window.arguments[2]) gScreenWidth = window.arguments[2]; if (window.arguments[3]) gScreenHeight = window.arguments[3]; } // XXX maybe we don't do this if the launch was kicked off from external Services.io.offline = false; // Broadcast a UIReady message so add-ons know we are finished with startup let event = document.createEvent("Events"); event.initEvent("UIReady", true, false); window.dispatchEvent(event); // restore the previous session let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); if (forceRestore || ss.shouldRestore()) { // A restored tab should not be active if we are loading a URL let restoreToFront = false; sendMessageToJava({ gecko: { type: "Session:RestoreBegin" } }); // Open any commandline URLs, except the homepage if (url && url != "about:home") { this.addTab(url); } else { // Let the session make a restored tab active restoreToFront = true; } // Be ready to handle any restore failures by making sure we have a valid tab opened let restoreCleanup = { observe: function(aSubject, aTopic, aData) { Services.obs.removeObserver(restoreCleanup, "sessionstore-windows-restored"); if (aData == "fail") { BrowserApp.addTab("about:home", { showProgress: false, selected: restoreToFront }); } sendMessageToJava({ gecko: { type: "Session:RestoreEnd" } }); } }; Services.obs.addObserver(restoreCleanup, "sessionstore-windows-restored", false); // Start the restore ss.restoreLastSession(restoreToFront, forceRestore); } else { this.addTab(url, { showProgress: url != "about:home" }); // show telemetry door hanger if we aren't restoring a session this._showTelemetryPrompt(); } // notify java that gecko has loaded sendMessageToJava({ gecko: { type: "Gecko:Ready" } }); // after gecko has loaded, set the checkerboarding pref once at startup (for testing only) sendMessageToJava({ gecko: { "type": "Checkerboard:Toggle", "value": Services.prefs.getBoolPref("gfx.show_checkerboard_pattern") } }); }, _showTelemetryPrompt: function _showTelemetryPrompt() { const PREF_TELEMETRY_PROMPTED = "toolkit.telemetry.prompted"; const PREF_TELEMETRY_ENABLED = "toolkit.telemetry.enabled"; const PREF_TELEMETRY_REJECTED = "toolkit.telemetry.rejected"; const PREF_TELEMETRY_SERVER_OWNER = "toolkit.telemetry.server_owner"; // This is used to reprompt users when privacy message changes const TELEMETRY_PROMPT_REV = 2; let serverOwner = Services.prefs.getCharPref(PREF_TELEMETRY_SERVER_OWNER); let telemetryPrompted = null; try { telemetryPrompted = Services.prefs.getIntPref(PREF_TELEMETRY_PROMPTED); } catch (e) { /* Optional */ } // If the user has seen the latest telemetry prompt, do not prompt again // else clear old prefs and reprompt if (telemetryPrompted === TELEMETRY_PROMPT_REV) return; Services.prefs.clearUserPref(PREF_TELEMETRY_PROMPTED); Services.prefs.clearUserPref(PREF_TELEMETRY_ENABLED); let buttons = [ { label: Strings.browser.GetStringFromName("telemetry.optin.yes"), callback: function () { Services.prefs.setIntPref(PREF_TELEMETRY_PROMPTED, TELEMETRY_PROMPT_REV); Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true); } }, { label: Strings.browser.GetStringFromName("telemetry.optin.no"), callback: function () { Services.prefs.setIntPref(PREF_TELEMETRY_PROMPTED, TELEMETRY_PROMPT_REV); Services.prefs.setBoolPref(PREF_TELEMETRY_REJECTED, true); } } ]; let brandShortName = Strings.brand.GetStringFromName("brandShortName"); let message = Strings.browser.formatStringFromName("telemetry.optin.message2", [serverOwner, brandShortName], 2); let learnMoreLabel = Strings.browser.GetStringFromName("telemetry.optin.learnMore"); let learnMoreUrl = Services.urlFormatter.formatURLPref("app.support.baseURL"); learnMoreUrl += "how-can-i-help-submitting-performance-data"; let options = { link: { label: learnMoreLabel, url: learnMoreUrl } }; NativeWindow.doorhanger.show(message, "telemetry-optin", buttons, this.selectedTab.id, options); }, shutdown: function shutdown() { NativeWindow.uninit(); FormAssistant.uninit(); OfflineApps.uninit(); IndexedDB.uninit(); ViewportHandler.uninit(); XPInstallObserver.uninit(); ConsoleAPI.uninit(); CharacterEncoding.uninit(); SearchEngines.uninit(); }, // This function returns false during periods where the browser displayed document is // different from the browser content document, so user actions and some kinds of viewport // updates should be ignored. This period starts when we start loading a new page or // switch tabs, and ends when the new browser content document has been drawn and handed // off to the compositor. isBrowserContentDocumentDisplayed: function() { if (window.top.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).isFirstPaint) return false; let tab = this.selectedTab; if (!tab) return true; return tab.contentDocumentIsDisplayed; }, displayedDocumentChanged: function() { window.top.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).isFirstPaint = true; }, get tabs() { return this._tabs; }, get selectedTab() { return this._selectedTab; }, set selectedTab(aTab) { if (this._selectedTab) this._selectedTab.setActive(false); this._selectedTab = aTab; if (!aTab) return; aTab.setActive(true); aTab.setResolution(aTab._zoom, true); this.displayedDocumentChanged(); this.deck.selectedPanel = aTab.browser; }, get selectedBrowser() { if (this._selectedTab) return this._selectedTab.browser; return null; }, getTabForId: function getTabForId(aId) { let tabs = this._tabs; for (let i=0; i < tabs.length; i++) { if (tabs[i].id == aId) return tabs[i]; } return null; }, getTabForBrowser: function getTabForBrowser(aBrowser) { let tabs = this._tabs; for (let i = 0; i < tabs.length; i++) { if (tabs[i].browser == aBrowser) return tabs[i]; } return null; }, getTabForWindow: function getTabForWindow(aWindow) { let tabs = this._tabs; for (let i = 0; i < tabs.length; i++) { if (tabs[i].browser.contentWindow == aWindow) return tabs[i]; } return null; }, getBrowserForWindow: function getBrowserForWindow(aWindow) { let tabs = this._tabs; for (let i = 0; i < tabs.length; i++) { if (tabs[i].browser.contentWindow == aWindow) return tabs[i].browser; } return null; }, getBrowserForDocument: function getBrowserForDocument(aDocument) { let tabs = this._tabs; for (let i = 0; i < tabs.length; i++) { if (tabs[i].browser.contentDocument == aDocument) return tabs[i].browser; } return null; }, loadURI: function loadURI(aURI, aBrowser, aParams) { aBrowser = aBrowser || this.selectedBrowser; if (!aBrowser) return; aParams = aParams || {}; let flags = "flags" in aParams ? aParams.flags : Ci.nsIWebNavigation.LOAD_FLAGS_NONE; let postData = ("postData" in aParams && aParams.postData) ? aParams.postData : null; let referrerURI = "referrerURI" in aParams ? aParams.referrerURI : null; let charset = "charset" in aParams ? aParams.charset : null; if ("showProgress" in aParams) { let tab = this.getTabForBrowser(aBrowser); if (tab) tab.showProgress = aParams.showProgress; } try { aBrowser.loadURIWithFlags(aURI, flags, referrerURI, charset, postData); } catch(e) { let tab = this.getTabForBrowser(aBrowser); if (tab) { let message = { gecko: { type: "Content:LoadError", tabID: tab.id, uri: aBrowser.currentURI.spec, title: aBrowser.contentTitle } }; sendMessageToJava(message); dump("Handled load error: " + e) } } }, addTab: function addTab(aURI, aParams) { aParams = aParams || {}; let newTab = new Tab(aURI, aParams); this._tabs.push(newTab); let selected = "selected" in aParams ? aParams.selected : true; if (selected) this.selectedTab = newTab; let evt = document.createEvent("UIEvents"); evt.initUIEvent("TabOpen", true, false, window, null); newTab.browser.dispatchEvent(evt); return newTab; }, // Use this method to close a tab from JS. This method sends a message // to Java to close the tab in the Java UI (we'll get a Tab:Closed message // back from Java when that happens). closeTab: function closeTab(aTab) { if (!aTab) { Cu.reportError("Error trying to close tab (tab doesn't exist)"); return; } let message = { gecko: { type: "Tab:Close", tabID: aTab.id } }; sendMessageToJava(message); }, // Calling this will update the state in BrowserApp after a tab has been // closed in the Java UI. _handleTabClosed: function _handleTabClosed(aTab) { if (aTab == this.selectedTab) this.selectedTab = null; let evt = document.createEvent("UIEvents"); evt.initUIEvent("TabClose", true, false, window, null); aTab.browser.dispatchEvent(evt); aTab.destroy(); this._tabs.splice(this._tabs.indexOf(aTab), 1); }, // Use this method to select a tab from JS. This method sends a message // to Java to select the tab in the Java UI (we'll get a Tab:Selected message // back from Java when that happens). selectTab: function selectTab(aTab) { if (!aTab) { Cu.reportError("Error trying to select tab (tab doesn't exist)"); return; } // There's nothing to do if the tab is already selected if (aTab == this.selectedTab) return; let message = { gecko: { type: "Tab:Select", tabID: aTab.id } }; sendMessageToJava(message); }, // This method updates the state in BrowserApp after a tab has been selected // in the Java UI. _handleTabSelected: function _handleTabSelected(aTab) { this.selectedTab = aTab; let evt = document.createEvent("UIEvents"); evt.initUIEvent("TabSelect", true, false, window, null); aTab.browser.dispatchEvent(evt); }, quit: function quit() { // Figure out if there's at least one other browser window around. let lastBrowser = true; let e = Services.wm.getEnumerator("navigator:browser"); while (e.hasMoreElements() && lastBrowser) { let win = e.getNext(); if (win != window) lastBrowser = false; } if (lastBrowser) { // Let everyone know we are closing the last browser window let closingCanceled = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool); Services.obs.notifyObservers(closingCanceled, "browser-lastwindow-close-requested", null); if (closingCanceled.data) return; Services.obs.notifyObservers(null, "browser-lastwindow-close-granted", null); } window.QueryInterface(Ci.nsIDOMChromeWindow).minimize(); window.close(); }, saveAsPDF: function saveAsPDF(aBrowser) { // Create the final destination file location let fileName = ContentAreaUtils.getDefaultFileName(aBrowser.contentTitle, aBrowser.currentURI, null, null); fileName = fileName.trim() + ".pdf"; let dm = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager); let downloadsDir = dm.defaultDownloadsDirectory; let file = downloadsDir.clone(); file.append(fileName); file.createUnique(file.NORMAL_FILE_TYPE, parseInt("666", 8)); let printSettings = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(Ci.nsIPrintSettingsService).newPrintSettings; printSettings.printSilent = true; printSettings.showPrintProgress = false; printSettings.printBGImages = true; printSettings.printBGColors = true; printSettings.printToFile = true; printSettings.toFileName = file.path; printSettings.printFrameType = Ci.nsIPrintSettings.kFramesAsIs; printSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF; //XXX we probably need a preference here, the header can be useful printSettings.footerStrCenter = ""; printSettings.footerStrLeft = ""; printSettings.footerStrRight = ""; printSettings.headerStrCenter = ""; printSettings.headerStrLeft = ""; printSettings.headerStrRight = ""; // Create a valid mimeInfo for the PDF let ms = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); let mimeInfo = ms.getFromTypeAndExtension("application/pdf", "pdf"); let webBrowserPrint = aBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebBrowserPrint); let cancelable = { cancel: function (aReason) { webBrowserPrint.cancel(); } } let download = dm.addDownload(Ci.nsIDownloadManager.DOWNLOAD_TYPE_DOWNLOAD, aBrowser.currentURI, Services.io.newFileURI(file), "", mimeInfo, Date.now() * 1000, null, cancelable); webBrowserPrint.print(printSettings, download); }, getPreferences: function getPreferences(aPrefNames) { try { let json = JSON.parse(aPrefNames); let prefs = []; for each (let prefName in json) { let pref = { name: prefName }; // The plugin pref is actually two separate prefs, so // we need to handle it differently if (prefName == "plugin.enable") { // Use a string type for java's ListPreference pref.type = "string"; pref.value = PluginHelper.getPluginPreference(); prefs.push(pref); continue; } else if (prefName == MasterPassword.pref) { // Master password is not a "real" pref pref.type = "bool"; pref.value = MasterPassword.enabled; prefs.push(pref); continue; } try { switch (Services.prefs.getPrefType(prefName)) { case Ci.nsIPrefBranch.PREF_BOOL: pref.type = "bool"; pref.value = Services.prefs.getBoolPref(prefName); break; case Ci.nsIPrefBranch.PREF_INT: pref.type = "int"; pref.value = Services.prefs.getIntPref(prefName); break; case Ci.nsIPrefBranch.PREF_STRING: default: pref.type = "string"; pref.value = Services.prefs.getComplexValue(prefName, Ci.nsIPrefLocalizedString).data; break; } } catch (e) { // preference does not exist; do not send it continue; } // some preferences use integers or strings instead of booleans for // indicating enabled/disabled. since the java ui uses the type to // determine which ui elements to show, we need to normalize these // preferences to be actual booleans. switch (prefName) { case "network.cookie.cookieBehavior": pref.type = "bool"; pref.value = pref.value == 0; break; case "font.size.inflation.minTwips": pref.type = "string"; pref.value = pref.value.toString(); break; } prefs.push(pref); } sendMessageToJava({ gecko: { type: "Preferences:Data", preferences: prefs } }); } catch (e) {} }, setPreferences: function setPreferences(aPref) { let json = JSON.parse(aPref); // The plugin pref is actually two separate prefs, so // we need to handle it differently if (json.name == "plugin.enable") { PluginHelper.setPluginPreference(json.value); return; } else if(json.name == MasterPassword.pref) { if (MasterPassword.enabled) MasterPassword.removePassword(json.value); else MasterPassword.setPassword(json.value); } // when sending to java, we normalized special preferences that use // integers and strings to represent booleans. here, we convert them back // to their actual types so we can store them. switch (json.name) { case "network.cookie.cookieBehavior": json.type = "int"; json.value = (json.value ? 0 : 2); break; case "font.size.inflation.minTwips": json.type = "int"; json.value = parseInt(json.value); break; } if (json.type == "bool") Services.prefs.setBoolPref(json.name, json.value); else if (json.type == "int") Services.prefs.setIntPref(json.name, json.value); else { let pref = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(Ci.nsIPrefLocalizedString); pref.data = json.value; Services.prefs.setComplexValue(json.name, Ci.nsISupportsString, pref); } }, scrollToFocusedInput: function(aBrowser) { let doc = aBrowser.contentDocument; if (!doc) return; let focused = doc.activeElement; if ((focused instanceof HTMLInputElement && focused.mozIsTextField(false)) || (focused instanceof HTMLTextAreaElement)) { let tab = BrowserApp.getTabForBrowser(aBrowser); let win = aBrowser.contentWindow; // tell gecko to scroll the field into view. this will scroll any nested scrollable elements // as well as the browser's content window, and modify the scrollX and scrollY on the content window. focused.scrollIntoView(false); // As Gecko isn't aware of the zoom level we're drawing with, the element may not entirely be in view // yet. Check for that, and scroll some extra to compensate, if necessary. let focusedRect = focused.getBoundingClientRect(); let visibleContentWidth = gScreenWidth / tab._zoom; let visibleContentHeight = gScreenHeight / tab._zoom; let positionChanged = false; let scrollX = win.scrollX; let scrollY = win.scrollY; if (focusedRect.right >= visibleContentWidth && focusedRect.left > 0) { // the element is too far off the right side, so we need to scroll to the right more scrollX += Math.min(focusedRect.left, focusedRect.right - visibleContentWidth); positionChanged = true; } else if (focusedRect.left < 0) { // the element is too far off the left side, so we need to scroll to the left more scrollX += focusedRect.left; positionChanged = true; } if (focusedRect.bottom >= visibleContentHeight && focusedRect.top > 0) { // the element is too far down, so we need to scroll down more scrollY += Math.min(focusedRect.top, focusedRect.bottom - visibleContentHeight); positionChanged = true; } else if (focusedRect.top < 0) { // the element is too far up, so we need to scroll up more scrollY += focusedRect.top; positionChanged = true; } if (positionChanged) win.scrollTo(scrollX, scrollY); // update userScrollPos so that we don't send a duplicate viewport update by triggering // our scroll listener tab.userScrollPos.x = win.scrollX; tab.userScrollPos.y = win.scrollY; // finally, let java know where we ended up tab.sendViewportUpdate(); } }, observe: function(aSubject, aTopic, aData) { let browser = this.selectedBrowser; if (!browser) return; if (aTopic == "Session:Back") { browser.goBack(); } else if (aTopic == "Session:Forward") { browser.goForward(); } else if (aTopic == "Session:Reload") { browser.reload(); } else if (aTopic == "Session:Stop") { browser.stop(); } else if (aTopic == "Tab:Add" || aTopic == "Tab:Load") { let data = JSON.parse(aData); // Pass LOAD_FLAGS_DISALLOW_INHERIT_OWNER to prevent any loads from // inheriting the currently loaded document's principal. let flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP; if (data.userEntered) flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_OWNER; let params = { selected: true, parentId: ("parentId" in data) ? data.parentId : -1, flags: flags }; let url = data.url; if (data.engine) { let engine = Services.search.getEngineByName(data.engine); if (engine) { let submission = engine.getSubmission(url); url = submission.uri.spec; params.postData = submission.postData; } } // Don't show progress throbber for about:home if (url == "about:home") params.showProgress = false; if (aTopic == "Tab:Add") this.addTab(url, params); else this.loadURI(url, browser, params); } else if (aTopic == "Tab:Selected") { this._handleTabSelected(this.getTabForId(parseInt(aData))); } else if (aTopic == "Tab:Closed") { this._handleTabClosed(this.getTabForId(parseInt(aData))); } else if (aTopic == "Browser:Quit") { this.quit(); } else if (aTopic == "SaveAs:PDF") { this.saveAsPDF(browser); } else if (aTopic == "Preferences:Get") { this.getPreferences(aData); } else if (aTopic == "Preferences:Set") { this.setPreferences(aData); } else if (aTopic == "ScrollTo:FocusedInput") { this.scrollToFocusedInput(browser); } else if (aTopic == "Sanitize:ClearAll") { Sanitizer.sanitize(); } else if (aTopic == "FullScreen:Exit") { browser.contentDocument.mozCancelFullScreen(); } else if (aTopic == "Viewport:Change") { if (this.isBrowserContentDocumentDisplayed()) this.selectedTab.setViewport(JSON.parse(aData)); } else if (aTopic == "SearchEngines:Get") { this.getSearchEngines(); } else if (aTopic == "Passwords:Init") { var storage = Components.classes["@mozilla.org/login-manager/storage/mozStorage;1"]. getService(Components.interfaces.nsILoginManagerStorage); storage.init(); sendMessageToJava({gecko: { type: "Passwords:Init:Return" }}); Services.obs.removeObserver(this, "Passwords:Init", false); } else if (aTopic == "FormHistory:Init") { var fh = Components.classes["@mozilla.org/satchel/form-history;1"]. getService(Components.interfaces.nsIFormHistory2); var db = fh.DBConnection; sendMessageToJava({gecko: { type: "FormHistory:Init:Return" }}); Services.obs.removeObserver(this, "FormHistory:Init", false); } else if (aTopic == "sessionstore-state-purge-complete") { sendMessageToJava({ gecko: { type: "Session:StatePurged" }}); } }, get defaultBrowserWidth() { delete this.defaultBrowserWidth; let width = Services.prefs.getIntPref("browser.viewport.desktopWidth"); return this.defaultBrowserWidth = width; }, // nsIAndroidBrowserApp getBrowserTab: function(tabId) { return this.getTabForId(tabId); } }; var NativeWindow = { init: function() { Services.obs.addObserver(this, "Menu:Clicked", false); Services.obs.addObserver(this, "Doorhanger:Reply", false); this.contextmenus.init(); }, uninit: function() { Services.obs.removeObserver(this, "Menu:Clicked"); Services.obs.removeObserver(this, "Doorhanger:Reply"); this.contextmenus.uninit(); }, toast: { show: function(aMessage, aDuration) { sendMessageToJava({ gecko: { type: "Toast:Show", message: aMessage, duration: aDuration } }); } }, menu: { _callbacks: [], _menuId: 0, add: function(aName, aIcon, aCallback) { sendMessageToJava({ gecko: { type: "Menu:Add", name: aName, icon: aIcon, id: this._menuId } }); this._callbacks[this._menuId] = aCallback; this._menuId++; return this._menuId - 1; }, remove: function(aId) { sendMessageToJava({ gecko: {type: "Menu:Remove", id: aId }}); } }, doorhanger: { _callbacks: {}, _callbacksId: 0, _promptId: 0, /** * @param aOptions * An options JavaScript object holding additional properties for the * notification. The following properties are currently supported: * persistence: An integer. The notification will not automatically * dismiss for this many page loads. If persistence is set * to -1, the doorhanger will never automatically dismiss. * timeout: A time in milliseconds. The notification will not * automatically dismiss before this time. */ show: function(aMessage, aValue, aButtons, aTabID, aOptions) { aButtons.forEach((function(aButton) { this._callbacks[this._callbacksId] = { cb: aButton.callback, prompt: this._promptId }; aButton.callback = this._callbacksId; this._callbacksId++; }).bind(this)); this._promptId++; let json = { gecko: { type: "Doorhanger:Add", message: aMessage, value: aValue, buttons: aButtons, // use the current tab if none is provided tabID: aTabID || BrowserApp.selectedTab.id, options: aOptions || {} } }; sendMessageToJava(json); }, hide: function(aValue, aTabID) { sendMessageToJava({ type: "Doorhanger:Remove", value: aValue, tabID: aTabID }); } }, observe: function(aSubject, aTopic, aData) { if (aTopic == "Menu:Clicked") { if (this.menu._callbacks[aData]) this.menu._callbacks[aData](); } else if (aTopic == "Doorhanger:Reply") { let reply_id = aData; if (this.doorhanger._callbacks[reply_id]) { let prompt = this.doorhanger._callbacks[reply_id].prompt; this.doorhanger._callbacks[reply_id].cb(); for (let id in this.doorhanger._callbacks) { if (this.doorhanger._callbacks[id].prompt == prompt) { delete this.doorhanger._callbacks[id]; } } } } }, contextmenus: { items: {}, // a list of context menu items that we may show _contextId: 0, // id to assign to new context menu items if they are added init: function() { this.imageContext = this.SelectorContext("img"); Services.obs.addObserver(this, "Gesture:LongPress", false); // TODO: These should eventually move into more appropriate classes this.add(Strings.browser.GetStringFromName("contextmenu.openInNewTab"), this.linkOpenableContext, function(aTarget) { let url = NativeWindow.contextmenus._getLinkURL(aTarget); BrowserApp.addTab(url, { selected: false, parentId: BrowserApp.selectedTab.id }); let newtabStrings = Strings.browser.GetStringFromName("newtabpopup.opened"); let label = PluralForm.get(1, newtabStrings).replace("#1", 1); NativeWindow.toast.show(label, "short"); }); this.add(Strings.browser.GetStringFromName("contextmenu.shareLink"), this.linkShareableContext, function(aTarget) { let url = NativeWindow.contextmenus._getLinkURL(aTarget); let title = aTarget.textContent || aTarget.title; let sharing = Cc["@mozilla.org/uriloader/external-sharing-app-service;1"].getService(Ci.nsIExternalSharingAppService); sharing.shareWithDefault(url, "text/plain", title); }); this.add(Strings.browser.GetStringFromName("contextmenu.bookmarkLink"), this.linkBookmarkableContext, function(aTarget) { let url = NativeWindow.contextmenus._getLinkURL(aTarget); let title = aTarget.textContent || aTarget.title || url; sendMessageToJava({ gecko: { type: "Bookmark:Insert", url: url, title: title } }); }); this.add(Strings.browser.GetStringFromName("contextmenu.fullScreen"), this.SelectorContext("video:not(:-moz-full-screen)"), function(aTarget) { aTarget.mozRequestFullScreen(); }); this.add(Strings.browser.GetStringFromName("contextmenu.saveImage"), this.imageContext, function(aTarget) { let imageCache = Cc["@mozilla.org/image/cache;1"].getService(Ci.imgICache); let props = imageCache.findEntryProperties(aTarget.currentURI, aTarget.ownerDocument.characterSet); var contentDisposition = ""; var type = ""; try { String(props.get("content-disposition", Ci.nsISupportsCString)); String(props.get("type", Ci.nsISupportsCString)); } catch(ex) { } var browser = BrowserApp.getBrowserForDocument(aTarget.ownerDocument); ContentAreaUtils.internalSave(aTarget.currentURI.spec, null, null, contentDisposition, type, false, "SaveImageTitle", null, browser.documentURI, true, null); }); }, uninit: function() { Services.obs.removeObserver(this, "Gesture:LongPress"); }, add: function(aName, aSelector, aCallback) { if (!aName) throw "Menu items must have a name"; let item = { name: aName, context: aSelector, callback: aCallback, matches: function(aElt) { return this.context.matches(aElt); }, getValue: function() { return { label: this.name, id: this.id } } }; item.id = this._contextId++; this.items[item.id] = item; return item.id; }, remove: function(aId) { delete this.items[aId]; }, SelectorContext: function(aSelector) { return { matches: function(aElt) { if (aElt.mozMatchesSelector) return aElt.mozMatchesSelector(aSelector); return false; } } }, linkOpenableContext: { matches: function linkOpenableContextMatches(aElement) { let uri = NativeWindow.contextmenus._getLink(aElement); if (uri) { let scheme = uri.scheme; let dontOpen = /^(mailto|javascript|news|snews)$/; return (scheme && !dontOpen.test(scheme)); } return false; } }, linkShareableContext: { matches: function linkShareableContextMatches(aElement) { let uri = NativeWindow.contextmenus._getLink(aElement); if (uri) { let scheme = uri.scheme; let dontShare = /^(chrome|about|file|javascript|resource)$/; return (scheme && !dontShare.test(scheme)); } return false; } }, linkBookmarkableContext: { matches: function linkBookmarkableContextMatches(aElement) { let uri = NativeWindow.contextmenus._getLink(aElement); if (uri) { let scheme = uri.scheme; let dontBookmark = /^(mailto)$/; return (scheme && !dontBookmark.test(scheme)); } return false; } }, textContext: { matches: function textContext(aElement) { return ((aElement instanceof Ci.nsIDOMHTMLInputElement && aElement.mozIsTextField(false)) || aElement instanceof Ci.nsIDOMHTMLTextAreaElement); } }, _sendToContent: function(aX, aY) { // initially we look for nearby clickable elements. If we don't find one we fall back to using whatever this click was on let rootElement = ElementTouchHelper.elementFromPoint(BrowserApp.selectedBrowser.contentWindow, aX, aY); if (!rootElement) rootElement = ElementTouchHelper.anyElementFromPoint(BrowserApp.selectedBrowser.contentWindow, aX, aY) this.menuitems = null; let element = rootElement; if (!element) return; while (element) { for each (let item in this.items) { // since we'll have to spin through this for each element, check that // it is not already in the list if ((!this.menuitems || !this.menuitems[item.id]) && item.matches(element)) { if (!this.menuitems) this.menuitems = {}; this.menuitems[item.id] = item; } } if (this.linkOpenableContext.matches(element) || this.textContext.matches(element)) break; element = element.parentNode; } // only send the contextmenu event to content if we are planning to show a context menu (i.e. not on every long tap) if (this.menuitems) { BrowserEventHandler.blockClick = true; let event = rootElement.ownerDocument.createEvent("MouseEvent"); event.initMouseEvent("contextmenu", true, true, content, 0, aX, aY, aX, aY, false, false, false, false, 0, null); rootElement.ownerDocument.defaultView.addEventListener("contextmenu", this, false); rootElement.dispatchEvent(event); } }, _show: function(aEvent) { if (aEvent.defaultPrevented) return; let popupNode = aEvent.originalTarget; let title = ""; if ((popupNode instanceof Ci.nsIDOMHTMLAnchorElement && popupNode.href) || (popupNode instanceof Ci.nsIDOMHTMLAreaElement && popupNode.href)) { title = this._getLinkURL(popupNode); } else if (popupNode instanceof Ci.nsIImageLoadingContent && popupNode.currentURI) { title = popupNode.currentURI.spec; } else if (popupNode instanceof Ci.nsIDOMHTMLMediaElement) { title = (popupNode.currentSrc || popupNode.src); } // convert this.menuitems object to an array for sending to native code let itemArray = []; for each (let item in this.menuitems) { itemArray.push(item.getValue()); } let msg = { gecko: { type: "Prompt:Show", title: title, listitems: itemArray } }; let data = JSON.parse(sendMessageToJava(msg)); let selectedId = itemArray[data.button].id; let selectedItem = this.menuitems[selectedId]; if (selectedItem && selectedItem.callback) { while (popupNode) { if (selectedItem.matches(popupNode)) { selectedItem.callback.call(selectedItem, popupNode); break; } popupNode = popupNode.parentNode; } } this.menuitems = null; }, handleEvent: function(aEvent) { aEvent.target.ownerDocument.defaultView.removeEventListener("contextmenu", this, false); this._show(aEvent); }, observe: function(aSubject, aTopic, aData) { BrowserEventHandler._cancelTapHighlight(); let data = JSON.parse(aData); // content gets first crack at cancelling context menus this._sendToContent(data.x, data.y); }, // XXX - These are stolen from Util.js, we should remove them if we bring it back makeURLAbsolute: function makeURLAbsolute(base, url) { // Note: makeURI() will throw if url is not a valid URI return this.makeURI(url, null, this.makeURI(base)).spec; }, makeURI: function makeURI(aURL, aOriginCharset, aBaseURI) { return Services.io.newURI(aURL, aOriginCharset, aBaseURI); }, _getLink: function(aElement) { if (aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE && ((aElement instanceof Ci.nsIDOMHTMLAnchorElement && aElement.href) || (aElement instanceof Ci.nsIDOMHTMLAreaElement && aElement.href) || aElement instanceof Ci.nsIDOMHTMLLinkElement || aElement.getAttributeNS(kXLinkNamespace, "type") == "simple")) { try { let url = NativeWindow.contextmenus._getLinkURL(aElement); return Services.io.newURI(url, null, null); } catch (e) {} } return null; }, _getLinkURL: function ch_getLinkURL(aLink) { let href = aLink.href; if (href) return href; href = aLink.getAttributeNS(kXLinkNamespace, "href"); if (!href || !href.match(/\S/)) { // Without this we try to save as the current doc, // for example, HTML case also throws if empty throw "Empty href"; } return this.makeURLAbsolute(aLink.baseURI, href); } } }; function nsBrowserAccess() { } nsBrowserAccess.prototype = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserDOMWindow]), _getBrowser: function _getBrowser(aURI, aOpener, aWhere, aContext) { let isExternal = (aContext == Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL); if (isExternal && aURI && aURI.schemeIs("chrome")) return null; let loadflags = isExternal ? Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL : Ci.nsIWebNavigation.LOAD_FLAGS_NONE; if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW) { switch (aContext) { case Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL: aWhere = Services.prefs.getIntPref("browser.link.open_external"); break; default: // OPEN_NEW or an illegal value aWhere = Services.prefs.getIntPref("browser.link.open_newwindow"); } } Services.io.offline = false; let referrer; if (aOpener) { try { let location = aOpener.location; referrer = Services.io.newURI(location, null, null); } catch(e) { } } let newTab = (aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW || aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWTAB); if (newTab) { let parentId = -1; if (!isExternal) { let parent = BrowserApp.getTabForWindow(aOpener.top); if (parent) parentId = parent.id; } // BrowserApp.addTab calls loadURIWithFlags with the appropriate params let tab = BrowserApp.addTab(aURI ? aURI.spec : "about:blank", { flags: loadflags, referrerURI: referrer, external: isExternal, parentId: parentId, selected: true }); return tab.browser; } // OPEN_CURRENTWINDOW and illegal values let browser = BrowserApp.selectedBrowser; if (aURI && browser) browser.loadURIWithFlags(aURI.spec, loadflags, referrer, null, null); return browser; }, openURI: function browser_openURI(aURI, aOpener, aWhere, aContext) { let browser = this._getBrowser(aURI, aOpener, aWhere, aContext); return browser ? browser.contentWindow : null; }, openURIInFrame: function browser_openURIInFrame(aURI, aOpener, aWhere, aContext) { let browser = this._getBrowser(aURI, aOpener, aWhere, aContext); return browser ? browser.QueryInterface(Ci.nsIFrameLoaderOwner) : null; }, isTabContentWindow: function(aWindow) { return BrowserApp.getBrowserForWindow(aWindow) != null; } }; let gTabIDFactory = 0; // track the last known screen size so that new tabs // get created with the right size rather than being 1x1 let gScreenWidth = 1; let gScreenHeight = 1; function Tab(aURL, aParams) { this.browser = null; this.id = 0; this.showProgress = true; this.create(aURL, aParams); this._zoom = 1.0; this.userScrollPos = { x: 0, y: 0 }; this._pluginCount = 0; this._pluginOverlayShowing = false; this.contentDocumentIsDisplayed = true; } Tab.prototype = { create: function(aURL, aParams) { if (this.browser) return; aParams = aParams || {}; this.browser = document.createElement("browser"); this.browser.setAttribute("type", "content-targetable"); this.setBrowserSize(kDefaultCSSViewportWidth, kDefaultCSSViewportHeight); BrowserApp.deck.appendChild(this.browser); this.browser.stop(); let frameLoader = this.browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader; frameLoader.renderMode = Ci.nsIFrameLoader.RENDER_MODE_ASYNC_SCROLL; frameLoader.clampScrollPosition = false; // only set tab uri if uri is valid let uri = null; try { uri = Services.io.newURI(aURL, null, null).spec; } catch (e) {} this.id = ++gTabIDFactory; let message = { gecko: { type: "Tab:Added", tabID: this.id, uri: uri, parentId: ("parentId" in aParams) ? aParams.parentId : -1, external: ("external" in aParams) ? aParams.external : false, selected: ("selected" in aParams) ? aParams.selected : true, title: aParams.title || aURL, delayLoad: aParams.delayLoad || false } }; sendMessageToJava(message); this.overscrollController = new OverscrollController(this); this.browser.contentWindow.controllers .insertControllerAt(0, this.overscrollController); let flags = Ci.nsIWebProgress.NOTIFY_STATE_ALL | Ci.nsIWebProgress.NOTIFY_LOCATION | Ci.nsIWebProgress.NOTIFY_SECURITY; this.browser.addProgressListener(this, flags); this.browser.sessionHistory.addSHistoryListener(this); this.browser.addEventListener("DOMContentLoaded", this, true); this.browser.addEventListener("DOMLinkAdded", this, true); this.browser.addEventListener("DOMTitleChanged", this, true); this.browser.addEventListener("DOMWindowClose", this, true); this.browser.addEventListener("DOMWillOpenModalDialog", this, true); this.browser.addEventListener("scroll", this, true); this.browser.addEventListener("PluginClickToPlay", this, true); this.browser.addEventListener("pagehide", this, true); this.browser.addEventListener("pageshow", this, true); Services.obs.addObserver(this, "before-first-paint", false); if (!aParams.delayLoad) { let flags = "flags" in aParams ? aParams.flags : Ci.nsIWebNavigation.LOAD_FLAGS_NONE; let postData = ("postData" in aParams && aParams.postData) ? aParams.postData.value : null; let referrerURI = "referrerURI" in aParams ? aParams.referrerURI : null; let charset = "charset" in aParams ? aParams.charset : null; // This determines whether or not we show the progress throbber in the urlbar this.showProgress = "showProgress" in aParams ? aParams.showProgress : true; try { this.browser.loadURIWithFlags(aURL, flags, referrerURI, charset, postData); } catch(e) { let message = { gecko: { type: "Content:LoadError", tabID: this.id, uri: this.browser.currentURI.spec, title: this.browser.contentTitle } }; sendMessageToJava(message); dump("Handled load error: " + e) } } }, destroy: function() { if (!this.browser) return; this.browser.controllers.contentWindow .removeController(this.overscrollController); this.browser.removeProgressListener(this); this.browser.removeEventListener("DOMContentLoaded", this, true); this.browser.removeEventListener("DOMLinkAdded", this, true); this.browser.removeEventListener("DOMTitleChanged", this, true); this.browser.removeEventListener("DOMWindowClose", this, true); this.browser.removeEventListener("DOMWillOpenModalDialog", this, true); this.browser.removeEventListener("scroll", this, true); this.browser.removeEventListener("PluginClickToPlay", this, true); this.browser.removeEventListener("pagehide", this, true); this.browser.removeEventListener("pageshow", this, true); Services.obs.removeObserver(this, "before-first-paint"); // Make sure the previously selected panel remains selected. The selected panel of a deck is // not stable when panels are removed. let selectedPanel = BrowserApp.deck.selectedPanel; BrowserApp.deck.removeChild(this.browser); BrowserApp.deck.selectedPanel = selectedPanel; this.browser = null; }, // This should be called to update the browser when the tab gets selected/unselected setActive: function setActive(aActive) { if (!this.browser) return; if (aActive) { this.browser.setAttribute("type", "content-primary"); this.browser.focus(); this.browser.docShellIsActive = true; } else { this.browser.setAttribute("type", "content-targetable"); this.browser.docShellIsActive = false; } }, setDisplayPort: function(aViewportX, aViewportY, aDisplayPortRect) { let zoom = this._zoom; if (zoom <= 0) return; let element = this.browser.contentDocument.documentElement; if (!element) return; let cwu = window.top.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); cwu.setDisplayPortForElement((aDisplayPortRect.left - aViewportX) / zoom, (aDisplayPortRect.top - aViewportY) / zoom, (aDisplayPortRect.right - aDisplayPortRect.left) / zoom, (aDisplayPortRect.bottom - aDisplayPortRect.top) / zoom, element); }, setViewport: function(aViewport) { // Transform coordinates based on zoom let x = aViewport.x / aViewport.zoom; let y = aViewport.y / aViewport.zoom; // Set scroll position let win = this.browser.contentWindow; win.scrollTo(x, y); this.userScrollPos.x = win.scrollX; this.userScrollPos.y = win.scrollY; this.setResolution(aViewport.zoom, false); this.setDisplayPort(aViewport.x, aViewport.y, aViewport.displayPort); }, setResolution: function(aZoom, aForce) { // Set zoom level if (aForce || Math.abs(aZoom - this._zoom) >= 1e-6) { this._zoom = aZoom; if (BrowserApp.selectedTab == this) { let cwu = window.top.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); cwu.setResolution(aZoom, aZoom); } } }, getViewport: function() { let viewport = { width: gScreenWidth, height: gScreenHeight, pageWidth: gScreenWidth, pageHeight: gScreenHeight, zoom: this._zoom }; // Set the viewport offset to current scroll offset viewport.x = this.browser.contentWindow.scrollX || 0; viewport.y = this.browser.contentWindow.scrollY || 0; // Transform coordinates based on zoom viewport.x = Math.round(viewport.x * viewport.zoom); viewport.y = Math.round(viewport.y * viewport.zoom); let doc = this.browser.contentDocument; if (doc != null) { let pageWidth = viewport.width, pageHeight = viewport.height; if (doc instanceof SVGDocument) { let rect = doc.rootElement.getBoundingClientRect(); // we need to add rect.left and rect.top twice so that the SVG is drawn // centered on the page; if we add it only once then the SVG will be // on the bottom-right of the page and if we don't add it at all then // we end up with a cropped SVG (see bug 712065) pageWidth = Math.ceil(rect.left + rect.width + rect.left); pageHeight = Math.ceil(rect.top + rect.height + rect.top); } else { let body = doc.body || { scrollWidth: pageWidth, scrollHeight: pageHeight }; let html = doc.documentElement || { scrollWidth: pageWidth, scrollHeight: pageHeight }; pageWidth = Math.max(body.scrollWidth, html.scrollWidth); pageHeight = Math.max(body.scrollHeight, html.scrollHeight); } /* Transform the page width and height based on the zoom factor. */ pageWidth *= viewport.zoom; pageHeight *= viewport.zoom; /* * Avoid sending page sizes of less than screen size before we hit DOMContentLoaded, because * this causes the page size to jump around wildly during page load. After the page is loaded, * send updates regardless of page size; we'll zoom to fit the content as needed. */ if (doc.readyState === 'complete' || (pageWidth >= gScreenWidth && pageHeight >= gScreenHeight)) { viewport.pageWidth = pageWidth; viewport.pageHeight = pageHeight; } } return viewport; }, sendViewportUpdate: function() { if (BrowserApp.selectedTab != this) return; if (!BrowserApp.isBrowserContentDocumentDisplayed()) return; let message = this.getViewport(); message.type = "Viewport:Update"; let displayPort = sendMessageToJava({ gecko: message }); if (displayPort != null) this.setDisplayPort(message.x, message.y, JSON.parse(displayPort)); }, handleEvent: function(aEvent) { switch (aEvent.type) { case "DOMContentLoaded": { let target = aEvent.originalTarget; // ignore on frames if (target.defaultView != this.browser.contentWindow) return; // Sample the background color of the page and pass it along. (This is used to draw the // checkerboard.) Right now we don't detect changes in the background color after this // event fires; it's not clear that doing so is worth the effort. var backgroundColor = null; try { let browser = this.selectedBrowser; if (browser) { let { contentDocument, contentWindow } = browser; let computedStyle = contentWindow.getComputedStyle(contentDocument.body); backgroundColor = computedStyle.backgroundColor; } } catch (e) { // Ignore. Catching and ignoring exceptions here ensures that Talos succeeds. } sendMessageToJava({ gecko: { type: "DOMContentLoaded", tabID: this.id, windowID: 0, uri: this.browser.currentURI.spec, title: this.browser.contentTitle, bgColor: backgroundColor } }); // Attach a listener to watch for "click" events bubbling up from error // pages and other similar page. This lets us fix bugs like 401575 which // require error page UI to do privileged things, without letting error // pages have any privilege themselves. if (/^about:/.test(target.documentURI)) { this.browser.addEventListener("click", ErrorPageEventHandler, false); this.browser.addEventListener("pagehide", function listener() { this.browser.removeEventListener("click", ErrorPageEventHandler, false); this.browser.removeEventListener("pagehide", listener, true); }.bind(this), true); } // Show a plugin doorhanger if there are plugins on the page but no // clickable overlays showing (this doesn't work on pages loaded after // back/forward navigation - see bug 719875) if (this._pluginCount && !this._pluginOverlayShowing) PluginHelper.showDoorHanger(this); break; } case "DOMLinkAdded": { let target = aEvent.originalTarget; if (!target.href || target.disabled) return; // ignore on frames if (target.ownerDocument.defaultView != this.browser.contentWindow) return; // sanitize the rel string let list = []; if (target.rel) { list = target.rel.toLowerCase().split(/\s+/); let hash = {}; list.forEach(function(value) { hash[value] = true; }); list = []; for (let rel in hash) list.push("[" + rel + "]"); } let json = { type: "DOMLinkAdded", tabID: this.id, href: resolveGeckoURI(target.href), charset: target.ownerDocument.characterSet, title: target.title, rel: list.join(" ") }; // rel=icon can also have a sizes attribute if (target.hasAttribute("sizes")) json.sizes = target.getAttribute("sizes"); sendMessageToJava({ gecko: json }); break; } case "DOMTitleChanged": { if (!aEvent.isTrusted) return; // ignore on frames if (aEvent.target.defaultView != this.browser.contentWindow) return; sendMessageToJava({ gecko: { type: "DOMTitleChanged", tabID: this.id, title: aEvent.target.title.substring(0, 255) } }); break; } case "DOMWindowClose": { if (!aEvent.isTrusted) return; // Find the relevant tab, and close it from Java if (this.browser.contentWindow == aEvent.target) { aEvent.preventDefault(); sendMessageToJava({ gecko: { type: "DOMWindowClose", tabID: this.id } }); } break; } case "DOMWillOpenModalDialog": { if (!aEvent.isTrusted) return; // We're about to open a modal dialog, make sure the opening // tab is brought to the front. let tab = BrowserApp.getTabForWindow(aEvent.target.top); BrowserApp.selectTab(tab); break; } case "scroll": { let win = this.browser.contentWindow; if (this.userScrollPos.x != win.scrollX || this.userScrollPos.y != win.scrollY) { this.sendViewportUpdate(); } break; } case "PluginClickToPlay": { // Keep track of the number of plugins to know whether or not to show // the hidden plugins doorhanger this._pluginCount++; let plugin = aEvent.target; let overlay = plugin.ownerDocument.getAnonymousElementByAttribute(plugin, "class", "mainBox"); if (!overlay) return; // If the overlay is too small, hide the overlay and act like this // is a hidden plugin object if (PluginHelper.isTooSmall(plugin, overlay)) { overlay.style.visibility = "hidden"; return; } // Add click to play listener to the overlay overlay.addEventListener("click", (function(event) { // Play all the plugin objects when the user clicks on one PluginHelper.playAllPlugins(this, event); }).bind(this), true); this._pluginOverlayShowing = true; break; } case "pagehide": { // Check to make sure it's top-level pagehide if (aEvent.target.defaultView == this.browser.contentWindow) { // Reset plugin state when we leave the page this._pluginCount = 0; this._pluginOverlayShowing = false; } break; } } }, onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) { let contentWin = aWebProgress.DOMWindow; if (contentWin != contentWin.top) return; if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) { // Filter optimization: Only really send NETWORK state changes to Java listener let browser = BrowserApp.getBrowserForWindow(aWebProgress.DOMWindow); let uri = ""; if (browser) uri = browser.currentURI.spec; // Check to see if we restoring the content from a previous presentation (session) // since there should be no real network activity let restoring = aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING; let showProgress = restoring ? false : this.showProgress; // true if the page loaded successfully (i.e., no 404s or other errors) let success = false; try { success = aRequest.QueryInterface(Components.interfaces.nsIHttpChannel).requestSucceeded; } catch (e) { } let message = { gecko: { type: "Content:StateChange", tabID: this.id, uri: uri, state: aStateFlags, showProgress: showProgress, success: success } }; sendMessageToJava(message); // Reset showProgress after state change this.showProgress = true; } }, onLocationChange: function(aWebProgress, aRequest, aLocationURI, aFlags) { let contentWin = aWebProgress.DOMWindow; if (contentWin != contentWin.top) return; let browser = BrowserApp.getBrowserForWindow(contentWin); let uri = browser.currentURI.spec; let documentURI = ""; let contentType = ""; if (browser.contentDocument) { documentURI = browser.contentDocument.documentURIObject.spec; contentType = browser.contentDocument.contentType; } let message = { gecko: { type: "Content:LocationChange", tabID: this.id, uri: uri, documentURI: documentURI, contentType: contentType } }; sendMessageToJava(message); if ((aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) == 0) { // XXX This code assumes that this is the earliest hook we have at which // browser.contentDocument is changed to the new document we're loading this.contentDocumentIsDisplayed = false; } else { this.sendViewportUpdate(); } }, onSecurityChange: function(aWebProgress, aRequest, aState) { let mode = "unknown"; if (aState & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL) mode = "identified"; else if (aState & Ci.nsIWebProgressListener.STATE_SECURE_HIGH) mode = "verified"; else if (aState & Ci.nsIWebProgressListener.STATE_IS_BROKEN) mode = "mixed"; else mode = "unknown"; let message = { gecko: { type: "Content:SecurityChange", tabID: this.id, mode: mode } }; sendMessageToJava(message); }, onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) { }, onStatusChange: function(aBrowser, aWebProgress, aRequest, aStatus, aMessage) { }, _sendHistoryEvent: function(aMessage, aIndex, aUri) { let message = { gecko: { type: "SessionHistory:" + aMessage, tabID: this.id, } }; if (aIndex != -1) { message.gecko.index = aIndex; } if (aUri != null) { message.gecko.uri = aUri; } sendMessageToJava(message); }, OnHistoryNewEntry: function(aUri) { this._sendHistoryEvent("New", -1, aUri.spec); }, OnHistoryGoBack: function(aUri) { this._sendHistoryEvent("Back", -1, null); return true; }, OnHistoryGoForward: function(aUri) { this._sendHistoryEvent("Forward", -1, null); return true; }, OnHistoryReload: function(aUri, aFlags) { // we don't do anything with this, so don't propagate it // for now anyway return true; }, OnHistoryGotoIndex: function(aIndex, aUri) { this._sendHistoryEvent("Goto", aIndex, null); return true; }, OnHistoryPurge: function(aNumEntries) { this._sendHistoryEvent("Purge", -1, null); return true; }, get metadata() { return ViewportHandler.getMetadataForDocument(this.browser.contentDocument); }, /** Update viewport when the metadata changes. */ updateViewportMetadata: function updateViewportMetadata(aMetadata) { if (aMetadata && aMetadata.autoScale) { let scaleRatio = aMetadata.scaleRatio = ViewportHandler.getScaleRatio(); if ("defaultZoom" in aMetadata && aMetadata.defaultZoom > 0) aMetadata.defaultZoom *= scaleRatio; if ("minZoom" in aMetadata && aMetadata.minZoom > 0) aMetadata.minZoom *= scaleRatio; if ("maxZoom" in aMetadata && aMetadata.maxZoom > 0) aMetadata.maxZoom *= scaleRatio; } ViewportHandler.setMetadataForDocument(this.browser.contentDocument, aMetadata); this.updateViewportSize(gScreenWidth); }, /** Update viewport when the metadata or the window size changes. */ updateViewportSize: function updateViewportSize(aOldScreenWidth) { // When this function gets called on window resize, we must execute // this.sendViewportUpdate() so that refreshDisplayPort is called. // Ensure that when making changes to this function that code path // is not accidentally removed (the call to sendViewportUpdate() is // at the very end). let browser = this.browser; if (!browser) return; let screenW = gScreenWidth; let screenH = gScreenHeight; let viewportW, viewportH; let metadata = this.metadata; if (metadata.autoSize) { if ("scaleRatio" in metadata) { viewportW = screenW / metadata.scaleRatio; viewportH = screenH / metadata.scaleRatio; } else { viewportW = screenW; viewportH = screenH; } } else { viewportW = metadata.width; viewportH = metadata.height; // If (scale * width) < device-width, increase the width (bug 561413). let maxInitialZoom = metadata.defaultZoom || metadata.maxZoom; if (maxInitialZoom && viewportW) viewportW = Math.max(viewportW, screenW / maxInitialZoom); let validW = viewportW > 0; let validH = viewportH > 0; if (!validW) viewportW = validH ? (viewportH * (screenW / screenH)) : BrowserApp.defaultBrowserWidth; if (!validH) viewportH = viewportW * (screenH / screenW); } // Make sure the viewport height is not shorter than the window when // the page is zoomed out to show its full width. let minScale = this.getPageZoomLevel(); viewportH = Math.max(viewportH, screenH / minScale); let oldBrowserWidth = this.browserWidth; this.setBrowserSize(viewportW, viewportH); // Avoid having the scroll position jump around after device rotation. let win = this.browser.contentWindow; this.userScrollPos.x = win.scrollX; this.userScrollPos.y = win.scrollY; // This change to the zoom accounts for all types of changes I can conceive: // 1. screen size changes, CSS viewport does not (pages with no meta viewport // or a fixed size viewport) // 2. screen size changes, CSS viewport also does (pages with a device-width // viewport) // 3. screen size remains constant, but CSS viewport changes (meta viewport // tag is added or removed) // 4. neither screen size nor CSS viewport changes // // In all of these cases, we maintain how much actual content is visible // within the screen width. Note that "actual content" may be different // with respect to CSS pixels because of the CSS viewport size changing. let zoomScale = (screenW * oldBrowserWidth) / (aOldScreenWidth * viewportW); this.setResolution(this._zoom * zoomScale, false); this.sendViewportUpdate(); }, getPageZoomLevel: function getPageZoomLevel() { // This may get called during a Viewport:Change message while the document // has not loaded yet. if (!this.browser.contentDocument || !this.browser.contentDocument.body) return 1.0; return gScreenWidth / this.browser.contentDocument.body.clientWidth; }, setBrowserSize: function(aWidth, aHeight) { this.browserWidth = aWidth; if (!this.browser.contentWindow) return; let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); cwu.setCSSViewport(aWidth, aHeight); }, getRequestLoadContext: function(aRequest) { if (aRequest && aRequest.notificationCallbacks) { try { return aRequest.notificationCallbacks.getInterface(Ci.nsILoadContext); } catch (ex) { } } if (aRequest && aRequest.loadGroup && aRequest.loadGroup.notificationCallbacks) { try { return aRequest.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext); } catch (ex) { } } return null; }, getWindowForRequest: function(aRequest) { let loadContext = this.getRequestLoadContext(aRequest); if (loadContext) return loadContext.associatedWindow; return null; }, observe: function(aSubject, aTopic, aData) { switch (aTopic) { case "before-first-paint": // Is it on the top level? let contentDocument = aSubject; if (contentDocument == this.browser.contentDocument) { // reset CSS viewport and zoom to default on new page this.setBrowserSize(kDefaultCSSViewportWidth, kDefaultCSSViewportHeight); this.setResolution(gScreenWidth / this.browserWidth, false); // and then use the metadata to figure out how it needs to be updated ViewportHandler.updateMetadata(this); // The document element must have a display port on it whenever we are about to // paint. This is the point just before the first paint, so we set the display port // to a default value here. Once Java is aware of this document it will overwrite // it with a better-calculated display port. this.setDisplayPort(0, 0, {left: 0, top: 0, right: gScreenWidth, bottom: gScreenHeight }); BrowserApp.displayedDocumentChanged(); this.contentDocumentIsDisplayed = true; } break; } }, // nsIBrowserTab get window() { if (!this.browser) return null; return this.browser.contentWindow; }, get scale() { return this._zoom; }, QueryInterface: XPCOMUtils.generateQI([ Ci.nsIWebProgressListener, Ci.nsISHistoryListener, Ci.nsIObserver, Ci.nsISupportsWeakReference, Ci.nsIBrowserTab ]) }; var BrowserEventHandler = { init: function init() { Services.obs.addObserver(this, "Gesture:SingleTap", false); Services.obs.addObserver(this, "Gesture:ShowPress", false); Services.obs.addObserver(this, "Gesture:CancelTouch", false); Services.obs.addObserver(this, "Gesture:DoubleTap", false); Services.obs.addObserver(this, "Gesture:Scroll", false); Services.obs.addObserver(this, "dom-touch-listener-added", false); BrowserApp.deck.addEventListener("DOMUpdatePageReport", PopupBlockerObserver.onUpdatePageReport, false); }, observe: function(aSubject, aTopic, aData) { if (aTopic == "dom-touch-listener-added") { let tab = BrowserApp.getTabForWindow(aSubject); if (!tab) return; sendMessageToJava({ gecko: { type: "Tab:HasTouchListener", tabID: tab.id } }); return; } // the remaining events are all dependent on the browser content document being the // same as the browser displayed document. if they are not the same, we should ignore // the event. if (!BrowserApp.isBrowserContentDocumentDisplayed()) return; if (aTopic == "Gesture:Scroll") { // If we've lost our scrollable element, return. Don't cancel the // override, as we probably don't want Java to handle panning until the // user releases their finger. if (this._scrollableElement == null) return; // If this is the first scroll event and we can't scroll in the direction // the user wanted, and neither can any non-root sub-frame, cancel the // override so that Java can handle panning the main document. let data = JSON.parse(aData); if (this._firstScrollEvent) { while (this._scrollableElement != null && !this._elementCanScroll(this._scrollableElement, data.x, data.y)) this._scrollableElement = this._findScrollableElement(this._scrollableElement, false); let doc = BrowserApp.selectedBrowser.contentDocument; if (this._scrollableElement == doc.body || this._scrollableElement == doc.documentElement) { sendMessageToJava({ gecko: { type: "Panning:CancelOverride" } }); return; } this._firstScrollEvent = false; } // Scroll the scrollable element if (this._elementCanScroll(this._scrollableElement, data.x, data.y)) { this._scrollElementBy(this._scrollableElement, data.x, data.y); sendMessageToJava({ gecko: { type: "Gesture:ScrollAck", scrolled: true } }); } else { sendMessageToJava({ gecko: { type: "Gesture:ScrollAck", scrolled: false } }); } } else if (aTopic == "Gesture:CancelTouch") { this._cancelTapHighlight(); } else if (aTopic == "Gesture:ShowPress") { let data = JSON.parse(aData); let closest = ElementTouchHelper.elementFromPoint(BrowserApp.selectedBrowser.contentWindow, data.x, data.y); if (!closest) closest = ElementTouchHelper.anyElementFromPoint(BrowserApp.selectedBrowser.contentWindow, data.x, data.y); if (closest) { this._doTapHighlight(closest); // If we've pressed a scrollable element, let Java know that we may // want to override the scroll behaviour (for document sub-frames) this._scrollableElement = this._findScrollableElement(closest, true); this._firstScrollEvent = true; if (this._scrollableElement != null) { // Discard if it's the top-level scrollable, we let Java handle this let doc = BrowserApp.selectedBrowser.contentDocument; if (this._scrollableElement != doc.body && this._scrollableElement != doc.documentElement) sendMessageToJava({ gecko: { type: "Panning:Override" } }); } } } else if (aTopic == "Gesture:SingleTap") { let element = this._highlightElement; if (element && !SelectHelper.handleClick(element)) { try { let data = JSON.parse(aData); this._sendMouseEvent("mousemove", element, data.x, data.y); this._sendMouseEvent("mousedown", element, data.x, data.y); this._sendMouseEvent("mouseup", element, data.x, data.y); if (ElementTouchHelper.isElementClickable(element)) Haptic.performSimpleAction(Haptic.LongPress); } catch(e) { Cu.reportError(e); } } this._cancelTapHighlight(); } else if (aTopic == "Gesture:DoubleTap") { this._cancelTapHighlight(); this.onDoubleTap(aData); } }, _zoomOut: function() { sendMessageToJava({ gecko: { type: "Browser:ZoomToPageWidth"} }); }, onDoubleTap: function(aData) { let data = JSON.parse(aData); let win = BrowserApp.selectedBrowser.contentWindow; let zoom = BrowserApp.selectedTab._zoom; let element = ElementTouchHelper.anyElementFromPoint(win, data.x, data.y); if (!element) { this._zoomOut(); return; } win = element.ownerDocument.defaultView; while (element && win.getComputedStyle(element,null).display == "inline") element = element.parentNode; if (!element) { this._zoomOut(); } else { const margin = 15; const minDifference = -20; const maxDifference = 20; let rect = ElementTouchHelper.getBoundingContentRect(element); let viewport = BrowserApp.selectedTab.getViewport(); let vRect = new Rect(viewport.x, viewport.y, viewport.width, viewport.height); let zoom = viewport.zoom; let bRect = new Rect(Math.max(0,rect.x - margin), rect.y, rect.w + 2*margin, rect.h); // constrict the rect to the screen width bRect.width = Math.min(bRect.width, viewport.pageWidth/zoom - bRect.x); bRect.scale(zoom, zoom); let overlap = vRect.intersect(bRect); let overlapArea = overlap.width*overlap.height; // we want to know if the area of the element showing is near the max we can show // on the screen at any time and if its already stretching the width of the screen let availHeight = Math.min(bRect.width*vRect.height/vRect.width, bRect.height); let showing = overlapArea/(bRect.width*availHeight); let dw = (bRect.width - vRect.width)/zoom; let dx = (bRect.x - vRect.x)/zoom; if (showing > 0.9 && dx > minDifference && dx < maxDifference && dw > minDifference && dw < maxDifference) { this._zoomOut(); return; } rect.type = "Browser:ZoomToRect"; rect.x = bRect.x; rect.y = bRect.y; rect.w = bRect.width; rect.h = availHeight; sendMessageToJava({ gecko: rect }); } }, _firstScrollEvent: false, _scrollableElement: null, _highlightElement: null, _doTapHighlight: function _doTapHighlight(aElement) { DOMUtils.setContentState(aElement, kStateActive); this._highlightElement = aElement; }, _cancelTapHighlight: function _cancelTapHighlight() { DOMUtils.setContentState(BrowserApp.selectedBrowser.contentWindow.document.documentElement, kStateActive); this._highlightElement = null; }, _updateLastPosition: function(x, y, dx, dy) { this.lastX = x; this.lastY = y; this.lastTime = Date.now(); this.motionBuffer.push({ dx: dx, dy: dy, time: this.lastTime }); }, _sendMouseEvent: function _sendMouseEvent(aName, aElement, aX, aY, aButton) { // the element can be out of the aX/aY point because of the touch radius // if outside, we gracefully move the touch point to the center of the element if (!(aElement instanceof HTMLHtmlElement)) { let isTouchClick = true; let rects = ElementTouchHelper.getContentClientRects(aElement); for (let i = 0; i < rects.length; i++) { let rect = rects[i]; // We might be able to deal with fractional pixels, but mouse events won't. // Deflate the bounds in by 1 pixel to deal with any fractional scroll offset issues. let inBounds = (aX > rect.left + 1 && aX < (rect.left + rect.width - 1)) && (aY > rect.top + 1 && aY < (rect.top + rect.height - 1)); if (inBounds) { isTouchClick = false; break; } } if (isTouchClick) { let rect = {x: rects[0].left, y: rects[0].top, w: rects[0].width, h: rects[0].height}; if (rect.w == 0 && rect.h == 0) return; let point = { x: rect.x + rect.w/2, y: rect.y + rect.h/2 }; aX = point.x; aY = point.y; } } let window = aElement.ownerDocument.defaultView; try { let cwu = window.top.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); aButton = aButton || 0; cwu.sendMouseEventToWindow(aName, Math.round(aX), Math.round(aY), aButton, 1, 0, true); } catch(e) { Cu.reportError(e); } }, _hasScrollableOverflow: function(elem) { var win = elem.ownerDocument.defaultView; if (!win) return false; var computedStyle = win.getComputedStyle(elem); if (!computedStyle) return false; return computedStyle.overflow == 'auto' || computedStyle.overflow == 'scroll'; }, _findScrollableElement: function(elem, checkElem) { // Walk the DOM tree until we find a scrollable element let scrollable = false; while (elem) { /* Element is scrollable if its scroll-size exceeds its client size, and: * - It has overflow 'auto' or 'scroll' * - It's a textarea * - It's an HTML/BODY node * - It's a select element showing multiple rows */ if (checkElem) { if (((elem.scrollHeight > elem.clientHeight) || (elem.scrollWidth > elem.clientWidth)) && (this._hasScrollableOverflow(elem) || elem.mozMatchesSelector("html, body, textarea")) || (elem instanceof HTMLSelectElement && (elem.size > 1 || elem.multiple))) { scrollable = true; break; } } else { checkElem = true; } // Propagate up iFrames if (!elem.parentNode && elem.documentElement && elem.documentElement.ownerDocument) elem = elem.documentElement.ownerDocument.defaultView.frameElement; else elem = elem.parentNode; } if (!scrollable) return null; return elem; }, _elementReceivesInput: function(aElement) { return aElement instanceof Element && kElementsReceivingInput.hasOwnProperty(aElement.tagName.toLowerCase()) || this._isEditable(aElement); }, _isEditable: function(aElement) { let canEdit = false; if (aElement.isContentEditable || aElement.designMode == "on") { canEdit = true; } else if (aElement instanceof HTMLIFrameElement && (aElement.contentDocument.body.isContentEditable || aElement.contentDocument.designMode == "on")) { canEdit = true; } else { canEdit = aElement.ownerDocument && aElement.ownerDocument.designMode == "on"; } return canEdit; }, _scrollElementBy: function(elem, x, y) { elem.scrollTop = elem.scrollTop + y; elem.scrollLeft = elem.scrollLeft + x; }, _elementCanScroll: function(elem, x, y) { let scrollX = true; let scrollY = true; if (x < 0) { if (elem.scrollLeft <= 0) { scrollX = false; } } else if (elem.scrollLeft >= (elem.scrollWidth - elem.clientWidth)) { scrollX = false; } if (y < 0) { if (elem.scrollTop <= 0) { scrollY = false; } } else if (elem.scrollTop >= (elem.scrollHeight - elem.clientHeight)) { scrollY = false; } return scrollX || scrollY; } }; const kReferenceDpi = 240; // standard "pixel" size used in some preferences const ElementTouchHelper = { anyElementFromPoint: function(aWindow, aX, aY) { let cwu = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); let elem = cwu.elementFromPoint(aX, aY, false, true); while (elem && (elem instanceof HTMLIFrameElement || elem instanceof HTMLFrameElement)) { let rect = elem.getBoundingClientRect(); aX -= rect.left; aY -= rect.top; cwu = elem.contentDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); elem = cwu.elementFromPoint(aX, aY, false, true); } return elem; }, elementFromPoint: function(aWindow, aX, aY) { // browser's elementFromPoint expect browser-relative client coordinates. // subtract browser's scroll values to adjust let cwu = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); let elem = this.getClosest(cwu, aX, aY); // step through layers of IFRAMEs and FRAMES to find innermost element while (elem && (elem instanceof HTMLIFrameElement || elem instanceof HTMLFrameElement)) { // adjust client coordinates' origin to be top left of iframe viewport let rect = elem.getBoundingClientRect(); aX -= rect.left; aY -= rect.top; cwu = elem.contentDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); elem = ElementTouchHelper.getClosest(cwu, aX, aY); } return elem; }, get radius() { let prefs = Services.prefs; delete this.radius; return this.radius = { "top": prefs.getIntPref("browser.ui.touch.top"), "right": prefs.getIntPref("browser.ui.touch.right"), "bottom": prefs.getIntPref("browser.ui.touch.bottom"), "left": prefs.getIntPref("browser.ui.touch.left") }; }, get weight() { delete this.weight; return this.weight = { "visited": Services.prefs.getIntPref("browser.ui.touch.weight.visited") }; }, /* Retrieve the closest element to a point by looking at borders position */ getClosest: function getClosest(aWindowUtils, aX, aY) { if (!this.dpiRatio) this.dpiRatio = aWindowUtils.displayDPI / kReferenceDpi; let dpiRatio = this.dpiRatio; let target = aWindowUtils.elementFromPoint(aX, aY, true, /* ignore root scroll frame*/ false); /* don't flush layout */ // if this element is clickable we return quickly if (this.isElementClickable(target)) return target; let target = null; let nodes = aWindowUtils.nodesFromRect(aX, aY, this.radius.top * dpiRatio, this.radius.right * dpiRatio, this.radius.bottom * dpiRatio, this.radius.left * dpiRatio, true, false); let threshold = Number.POSITIVE_INFINITY; for (let i = 0; i < nodes.length; i++) { let current = nodes[i]; if (!current.mozMatchesSelector || !this.isElementClickable(current)) continue; let rect = current.getBoundingClientRect(); let distance = this._computeDistanceFromRect(aX, aY, rect); // increase a little bit the weight for already visited items if (current && current.mozMatchesSelector("*:visited")) distance *= (this.weight.visited / 100); if (distance < threshold) { target = current; threshold = distance; } } return target; }, isElementClickable: function isElementClickable(aElement) { const selector = "a,:link,:visited,[role=button],button,input,select,textarea,label"; for (let elem = aElement; elem; elem = elem.parentNode) { if (this._hasMouseListener(elem)) return true; if (elem.mozMatchesSelector && elem.mozMatchesSelector(selector)) return true; } return false; }, _computeDistanceFromRect: function _computeDistanceFromRect(aX, aY, aRect) { let x = 0, y = 0; let xmost = aRect.left + aRect.width; let ymost = aRect.top + aRect.height; // compute horizontal distance from left/right border depending if X is // before/inside/after the element's rectangle if (aRect.left < aX && aX < xmost) x = Math.min(xmost - aX, aX - aRect.left); else if (aX < aRect.left) x = aRect.left - aX; else if (aX > xmost) x = aX - xmost; // compute vertical distance from top/bottom border depending if Y is // above/inside/below the element's rectangle if (aRect.top < aY && aY < ymost) y = Math.min(ymost - aY, aY - aRect.top); else if (aY < aRect.top) y = aRect.top - aY; if (aY > ymost) y = aY - ymost; return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); }, _els: Cc["@mozilla.org/eventlistenerservice;1"].getService(Ci.nsIEventListenerService), _clickableEvents: ["mousedown", "mouseup", "click"], _hasMouseListener: function _hasMouseListener(aElement) { let els = this._els; let listeners = els.getListenerInfoFor(aElement, {}); for (let i = 0; i < listeners.length; i++) { if (this._clickableEvents.indexOf(listeners[i].type) != -1) return true; } return false; }, getContentClientRects: function(aElement) { let offset = { x: 0, y: 0 }; let nativeRects = aElement.getClientRects(); // step out of iframes and frames, offsetting scroll values for (let frame = aElement.ownerDocument.defaultView; frame.frameElement; frame = frame.parent) { // adjust client coordinates' origin to be top left of iframe viewport let rect = frame.frameElement.getBoundingClientRect(); let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth; let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth; offset.x += rect.left + parseInt(left); offset.y += rect.top + parseInt(top); } let result = []; for (let i = nativeRects.length - 1; i >= 0; i--) { let r = nativeRects[i]; result.push({ left: r.left + offset.x, top: r.top + offset.y, width: r.width, height: r.height }); } return result; }, getBoundingContentRect: function(aElement) { if (!aElement) return {x: 0, y: 0, w: 0, h: 0}; let document = aElement.ownerDocument; while (document.defaultView.frameElement) document = document.defaultView.frameElement.ownerDocument; let cwu = document.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); let scrollX = {}, scrollY = {}; cwu.getScrollXY(false, scrollX, scrollY); let r = aElement.getBoundingClientRect(); // step out of iframes and frames, offsetting scroll values for (let frame = aElement.ownerDocument.defaultView; frame.frameElement && frame != content; frame = frame.parent) { // adjust client coordinates' origin to be top left of iframe viewport let rect = frame.frameElement.getBoundingClientRect(); let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth; let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth; scrollX.value += rect.left + parseInt(left); scrollY.value += rect.top + parseInt(top); } return {x: r.left + scrollX.value, y: r.top + scrollY.value, w: r.width, h: r.height }; } }; var ErrorPageEventHandler = { handleEvent: function(aEvent) { switch (aEvent.type) { case "click": { // Don't trust synthetic events if (!aEvent.isTrusted) return; let target = aEvent.originalTarget; let errorDoc = target.ownerDocument; // If the event came from an ssl error page, it is probably either the "Add // Exception…" or "Get me out of here!" button if (/^about:certerror\?e=nssBadCert/.test(errorDoc.documentURI)) { let perm = errorDoc.getElementById("permanentExceptionButton"); let temp = errorDoc.getElementById("temporaryExceptionButton"); if (target == temp || target == perm) { // Handle setting an cert exception and reloading the page try { // Add a new SSL exception for this URL let uri = Services.io.newURI(errorDoc.location.href, null, null); let sslExceptions = new SSLExceptions(); if (target == perm) sslExceptions.addPermanentException(uri); else sslExceptions.addTemporaryException(uri); } catch (e) { dump("Failed to set cert exception: " + e + "\n"); } errorDoc.location.reload(); } else if (target == errorDoc.getElementById("getMeOutOfHereButton")) { errorDoc.location = "about:home"; } } break; } } } }; var FormAssistant = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver]), // Used to keep track of the element that corresponds to the current // autocomplete suggestions _currentInputElement: null, // Keep track of whether or not an invalid form has been submitted _invalidSubmit: false, init: function() { Services.obs.addObserver(this, "FormAssist:AutoComplete", false); Services.obs.addObserver(this, "FormAssist:Hidden", false); Services.obs.addObserver(this, "invalidformsubmit", false); // We need to use a capturing listener for focus events BrowserApp.deck.addEventListener("focus", this, true); BrowserApp.deck.addEventListener("input", this, false); BrowserApp.deck.addEventListener("pageshow", this, false); }, uninit: function() { Services.obs.removeObserver(this, "FormAssist:AutoComplete"); Services.obs.removeObserver(this, "FormAssist:Hidden"); Services.obs.removeObserver(this, "invalidformsubmit"); BrowserApp.deck.removeEventListener("focus", this); BrowserApp.deck.removeEventListener("input", this); BrowserApp.deck.removeEventListener("pageshow", this); }, observe: function(aSubject, aTopic, aData) { switch (aTopic) { case "FormAssist:AutoComplete": if (!this._currentInputElement) break; // Remove focus from the textbox to avoid some bad IME interactions this._currentInputElement.blur(); this._currentInputElement.value = aData; break; case "FormAssist:Hidden": this._currentInputElement = null; break; } }, notifyInvalidSubmit: function notifyInvalidSubmit(aFormElement, aInvalidElements) { if (!aInvalidElements.length) return; // Ignore this notificaiton if the current tab doesn't contain the invalid form if (BrowserApp.selectedBrowser.contentDocument != aFormElement.ownerDocument.defaultView.top.document) return; this._invalidSubmit = true; // Our focus listener will show the element's validation message let currentElement = aInvalidElements.queryElementAt(0, Ci.nsISupports); currentElement.focus(); }, handleEvent: function(aEvent) { switch (aEvent.type) { case "focus": let currentElement = aEvent.target; // Prioritize a form validation message over autocomplete suggestions // when the element is first focused (a form validation message will // only be available if an invalid form was submitted) if (this._showValidationMessage(currentElement)) break; this._showAutoCompleteSuggestions(currentElement) break; case "input": currentElement = aEvent.target; // Since we can only show one popup at a time, prioritze autocomplete // suggestions over a form validation message if (this._showAutoCompleteSuggestions(currentElement)) break; if (this._showValidationMessage(currentElement)) break; // If we're not showing autocomplete suggestions, hide the form assist popup this._hideFormAssistPopup(); break; // Reset invalid submit state on each pageshow case "pageshow": let target = aEvent.originalTarget; if (target == content.document || target.ownerDocument == content.document) this._invalidSubmit = false; } }, // We only want to show autocomplete suggestions for certain elements _isAutoComplete: function _isAutoComplete(aElement) { if (!(aElement instanceof HTMLInputElement) || (aElement.getAttribute("type") == "password") || (aElement.hasAttribute("autocomplete") && aElement.getAttribute("autocomplete").toLowerCase() == "off")) return false; return true; }, // Retrieves autocomplete suggestions for an element from the form autocomplete service. _getAutoCompleteSuggestions: function _getAutoCompleteSuggestions(aSearchString, aElement) { // Cache the form autocomplete service for future use if (!this._formAutoCompleteService) this._formAutoCompleteService = Cc["@mozilla.org/satchel/form-autocomplete;1"]. getService(Ci.nsIFormAutoComplete); let results = this._formAutoCompleteService.autoCompleteSearch(aElement.name || aElement.id, aSearchString, aElement, null); let suggestions = []; for (let i = 0; i < results.matchCount; i++) { let value = results.getValueAt(i); // Do not show the value if it is the current one in the input field if (value == aSearchString) continue; // Supply a label and value, since they can differ for datalist suggestions suggestions.push({ label: value, value: value }); } return suggestions; }, /** * (Copied from mobile/xul/chrome/content/forms.js) * This function is similar to getListSuggestions from * components/satchel/src/nsInputListAutoComplete.js but sadly this one is * used by the autocomplete.xml binding which is not in used in fennec */ _getListSuggestions: function _getListSuggestions(aElement) { if (!(aElement instanceof HTMLInputElement) || !aElement.list) return []; let suggestions = []; let filter = !aElement.hasAttribute("mozNoFilter"); let lowerFieldValue = aElement.value.toLowerCase(); let options = aElement.list.options; let length = options.length; for (let i = 0; i < length; i++) { let item = options.item(i); let label = item.value; if (item.label) label = item.label; else if (item.text) label = item.text; if (filter && label.toLowerCase().indexOf(lowerFieldValue) == -1) continue; suggestions.push({ label: label, value: item.value }); } return suggestions; }, // Gets the element position data necessary for the Java UI to position // the form assist popup. _getElementPositionData: function _getElementPositionData(aElement) { let rect = ElementTouchHelper.getBoundingContentRect(aElement); let viewport = BrowserApp.selectedTab.getViewport(); return { rect: [rect.x - (viewport.x / viewport.zoom), rect.y - (viewport.y / viewport.zoom), rect.w, rect.h], zoom: viewport.zoom } }, // Retrieves autocomplete suggestions for an element from the form autocomplete service // and sends the suggestions to the Java UI, along with element position data. // Returns true if there are suggestions to show, false otherwise. _showAutoCompleteSuggestions: function _showAutoCompleteSuggestions(aElement) { if (!this._isAutoComplete(aElement)) return false; let autoCompleteSuggestions = this._getAutoCompleteSuggestions(aElement.value, aElement); let listSuggestions = this._getListSuggestions(aElement); // On desktop, we show datalist suggestions below autocomplete suggestions, // without duplicates removed. let suggestions = autoCompleteSuggestions.concat(listSuggestions); // Return false if there are no suggestions to show if (!suggestions.length) return false; let positionData = this._getElementPositionData(aElement); sendMessageToJava({ gecko: { type: "FormAssist:AutoComplete", suggestions: suggestions, rect: positionData.rect, zoom: positionData.zoom } }); // Keep track of input element so we can fill it in if the user // selects an autocomplete suggestion this._currentInputElement = aElement; return true; }, // Only show a validation message if the user submitted an invalid form, // there's a non-empty message string, and the element is the correct type _isValidateable: function _isValidateable(aElement) { if (!this._invalidSubmit || !aElement.validationMessage || !(aElement instanceof HTMLInputElement || aElement instanceof HTMLTextAreaElement || aElement instanceof HTMLSelectElement || aElement instanceof HTMLButtonElement)) return false; return true; }, // Sends a validation message and position data for an element to the Java UI. // Returns true if there's a validation message to show, false otherwise. _showValidationMessage: function _sendValidationMessage(aElement) { if (!this._isValidateable(aElement)) return false; let positionData = this._getElementPositionData(aElement); sendMessageToJava({ gecko: { type: "FormAssist:ValidationMessage", validationMessage: aElement.validationMessage, rect: positionData.rect, zoom: positionData.zoom } }); return true; }, _hideFormAssistPopup: function _hideFormAssistPopup() { sendMessageToJava({ gecko: { type: "FormAssist:Hide" } }); } }; var XPInstallObserver = { init: function xpi_init() { Services.obs.addObserver(XPInstallObserver, "addon-install-blocked", false); Services.obs.addObserver(XPInstallObserver, "addon-install-started", false); AddonManager.addInstallListener(XPInstallObserver); }, uninit: function xpi_uninit() { Services.obs.removeObserver(XPInstallObserver, "addon-install-blocked"); Services.obs.removeObserver(XPInstallObserver, "addon-install-started"); AddonManager.removeInstallListener(XPInstallObserver); }, observe: function xpi_observer(aSubject, aTopic, aData) { switch (aTopic) { case "addon-install-started": NativeWindow.toast.show(Strings.browser.GetStringFromName("alertAddonsDownloading"), "short"); break; case "addon-install-blocked": let installInfo = aSubject.QueryInterface(Ci.amIWebInstallInfo); let host = installInfo.originatingURI.host; let brandShortName = Strings.brand.GetStringFromName("brandShortName"); let notificationName, buttons, message; let strings = Strings.browser; let enabled = true; try { enabled = Services.prefs.getBoolPref("xpinstall.enabled"); } catch (e) {} if (!enabled) { notificationName = "xpinstall-disabled"; if (Services.prefs.prefIsLocked("xpinstall.enabled")) { message = strings.GetStringFromName("xpinstallDisabledMessageLocked"); buttons = []; } else { message = strings.formatStringFromName("xpinstallDisabledMessage2", [brandShortName, host], 2); buttons = [{ label: strings.GetStringFromName("xpinstallDisabledButton"), callback: function editPrefs() { Services.prefs.setBoolPref("xpinstall.enabled", true); return false; } }]; } } else { notificationName = "xpinstall"; message = strings.formatStringFromName("xpinstallPromptWarning2", [brandShortName, host], 2); buttons = [{ label: strings.GetStringFromName("xpinstallPromptAllowButton"), callback: function() { // Kick off the install installInfo.install(); return false; } }]; } NativeWindow.doorhanger.show(message, aTopic, buttons); break; } }, onInstallEnded: function(aInstall, aAddon) { let needsRestart = false; if (aInstall.existingAddon && (aInstall.existingAddon.pendingOperations & AddonManager.PENDING_UPGRADE)) needsRestart = true; else if (aAddon.pendingOperations & AddonManager.PENDING_INSTALL) needsRestart = true; if (needsRestart) { let buttons = [{ label: Strings.browser.GetStringFromName("notificationRestart.button"), callback: function() { // Notify all windows that an application quit has been requested let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool); Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart"); // If nothing aborted, quit the app if (cancelQuit.data == false) { let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup); appStartup.quit(Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit); } } }]; let message = Strings.browser.GetStringFromName("notificationRestart.normal"); NativeWindow.doorhanger.show(message, "addon-app-restart", buttons, BrowserApp.selectedTab.id, { persistence: -1 }); } else { let message = Strings.browser.GetStringFromName("alertAddonsInstalledNoRestart"); NativeWindow.toast.show(message, "short"); } }, onInstallFailed: function(aInstall) { NativeWindow.toast.show(Strings.browser.GetStringFromName("alertAddonsFail"), "short"); }, onDownloadProgress: function xpidm_onDownloadProgress(aInstall) {}, onDownloadFailed: function(aInstall) { this.onInstallFailed(aInstall); }, onDownloadCancelled: function(aInstall) { let host = (aInstall.originatingURI instanceof Ci.nsIStandardURL) && aInstall.originatingURI.host; if (!host) host = (aInstall.sourceURI instanceof Ci.nsIStandardURL) && aInstall.sourceURI.host; let error = (host || aInstall.error == 0) ? "addonError" : "addonLocalError"; if (aInstall.error != 0) error += aInstall.error; else if (aInstall.addon && aInstall.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) error += "Blocklisted"; else if (aInstall.addon && (!aInstall.addon.isCompatible || !aInstall.addon.isPlatformCompatible)) error += "Incompatible"; else return; // No need to show anything in this case. let msg = Strings.browser.GetStringFromName(error); // TODO: formatStringFromName msg = msg.replace("#1", aInstall.name); if (host) msg = msg.replace("#2", host); msg = msg.replace("#3", Strings.brand.GetStringFromName("brandShortName")); msg = msg.replace("#4", Services.appinfo.version); NativeWindow.toast.show(msg, "short"); } }; // Blindly copied from Safari documentation for now. const kViewportMinScale = 0; const kViewportMaxScale = 10; const kViewportMinWidth = 200; const kViewportMaxWidth = 10000; const kViewportMinHeight = 223; const kViewportMaxHeight = 10000; var ViewportHandler = { // The cached viewport metadata for each document. We tie viewport metadata to each document // instead of to each tab so that we don't have to update it when the document changes. Using an // ES6 weak map lets us avoid leaks. _metadata: new WeakMap(), init: function init() { addEventListener("DOMMetaAdded", this, false); addEventListener("resize", this, false); }, uninit: function uninit() { removeEventListener("DOMMetaAdded", this, false); removeEventListener("resize", this, false); }, handleEvent: function handleEvent(aEvent) { switch (aEvent.type) { case "DOMMetaAdded": let target = aEvent.originalTarget; if (target.name != "viewport") break; let document = target.ownerDocument; let browser = BrowserApp.getBrowserForDocument(document); let tab = BrowserApp.getTabForBrowser(browser); if (tab) this.updateMetadata(tab); break; case "resize": // guard against zero values corrupting our viewport numbers. this happens sometimes // during initialization. if (window.outerWidth == 0 || window.outerHeight == 0) break; // check dimensions changed to avoid infinite loop because updateViewportSize // triggers a resize on the content window and will trigger this listener again if (window.outerWidth == gScreenWidth && window.outerHeight == gScreenHeight) break; let oldScreenWidth = gScreenWidth; gScreenWidth = window.outerWidth; gScreenHeight = window.outerHeight; let tabs = BrowserApp.tabs; for (let i = 0; i < tabs.length; i++) tabs[i].updateViewportSize(oldScreenWidth); break; } }, resetMetadata: function resetMetadata(tab) { tab.updateViewportMetadata(null); }, updateMetadata: function updateMetadata(tab) { let metadata = this.getViewportMetadata(tab.browser.contentWindow); tab.updateViewportMetadata(metadata); }, /** * Returns an object with the page's preferred viewport properties: * defaultZoom (optional float): The initial scale when the page is loaded. * minZoom (optional float): The minimum zoom level. * maxZoom (optional float): The maximum zoom level. * width (optional int): The CSS viewport width in px. * height (optional int): The CSS viewport height in px. * autoSize (boolean): Resize the CSS viewport when the window resizes. * allowZoom (boolean): Let the user zoom in or out. * autoScale (boolean): Adjust the viewport properties to account for display density. */ getViewportMetadata: function getViewportMetadata(aWindow) { let doctype = aWindow.document.doctype; if (doctype && /(WAP|WML|Mobile)/.test(doctype.publicId)) return { defaultZoom: 1, autoSize: true, allowZoom: true, autoScale: true }; let windowUtils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); let handheldFriendly = windowUtils.getDocumentMetadata("HandheldFriendly"); if (handheldFriendly == "true") return { defaultZoom: 1, autoSize: true, allowZoom: true, autoScale: true }; if (aWindow.document instanceof XULDocument) return { defaultZoom: 1, autoSize: true, allowZoom: false, autoScale: false }; // viewport details found here // http://developer.apple.com/safari/library/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html // http://developer.apple.com/safari/library/documentation/AppleApplications/Reference/SafariWebContent/UsingtheViewport/UsingtheViewport.html // Note: These values will be NaN if parseFloat or parseInt doesn't find a number. // Remember that NaN is contagious: Math.max(1, NaN) == Math.min(1, NaN) == NaN. let scale = parseFloat(windowUtils.getDocumentMetadata("viewport-initial-scale")); let minScale = parseFloat(windowUtils.getDocumentMetadata("viewport-minimum-scale")); let maxScale = parseFloat(windowUtils.getDocumentMetadata("viewport-maximum-scale")); let widthStr = windowUtils.getDocumentMetadata("viewport-width"); let heightStr = windowUtils.getDocumentMetadata("viewport-height"); let width = this.clamp(parseInt(widthStr), kViewportMinWidth, kViewportMaxWidth); let height = this.clamp(parseInt(heightStr), kViewportMinHeight, kViewportMaxHeight); let allowZoomStr = windowUtils.getDocumentMetadata("viewport-user-scalable"); let allowZoom = !/^(0|no|false)$/.test(allowZoomStr); // WebKit allows 0, "no", or "false" scale = this.clamp(scale, kViewportMinScale, kViewportMaxScale); minScale = this.clamp(minScale, kViewportMinScale, kViewportMaxScale); maxScale = this.clamp(maxScale, kViewportMinScale, kViewportMaxScale); // If initial scale is 1.0 and width is not set, assume width=device-width let autoSize = (widthStr == "device-width" || (!widthStr && (heightStr == "device-height" || scale == 1.0))); return { defaultZoom: scale, minZoom: minScale, maxZoom: maxScale, width: width, height: height, autoSize: autoSize, allowZoom: allowZoom, autoScale: true }; }, clamp: function(num, min, max) { return Math.max(min, Math.min(max, num)); }, // The device-pixel-to-CSS-px ratio used to adjust meta viewport values. // This is higher on higher-dpi displays, so pages stay about the same physical size. getScaleRatio: function getScaleRatio() { let prefValue = Services.prefs.getIntPref("browser.viewport.scaleRatio"); if (prefValue > 0) return prefValue / 100; let dpi = this.displayDPI; if (dpi < 200) // Includes desktop displays, and LDPI and MDPI Android devices return 1; else if (dpi < 300) // Includes Nokia N900, and HDPI Android devices return 1.5; // For very high-density displays like the iPhone 4, calculate an integer ratio. return Math.floor(dpi / 150); }, get displayDPI() { let utils = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); delete this.displayDPI; return this.displayDPI = utils.displayDPI; }, /** * Returns the viewport metadata for the given document, or the default metrics if no viewport * metadata is available for that document. */ getMetadataForDocument: function getMetadataForDocument(aDocument) { let metadata = this._metadata.get(aDocument, this.getDefaultMetadata()); return metadata; }, /** Updates the saved viewport metadata for the given content document. */ setMetadataForDocument: function setMetadataForDocument(aDocument, aMetadata) { if (!aMetadata) this._metadata.delete(aDocument); else this._metadata.set(aDocument, aMetadata); }, /** Returns the default viewport metadata for a document. */ getDefaultMetadata: function getDefaultMetadata() { return { autoSize: false, allowZoom: true, autoScale: true, scaleRatio: ViewportHandler.getScaleRatio() }; } }; /** * Handler for blocked popups, triggered by DOMUpdatePageReport events in browser.xml */ var PopupBlockerObserver = { onUpdatePageReport: function onUpdatePageReport(aEvent) { let browser = BrowserApp.selectedBrowser; if (aEvent.originalTarget != browser) return; if (!browser.pageReport) return; let result = Services.perms.testExactPermission(BrowserApp.selectedBrowser.currentURI, "popup"); if (result == Ci.nsIPermissionManager.DENY_ACTION) return; // Only show the notification again if we've not already shown it. Since // notifications are per-browser, we don't need to worry about re-adding // it. if (!browser.pageReport.reported) { if (Services.prefs.getBoolPref("privacy.popups.showBrowserMessage")) { let brandShortName = Strings.brand.GetStringFromName("brandShortName"); let message; let popupCount = browser.pageReport.length; let strings = Strings.browser; if (popupCount > 1) message = strings.formatStringFromName("popupWarningMultiple", [brandShortName, popupCount], 2); else message = strings.formatStringFromName("popupWarning", [brandShortName], 1); let buttons = [ { label: strings.GetStringFromName("popupButtonAllowOnce"), callback: function() { PopupBlockerObserver.showPopupsForSite(); } }, { label: strings.GetStringFromName("popupButtonAlwaysAllow2"), callback: function() { PopupBlockerObserver.allowPopupsForSite(true); } }, { label: strings.GetStringFromName("popupButtonNeverWarn2"), callback: function() { PopupBlockerObserver.allowPopupsForSite(false); } } ]; NativeWindow.doorhanger.show(message, "popup-blocked", buttons); } // Record the fact that we've reported this blocked popup, so we don't // show it again. browser.pageReport.reported = true; } }, allowPopupsForSite: function allowPopupsForSite(aAllow) { let currentURI = BrowserApp.selectedBrowser.currentURI; Services.perms.add(currentURI, "popup", aAllow ? Ci.nsIPermissionManager.ALLOW_ACTION : Ci.nsIPermissionManager.DENY_ACTION); dump("Allowing popups for: " + currentURI); }, showPopupsForSite: function showPopupsForSite() { let uri = BrowserApp.selectedBrowser.currentURI; let pageReport = BrowserApp.selectedBrowser.pageReport; if (pageReport) { for (let i = 0; i < pageReport.length; ++i) { let popupURIspec = pageReport[i].popupWindowURI.spec; // Sometimes the popup URI that we get back from the pageReport // isn't useful (for instance, netscape.com's popup URI ends up // being "http://www.netscape.com", which isn't really the URI of // the popup they're trying to show). This isn't going to be // useful to the user, so we won't create a menu item for it. if (popupURIspec == "" || popupURIspec == "about:blank" || popupURIspec == uri.spec) continue; let popupFeatures = pageReport[i].popupWindowFeatures; let popupName = pageReport[i].popupWindowName; let parent = BrowserApp.selectedTab; BrowserApp.addTab(popupURIspec, { parentId: parent.id }); } } } }; var OfflineApps = { init: function() { BrowserApp.deck.addEventListener("MozApplicationManifest", this, false); }, uninit: function() { BrowserApp.deck.removeEventListener("MozApplicationManifest", this, false); }, handleEvent: function(aEvent) { if (aEvent.type == "MozApplicationManifest") this.offlineAppRequested(aEvent.originalTarget.defaultView); }, offlineAppRequested: function(aContentWindow) { if (!Services.prefs.getBoolPref("browser.offline-apps.notify")) return; let tab = BrowserApp.getTabForWindow(aContentWindow); let currentURI = aContentWindow.document.documentURIObject; // Don't bother showing UI if the user has already made a decision if (Services.perms.testExactPermission(currentURI, "offline-app") != Services.perms.UNKNOWN_ACTION) return; try { if (Services.prefs.getBoolPref("offline-apps.allow_by_default")) { // All pages can use offline capabilities, no need to ask the user return; } } catch(e) { // This pref isn't set by default, ignore failures } let host = currentURI.asciiHost; let notificationID = "offline-app-requested-" + host; let strings = Strings.browser; let buttons = [{ label: strings.GetStringFromName("offlineApps.allow"), callback: function() { OfflineApps.allowSite(aContentWindow.document); } }, { label: strings.GetStringFromName("offlineApps.never"), callback: function() { OfflineApps.disallowSite(aContentWindow.document); } }, { label: strings.GetStringFromName("offlineApps.notNow"), callback: function() { /* noop */ } }]; let message = strings.formatStringFromName("offlineApps.available2", [host], 1); NativeWindow.doorhanger.show(message, notificationID, buttons, tab.id); }, allowSite: function(aDocument) { Services.perms.add(aDocument.documentURIObject, "offline-app", Services.perms.ALLOW_ACTION); // When a site is enabled while loading, manifest resources will // start fetching immediately. This one time we need to do it // ourselves. this._startFetching(aDocument); }, disallowSite: function(aDocument) { Services.perms.add(aDocument.documentURIObject, "offline-app", Services.perms.DENY_ACTION); }, _startFetching: function(aDocument) { if (!aDocument.documentElement) return; let manifest = aDocument.documentElement.getAttribute("manifest"); if (!manifest) return; let manifestURI = Services.io.newURI(manifest, aDocument.characterSet, aDocument.documentURIObject); let updateService = Cc["@mozilla.org/offlinecacheupdate-service;1"].getService(Ci.nsIOfflineCacheUpdateService); updateService.scheduleUpdate(manifestURI, aDocument.documentURIObject, window); } }; var IndexedDB = { _permissionsPrompt: "indexedDB-permissions-prompt", _permissionsResponse: "indexedDB-permissions-response", _quotaPrompt: "indexedDB-quota-prompt", _quotaResponse: "indexedDB-quota-response", _quotaCancel: "indexedDB-quota-cancel", init: function IndexedDB_init() { Services.obs.addObserver(this, this._permissionsPrompt, false); Services.obs.addObserver(this, this._quotaPrompt, false); Services.obs.addObserver(this, this._quotaCancel, false); }, uninit: function IndexedDB_uninit() { Services.obs.removeObserver(this, this._permissionsPrompt, false); Services.obs.removeObserver(this, this._quotaPrompt, false); Services.obs.removeObserver(this, this._quotaCancel, false); }, observe: function IndexedDB_observe(subject, topic, data) { if (topic != this._permissionsPrompt && topic != this._quotaPrompt && topic != this._quotaCancel) { throw new Error("Unexpected topic!"); } let requestor = subject.QueryInterface(Ci.nsIInterfaceRequestor); let contentWindow = requestor.getInterface(Ci.nsIDOMWindow); let contentDocument = contentWindow.document; let tab = BrowserApp.getTabForWindow(contentWindow); if (!tab) return; let host = contentDocument.documentURIObject.asciiHost; let strings = Strings.browser; let message, responseTopic; if (topic == this._permissionsPrompt) { message = strings.formatStringFromName("offlineApps.available2", [host], 1); responseTopic = this._permissionsResponse; } else if (topic == this._quotaPrompt) { message = strings.formatStringFromName("indexedDBQuota.wantsTo", [ host, data ], 2); responseTopic = this._quotaResponse; } else if (topic == this._quotaCancel) { responseTopic = this._quotaResponse; } let notificationID = responseTopic + host; let observer = requestor.getInterface(Ci.nsIObserver); if (topic == this._quotaCancel) { NativeWindow.doorhanger.hide(notificationID, tab.id); observer.observe(null, responseTopic, Ci.nsIPermissionManager.UNKNOWN_ACTION); return; } let buttons = [{ label: strings.GetStringFromName("offlineApps.allow"), callback: function() { observer.observe(null, responseTopic, Ci.nsIPermissionManager.ALLOW_ACTION); } }, { label: strings.GetStringFromName("offlineApps.never"), callback: function() { observer.observe(null, responseTopic, Ci.nsIPermissionManager.DENY_ACTION); } }, { label: strings.GetStringFromName("offlineApps.notNow"), callback: function() { observer.observe(null, responseTopic, Ci.nsIPermissionManager.UNKNOWN_ACTION); } }]; NativeWindow.doorhanger.show(message, notificationID, buttons, tab.id); } }; var ConsoleAPI = { init: function init() { Services.obs.addObserver(this, "console-api-log-event", false); }, uninit: function uninit() { Services.obs.removeObserver(this, "console-api-log-event", false); }, observe: function observe(aMessage, aTopic, aData) { aMessage = aMessage.wrappedJSObject; let mappedArguments = Array.map(aMessage.arguments, this.formatResult, this); let joinedArguments = Array.join(mappedArguments, " "); if (aMessage.level == "error" || aMessage.level == "warn") { let flag = (aMessage.level == "error" ? Ci.nsIScriptError.errorFlag : Ci.nsIScriptError.warningFlag); let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(Ci.nsIScriptError); consoleMsg.init(joinedArguments, null, null, 0, 0, flag, "content javascript"); Services.console.logMessage(consoleMsg); } else if (aMessage.level == "trace") { let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties"); let args = aMessage.arguments; let filename = this.abbreviateSourceURL(args[0].filename); let functionName = args[0].functionName || bundle.GetStringFromName("stacktrace.anonymousFunction"); let lineNumber = args[0].lineNumber; let body = bundle.formatStringFromName("stacktrace.outputMessage", [filename, functionName, lineNumber], 3); body += "\n"; args.forEach(function(aFrame) { let functionName = aFrame.functionName || bundle.GetStringFromName("stacktrace.anonymousFunction"); body += " " + aFrame.filename + " :: " + functionName + " :: " + aFrame.lineNumber + "\n"; }); Services.console.logStringMessage(body); } else if (aMessage.level == "time" && aMessage.arguments) { let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties"); let body = bundle.formatStringFromName("timer.start", [aMessage.arguments.name], 1); Services.console.logStringMessage(body); } else if (aMessage.level == "timeEnd" && aMessage.arguments) { let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties"); let body = bundle.formatStringFromName("timer.end", [aMessage.arguments.name, aMessage.arguments.duration], 2); Services.console.logStringMessage(body); } else if (["group", "groupCollapsed", "groupEnd"].indexOf(aMessage.level) != -1) { // Do nothing yet } else { Services.console.logStringMessage(joinedArguments); } }, getResultType: function getResultType(aResult) { let type = aResult === null ? "null" : typeof aResult; if (type == "object" && aResult.constructor && aResult.constructor.name) type = aResult.constructor.name; return type.toLowerCase(); }, formatResult: function formatResult(aResult) { let output = ""; let type = this.getResultType(aResult); switch (type) { case "string": case "boolean": case "date": case "error": case "number": case "regexp": output = aResult.toString(); break; case "null": case "undefined": output = type; break; default: if (aResult.toSource) { try { output = aResult.toSource(); } catch (ex) { } } if (!output || output == "({})") { output = aResult.toString(); } break; } return output; }, abbreviateSourceURL: function abbreviateSourceURL(aSourceURL) { // Remove any query parameters. let hookIndex = aSourceURL.indexOf("?"); if (hookIndex > -1) aSourceURL = aSourceURL.substring(0, hookIndex); // Remove a trailing "/". if (aSourceURL[aSourceURL.length - 1] == "/") aSourceURL = aSourceURL.substring(0, aSourceURL.length - 1); // Remove all but the last path component. let slashIndex = aSourceURL.lastIndexOf("/"); if (slashIndex > -1) aSourceURL = aSourceURL.substring(slashIndex + 1); return aSourceURL; } }; var ClipboardHelper = { init: function() { NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copy"), ClipboardHelper.getCopyContext(false), ClipboardHelper.copy.bind(ClipboardHelper)); NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyAll"), ClipboardHelper.getCopyContext(true), ClipboardHelper.copy.bind(ClipboardHelper)); NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.selectAll"), ClipboardHelper.selectAllContext, ClipboardHelper.select.bind(ClipboardHelper)); NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.paste"), ClipboardHelper.pasteContext, ClipboardHelper.paste.bind(ClipboardHelper)); NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.changeInputMethod"), NativeWindow.contextmenus.textContext, ClipboardHelper.inputMethod.bind(ClipboardHelper)); }, get clipboardHelper() { delete this.clipboardHelper; return this.clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper); }, get clipboard() { delete this.clipboard; return this.clipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard); }, copy: function(aElement) { let selectionStart = aElement.selectionStart; let selectionEnd = aElement.selectionEnd; if (selectionStart != selectionEnd) { string = aElement.value.slice(selectionStart, selectionEnd); this.clipboardHelper.copyString(string); } else { this.clipboardHelper.copyString(aElement.value); } }, select: function(aElement) { if (!aElement || !(aElement instanceof Ci.nsIDOMNSEditableElement)) return; let target = aElement.QueryInterface(Ci.nsIDOMNSEditableElement); target.editor.selectAll(); target.focus(); }, paste: function(aElement) { if (!aElement || !(aElement instanceof Ci.nsIDOMNSEditableElement)) return; let target = aElement.QueryInterface(Ci.nsIDOMNSEditableElement); target.editor.paste(Ci.nsIClipboard.kGlobalClipboard); target.focus(); }, inputMethod: function(aElement) { Cc["@mozilla.org/imepicker;1"].getService(Ci.nsIIMEPicker).show(); }, getCopyContext: function(isCopyAll) { return { matches: function(aElement) { if (NativeWindow.contextmenus.textContext.matches(aElement)) { // Don't include "copy" for password fields. // mozIsTextField(true) tests for only non-password fields. if (aElement instanceof Ci.nsIDOMHTMLInputElement && !aElement.mozIsTextField(true)) return false; let selectionStart = aElement.selectionStart; let selectionEnd = aElement.selectionEnd; if (selectionStart != selectionEnd) return true; if (isCopyAll && aElement.textLength > 0) return true; } return false; } } }, selectAllContext: { matches: function selectAllContextMatches(aElement) { if (NativeWindow.contextmenus.textContext.matches(aElement)) { let selectionStart = aElement.selectionStart; let selectionEnd = aElement.selectionEnd; return (selectionStart > 0 || selectionEnd < aElement.textLength); } return false; } }, pasteContext: { matches: function(aElement) { if (NativeWindow.contextmenus.textContext.matches(aElement)) { let flavors = ["text/unicode"]; return ClipboardHelper.clipboard.hasDataMatchingFlavors(flavors, flavors.length, Ci.nsIClipboard.kGlobalClipboard); } return false; } } }; var PluginHelper = { showDoorHanger: function(aTab) { let message = Strings.browser.GetStringFromName("clickToPlayPlugins.message"); let buttons = [ { label: Strings.browser.GetStringFromName("clickToPlayPlugins.yes"), callback: function() { PluginHelper.playAllPlugins(aTab); } }, { label: Strings.browser.GetStringFromName("clickToPlayPlugins.no"), callback: function() { // Do nothing } } ] NativeWindow.doorhanger.show(message, "ask-to-play-plugins", buttons, aTab.id); }, playAllPlugins: function(aTab, aEvent) { if (aEvent) { if (!aEvent.isTrusted) return; aEvent.preventDefault(); } this._findAndPlayAllPlugins(aTab.browser.contentWindow); }, // Helper function that recurses through sub-frames to find all plugin objects _findAndPlayAllPlugins: function _findAndPlayAllPlugins(aWindow) { let embeds = aWindow.document.getElementsByTagName("embed"); for (let i = 0; i < embeds.length; i++) { if (!embeds[i].hasAttribute("played")) this._playPlugin(embeds[i]); } let objects = aWindow.document.getElementsByTagName("object"); for (let i = 0; i < objects.length; i++) { if (!objects[i].hasAttribute("played")) this._playPlugin(objects[i]); } for (let i = 0; i < aWindow.frames.length; i++) { this._findAndPlayAllPlugins(aWindow.frames[i]); } }, _playPlugin: function _playPlugin(aPlugin) { let objLoadingContent = aPlugin.QueryInterface(Ci.nsIObjectLoadingContent); objLoadingContent.playPlugin(); // Set an attribute on the plugin object to avoid re-loading it aPlugin.setAttribute("played", true); }, getPluginPreference: function getPluginPreference() { let pluginDisable = Services.prefs.getBoolPref("plugin.disable"); if (pluginDisable) return "0"; let clickToPlay = Services.prefs.getBoolPref("plugins.click_to_play"); return clickToPlay ? "2" : "1"; }, setPluginPreference: function setPluginPreference(aValue) { switch (aValue) { case "0": // Enable Plugins = No Services.prefs.setBoolPref("plugin.disable", true); Services.prefs.clearUserPref("plugins.click_to_play"); break; case "1": // Enable Plugins = Yes Services.prefs.clearUserPref("plugin.disable"); Services.prefs.setBoolPref("plugins.click_to_play", false); break; case "2": // Enable Plugins = Tap to Play (default) Services.prefs.clearUserPref("plugin.disable"); Services.prefs.clearUserPref("plugins.click_to_play"); break; } }, // Copied from /browser/base/content/browser.js isTooSmall : function (plugin, overlay) { // Is the 's size too small to hold what we want to show? let pluginRect = plugin.getBoundingClientRect(); // XXX bug 446693. The text-shadow on the submitted-report text at // the bottom causes scrollHeight to be larger than it should be. let overflows = (overlay.scrollWidth > pluginRect.width) || (overlay.scrollHeight - 5 > pluginRect.height); return overflows; } }; var PermissionsHelper = { _permissonTypes: ["password", "geolocation", "popup", "indexedDB", "offline-app", "desktop-notification"], _permissionStrings: { "password": { label: "password.rememberPassword", allowed: "password.remember", denied: "password.never" }, "geolocation": { label: "geolocation.shareLocation", allowed: "geolocation.alwaysAllow", denied: "geolocation.neverAllow" }, "popup": { label: "blockPopups.label", allowed: "popupButtonAlwaysAllow2", denied: "popupButtonNeverWarn2" }, "indexedDB": { label: "offlineApps.storeOfflineData", allowed: "offlineApps.allow", denied: "offlineApps.never" }, "offline-app": { label: "offlineApps.storeOfflineData", allowed: "offlineApps.allow", denied: "offlineApps.never" }, "desktop-notification": { label: "desktopNotification.useNotifications", allowed: "desktopNotification.allow", denied: "desktopNotification.dontAllow" } }, init: function init() { Services.obs.addObserver(this, "Permissions:Get", false); Services.obs.addObserver(this, "Permissions:Clear", false); }, observe: function observe(aSubject, aTopic, aData) { let uri = BrowserApp.selectedBrowser.currentURI; switch (aTopic) { case "Permissions:Get": let permissions = []; for (let i = 0; i < this._permissonTypes.length; i++) { let type = this._permissonTypes[i]; let value = this.getPermission(uri, type); // Only add the permission if it was set by the user if (value == Services.perms.UNKNOWN_ACTION) continue; // Get the strings that correspond to the permission type let typeStrings = this._permissionStrings[type]; let label = Strings.browser.GetStringFromName(typeStrings["label"]); // Get the key to look up the appropriate string entity let valueKey = value == Services.perms.ALLOW_ACTION ? "allowed" : "denied"; let valueString = Strings.browser.GetStringFromName(typeStrings[valueKey]); // If we implement a two-line UI, we will need to pass the label and // value individually and let java handle the formatting let setting = Strings.browser.formatStringFromName("siteSettings.labelToValue", [ label, valueString ], 2) permissions.push({ type: type, setting: setting }); } // Keep track of permissions, so we know which ones to clear this._currentPermissions = permissions; let host; try { host = uri.host; } catch(e) { host = uri.spec; } sendMessageToJava({ gecko: { type: "Permissions:Data", host: host, permissions: permissions } }); break; case "Permissions:Clear": // An array of the indices of the permissions we want to clear let permissionsToClear = JSON.parse(aData); for (let i = 0; i < permissionsToClear.length; i++) { let indexToClear = permissionsToClear[i]; let permissionType = this._currentPermissions[indexToClear]["type"]; this.clearPermission(uri, permissionType); } break; } }, /** * Gets the permission value stored for a specified permission type. * * @param aType * The permission type string stored in permission manager. * e.g. "geolocation", "indexedDB", "popup" * * @return A permission value defined in nsIPermissionManager. */ getPermission: function getPermission(aURI, aType) { // Password saving isn't a nsIPermissionManager permission type, so handle // it seperately. if (aType == "password") { // By default, login saving is enabled, so if it is disabled, the // user selected the never remember option if (!Services.logins.getLoginSavingEnabled(aURI.prePath)) return Services.perms.DENY_ACTION; // Check to see if the user ever actually saved a login if (Services.logins.countLogins(aURI.prePath, "", "")) return Services.perms.ALLOW_ACTION; return Services.perms.UNKNOWN_ACTION; } // Geolocation consumers use testExactPermission if (aType == "geolocation") return Services.perms.testExactPermission(aURI, aType); return Services.perms.testPermission(aURI, aType); }, /** * Clears a user-set permission value for the site given a permission type. * * @param aType * The permission type string stored in permission manager. * e.g. "geolocation", "indexedDB", "popup" */ clearPermission: function clearPermission(aURI, aType) { // Password saving isn't a nsIPermissionManager permission type, so handle // it seperately. if (aType == "password") { // Get rid of exisiting stored logings let logins = Services.logins.findLogins({}, aURI.prePath, "", ""); for (let i = 0; i < logins.length; i++) { Services.logins.removeLogin(logins[i]); } // Re-set login saving to enabled Services.logins.setLoginSavingEnabled(aURI.prePath, true); } else { Services.perms.remove(aURI.host, aType); // Clear content prefs set in ContentPermissionPrompt.js Services.contentPrefs.removePref(aURI, aType + ".request.remember"); } } }; var MasterPassword = { pref: "privacy.masterpassword.enabled", _tokenName: "", get _secModuleDB() { delete this._secModuleDB; return this._secModuleDB = Cc["@mozilla.org/security/pkcs11moduledb;1"].getService(Ci.nsIPKCS11ModuleDB); }, get _pk11DB() { delete this._pk11DB; return this._pk11DB = Cc["@mozilla.org/security/pk11tokendb;1"].getService(Ci.nsIPK11TokenDB); }, get enabled() { let slot = this._secModuleDB.findSlotByName(this._tokenName); if (slot) { let status = slot.status; return status != Ci.nsIPKCS11Slot.SLOT_UNINITIALIZED && status != Ci.nsIPKCS11Slot.SLOT_READY; } return false; }, setPassword: function setPassword(aPassword) { try { let status; let slot = this._secModuleDB.findSlotByName(this._tokenName); if (slot) status = slot.status; else return false; let token = this._pk11DB.findTokenByName(this._tokenName); if (status == Ci.nsIPKCS11Slot.SLOT_UNINITIALIZED) token.initPassword(aPassword); else if (status == Ci.nsIPKCS11Slot.SLOT_READY) token.changePassword("", aPassword); this.updatePref(); return true; } catch(e) { dump("MasterPassword.setPassword: " + e); } return false; }, removePassword: function removePassword(aOldPassword) { try { let token = this._pk11DB.getInternalKeyToken(); if (token.checkPassword(aOldPassword)) { token.changePassword(aOldPassword, ""); this.updatePref(); return true; } } catch(e) { dump("MasterPassword.removePassword: " + e + "\n"); } NativeWindow.toast.show(Strings.browser.GetStringFromName("masterPassword.incorrect"), "short"); return false; }, updatePref: function() { var prefs = []; let pref = { name: this.pref, type: "bool", value: this.enabled }; prefs.push(pref); sendMessageToJava({ gecko: { type: "Preferences:Data", preferences: prefs } }); } }; var CharacterEncoding = { _charsets: [], init: function init() { Services.obs.addObserver(this, "CharEncoding:Get", false); Services.obs.addObserver(this, "CharEncoding:Set", false); this.sendState(); }, uninit: function uninit() { Services.obs.removeObserver(this, "CharEncoding:Get", false); Services.obs.removeObserver(this, "CharEncoding:Set", false); }, observe: function observe(aSubject, aTopic, aData) { switch (aTopic) { case "CharEncoding:Get": this.getEncoding(); break; case "CharEncoding:Set": this.setEncoding(aData); break; } }, sendState: function sendState() { let showCharEncoding = "false"; try { showCharEncoding = Services.prefs.getComplexValue("browser.menu.showCharacterEncoding", Ci.nsIPrefLocalizedString).data; } catch (e) { /* Optional */ } sendMessageToJava({ gecko: { type: "CharEncoding:State", visible: showCharEncoding } }); }, getEncoding: function getEncoding() { function normalizeCharsetCode(charsetCode) { return charsetCode.trim().toLowerCase(); } function getTitle(charsetCode) { let charsetTitle = charsetCode; try { charsetTitle = Strings.charset.GetStringFromName(charsetCode + ".title"); } catch (e) { dump("error: title not found for " + charsetCode); } return charsetTitle; } if (!this._charsets.length) { let charsets = Services.prefs.getComplexValue("intl.charsetmenu.browser.static", Ci.nsIPrefLocalizedString).data; this._charsets = charsets.split(",").map(function (charset) { return { code: normalizeCharsetCode(charset), title: getTitle(charset) }; }); } // if document charset is not in charset options, add it let docCharset = normalizeCharsetCode(BrowserApp.selectedBrowser.contentDocument.characterSet); let selected = 0; let charsetCount = this._charsets.length; for (; selected < charsetCount && this._charsets[selected].code != docCharset; selected++); if (selected == charsetCount) { this._charsets.push({ code: docCharset, title: getTitle(docCharset) }); } sendMessageToJava({ gecko: { type: "CharEncoding:Data", charsets: this._charsets, selected: selected } }); }, setEncoding: function setEncoding(aEncoding) { let browser = BrowserApp.selectedBrowser; let docCharset = browser.docShell.QueryInterface(Ci.nsIDocCharset); docCharset.charset = aEncoding; browser.reload(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE); } }; function OverscrollController(aTab) { this.tab = aTab; } OverscrollController.prototype = { supportsCommand : function supportsCommand(aCommand) { if (aCommand != "cmd_linePrevious" && aCommand != "cmd_scrollPageUp") return false; return (this.tab.getViewport().y == 0); }, isCommandEnabled : function isCommandEnabled(aCommand) { return this.supportsCommand(aCommand); }, doCommand : function doCommand(aCommand){ sendMessageToJava({ gecko: { type: "ToggleChrome:Focus" } }); }, onEvent : function onEvent(aEvent) { } }; var SearchEngines = { _contextMenuId: null, init: function init() { Services.obs.addObserver(this, "SearchEngines:Get", false); let contextName = Strings.browser.GetStringFromName("contextmenu.addSearchEngine"); let filter = { matches: function (aElement) { return (aElement.form && NativeWindow.contextmenus.textContext.matches(aElement)); } }; this._contextMenuId = NativeWindow.contextmenus.add(contextName, filter, this.addEngine); }, uninit: function uninit() { Services.obs.removeObserver(this, "SearchEngines:Get", false); if (this._contextMenuId != null) NativeWindow.contextmenus.remove(this._contextMenuId); }, observe: function observe(aSubject, aTopic, aData) { if (aTopic == "SearchEngines:Get") { let engineData = Services.search.getVisibleEngines({}); let searchEngines = engineData.map(function (engine) { return { name: engine.name, iconURI: (engine.iconURI ? engine.iconURI.spec : null) }; }); sendMessageToJava({ gecko: { type: "SearchEngines:Data", searchEngines: searchEngines } }); } }, addEngine: function addEngine(aElement) { let form = aElement.form; let charset = aElement.ownerDocument.characterSet; let docURI = Services.io.newURI(aElement.ownerDocument.URL, charset, null); let formURL = Services.io.newURI(form.getAttribute("action"), charset, docURI).spec; let method = form.method.toUpperCase(); let formData = []; for each (let el in form.elements) { if (!el.type) continue; // make this text field a generic search parameter if (aElement == el) { formData.push({ name: el.name, value: "{searchTerms}" }); continue; } let type = el.type.toLowerCase(); let escapedName = escape(el.name); let escapedValue = escape(el.value); // add other form elements as parameters switch (el.type) { case "checkbox": case "radio": if (!el.checked) break; case "text": case "hidden": case "textarea": formData.push({ name: escapedName, value: escapedValue }); break; case "select-one": for each (let option in el.options) { if (option.selected) { formData.push({ name: escapedName, value: escapedValue }); break; } } } } // prompt user for name of search engine let promptTitle = Strings.browser.GetStringFromName("contextmenu.addSearchEngine"); let title = { value: (aElement.ownerDocument.title || docURI.host) }; if (!Services.prompt.prompt(null, promptTitle, null, title, null, {})) return; // fetch the favicon for this page let dbFile = FileUtils.getFile("ProfD", ["browser.db"]); let mDBConn = Services.storage.openDatabase(dbFile); let stmts = []; stmts[0] = mDBConn.createStatement("SELECT favicon FROM images WHERE url_key = ?"); stmts[0].bindStringParameter(0, docURI.spec); let favicon = null; mDBConn.executeAsync(stmts, stmts.length, { handleResult: function (results) { let bytes = results.getNextRow().getResultByName("favicon"); favicon = "data:image/png;base64," + btoa(String.fromCharCode.apply(null, bytes)); }, handleCompletion: function (reason) { // if there's already an engine with this name, add a number to // make the name unique (e.g., "Google" becomes "Google 2") let name = title.value; for (let i = 2; Services.search.getEngineByName(name); i++) name = title.value + " " + i; Services.search.addEngineWithDetails(name, favicon, null, null, method, formURL); let engine = Services.search.getEngineByName(name); engine.wrappedJSObject._queryCharset = charset; for each (let param in formData) { if (param.name && param.value) engine.addParam(param.name, param.value, null); } } }); } };