summaryrefslogtreecommitdiffstats
path: root/mobile/android/chrome
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/chrome')
-rw-r--r--mobile/android/chrome/content/.eslintrc23
-rw-r--r--mobile/android/chrome/content/ActionBarHandler.js731
-rw-r--r--mobile/android/chrome/content/CastingApps.js850
-rw-r--r--mobile/android/chrome/content/ConsoleAPI.js96
-rw-r--r--mobile/android/chrome/content/EmbedRT.js82
-rw-r--r--mobile/android/chrome/content/FeedHandler.js120
-rw-r--r--mobile/android/chrome/content/Feedback.js64
-rw-r--r--mobile/android/chrome/content/FindHelper.js197
-rw-r--r--mobile/android/chrome/content/InputWidgetHelper.js98
-rw-r--r--mobile/android/chrome/content/Linkify.js108
-rw-r--r--mobile/android/chrome/content/MasterPassword.js67
-rw-r--r--mobile/android/chrome/content/MemoryObserver.js88
-rw-r--r--mobile/android/chrome/content/OfflineApps.js77
-rw-r--r--mobile/android/chrome/content/PermissionsHelper.js188
-rw-r--r--mobile/android/chrome/content/PluginHelper.js221
-rw-r--r--mobile/android/chrome/content/PresentationView.js63
-rw-r--r--mobile/android/chrome/content/PresentationView.xul15
-rw-r--r--mobile/android/chrome/content/PrintHelper.js73
-rw-r--r--mobile/android/chrome/content/Reader.js290
-rw-r--r--mobile/android/chrome/content/RemoteDebugger.js355
-rw-r--r--mobile/android/chrome/content/SelectHelper.js161
-rw-r--r--mobile/android/chrome/content/WebcompatReporter.js144
-rw-r--r--mobile/android/chrome/content/WebrtcUI.js302
-rw-r--r--mobile/android/chrome/content/about.js151
-rw-r--r--mobile/android/chrome/content/about.xhtml77
-rw-r--r--mobile/android/chrome/content/aboutAccounts.js351
-rw-r--r--mobile/android/chrome/content/aboutAccounts.xhtml83
-rw-r--r--mobile/android/chrome/content/aboutAddons.js609
-rw-r--r--mobile/android/chrome/content/aboutAddons.xhtml63
-rw-r--r--mobile/android/chrome/content/aboutCertError.xhtml264
-rw-r--r--mobile/android/chrome/content/aboutDownloads.js373
-rw-r--r--mobile/android/chrome/content/aboutDownloads.xhtml62
-rw-r--r--mobile/android/chrome/content/aboutHealthReport.js192
-rw-r--r--mobile/android/chrome/content/aboutHealthReport.xhtml32
-rw-r--r--mobile/android/chrome/content/aboutHome.xhtml22
-rw-r--r--mobile/android/chrome/content/aboutLogins.js518
-rw-r--r--mobile/android/chrome/content/aboutLogins.xhtml90
-rw-r--r--mobile/android/chrome/content/aboutPrivateBrowsing.js32
-rw-r--r--mobile/android/chrome/content/aboutPrivateBrowsing.xhtml43
-rw-r--r--mobile/android/chrome/content/aboutRights.xhtml95
-rw-r--r--mobile/android/chrome/content/bindings/checkbox.xml75
-rw-r--r--mobile/android/chrome/content/bindings/settings.xml66
-rw-r--r--mobile/android/chrome/content/blockedSite.xhtml195
-rw-r--r--mobile/android/chrome/content/browser.css7
-rw-r--r--mobile/android/chrome/content/browser.js6999
-rw-r--r--mobile/android/chrome/content/browser.xul17
-rw-r--r--mobile/android/chrome/content/config.js673
-rw-r--r--mobile/android/chrome/content/config.xhtml86
-rw-r--r--mobile/android/chrome/content/content.js159
-rw-r--r--mobile/android/chrome/content/geckoview.js32
-rw-r--r--mobile/android/chrome/content/geckoview.xul16
-rw-r--r--mobile/android/chrome/content/healthreport-prefs.js6
-rw-r--r--mobile/android/chrome/content/languages.properties114
-rw-r--r--mobile/android/chrome/content/netError.xhtml406
-rw-r--r--mobile/android/chrome/jar.mn71
-rw-r--r--mobile/android/chrome/moz.build13
56 files changed, 16405 insertions, 0 deletions
diff --git a/mobile/android/chrome/content/.eslintrc b/mobile/android/chrome/content/.eslintrc
new file mode 100644
index 000000000..32513189a
--- /dev/null
+++ b/mobile/android/chrome/content/.eslintrc
@@ -0,0 +1,23 @@
+globals:
+ # TODO: Maybe this should be by file
+ BrowserApp: false
+ Cc: false
+ Ci: false
+ Cu: false
+ NativeWindow: false
+ PageActions: false
+ ReaderMode: false
+ SimpleServiceDiscovery: false
+ TabMirror: false
+ MediaPlayerApp: false
+ RokuApp: false
+ SearchEngines: false
+ ConsoleAPI: true
+ Point: false
+ Rect: false
+
+rules:
+ # Disabled stuff
+ no-console: 0 # TODO: Can we use console?
+ no-cond-assign: 0
+ no-fallthrough: 0
diff --git a/mobile/android/chrome/content/ActionBarHandler.js b/mobile/android/chrome/content/ActionBarHandler.js
new file mode 100644
index 000000000..190021043
--- /dev/null
+++ b/mobile/android/chrome/content/ActionBarHandler.js
@@ -0,0 +1,731 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
+
+const PHONE_REGEX = /^\+?[0-9\s,-.\(\)*#pw]{1,30}$/; // Are we a phone #?
+
+
+/**
+ * ActionBarHandler Object and methods. Interface between Gecko Text Selection code
+ * (AccessibleCaret, etc) and the Mobile ActionBar UI.
+ */
+var ActionBarHandler = {
+ // Error codes returned from _init().
+ START_TOUCH_ERROR: {
+ NO_CONTENT_WINDOW: "No valid content Window found.",
+ NONE: "",
+ },
+
+ _nextSelectionID: 1, // Next available.
+ _selectionID: null, // Unique Selection ID, assigned each time we _init().
+
+ _boundingClientRect: null, // Current selections boundingClientRect.
+ _actionBarActions: null, // Most-recent set of actions sent to ActionBar.
+
+ /**
+ * Receive and act on AccessibleCarets caret state-change
+ * (mozcaretstatechanged) events.
+ */
+ caretStateChangedHandler: function(e) {
+ // Close an open ActionBar, if carets no longer logically visible.
+ if (this._selectionID && !e.caretVisible) {
+ this._uninit(false);
+ return;
+ }
+
+ if (!this._selectionID && e.collapsed) {
+ switch (e.reason) {
+ case 'longpressonemptycontent':
+ case 'taponcaret':
+ // Show ActionBar when long pressing on an empty input or single
+ // tapping on the caret.
+ this._init(e.boundingClientRect);
+ break;
+
+ case 'updateposition':
+ // Do not show ActionBar when single tapping on an non-empty editable
+ // input.
+ break;
+
+ default:
+ break;
+ }
+ return;
+ }
+
+ // Open a closed ActionBar if carets actually visible.
+ if (!this._selectionID && e.caretVisuallyVisible) {
+ this._init(e.boundingClientRect);
+ return;
+ }
+
+ // Else, update an open ActionBar.
+ if (this._selectionID) {
+ if (!this._selectionHasChanged()) {
+ // Still the same active selection.
+ if (e.reason == 'presscaret' || e.reason == 'scroll') {
+ // boundingClientRect doesn't matter since we are hiding the floating
+ // toolbar.
+ this._updateVisibility();
+ } else {
+ // Selection changes update boundingClientRect.
+ this._boundingClientRect = e.boundingClientRect;
+ let forceUpdate = e.reason == 'updateposition' || e.reason == 'releasecaret';
+ this._sendActionBarActions(forceUpdate);
+ }
+ } else {
+ // We've started a new selection entirely.
+ this._uninit(false);
+ this._init(e.boundingClientRect);
+ }
+ }
+ },
+
+ /**
+ * ActionBarHandler notification observers.
+ */
+ observe: function(subject, topic, data) {
+ switch (topic) {
+ // User click an ActionBar button.
+ case "TextSelection:Action": {
+ if (!this._selectionID) {
+ break;
+ }
+ for (let type in this.actions) {
+ let action = this.actions[type];
+ if (action.id == data) {
+ action.action(this._targetElement, this._contentWindow);
+ break;
+ }
+ }
+ break;
+ }
+
+ // Provide selected text to FindInPageBar on request.
+ case "TextSelection:Get": {
+ Messaging.sendRequest({
+ type: "TextSelection:Data",
+ requestId: data,
+ text: this._getSelectedText(),
+ });
+
+ this._uninit();
+ break;
+ }
+
+ // User closed ActionBar by clicking "checkmark" button.
+ case "TextSelection:End": {
+ // End the requested selection only.
+ if (this._selectionID == JSON.parse(data).selectionID) {
+ this._uninit();
+ }
+ break;
+ }
+ }
+ },
+
+ /**
+ * Called when Gecko AccessibleCaret becomes visible.
+ */
+ _init: function(boundingClientRect) {
+ let [element, win] = this._getSelectionTargets();
+ if (!win) {
+ return this.START_TOUCH_ERROR.NO_CONTENT_WINDOW;
+ }
+
+ // Hold the ActionBar ID provided by Gecko.
+ this._selectionID = this._nextSelectionID++;
+ [this._targetElement, this._contentWindow] = [element, win];
+ this._boundingClientRect = boundingClientRect;
+
+ // Open the ActionBar, send it's actions list.
+ Messaging.sendRequest({
+ type: "TextSelection:ActionbarInit",
+ selectionID: this._selectionID,
+ });
+ this._sendActionBarActions(true);
+
+ return this.START_TOUCH_ERROR.NONE;
+ },
+
+ /**
+ * Called when content is scrolled and handles are hidden.
+ */
+ _updateVisibility: function() {
+ Messaging.sendRequest({
+ type: "TextSelection:Visibility",
+ selectionID: this._selectionID,
+ });
+ },
+
+ /**
+ * Determines the window containing the selection, and its
+ * editable element if present.
+ */
+ _getSelectionTargets: function() {
+ let [element, win] = [Services.focus.focusedElement, Services.focus.focusedWindow];
+ if (!element) {
+ // No focused editable.
+ return [null, win];
+ }
+
+ // Return focused editable text element and its window.
+ if (((element instanceof HTMLInputElement) && element.mozIsTextField(false)) ||
+ (element instanceof HTMLTextAreaElement) ||
+ element.isContentEditable) {
+ return [element, win];
+ }
+
+ // Focused element can't contain text.
+ return [null, win];
+ },
+
+ /**
+ * The active Selection has changed, if the current focused element / win,
+ * pair, or state of the win's designMode changes.
+ */
+ _selectionHasChanged: function() {
+ let [element, win] = this._getSelectionTargets();
+ return (this._targetElement !== element ||
+ this._contentWindow !== win ||
+ this._isInDesignMode(this._contentWindow) !== this._isInDesignMode(win));
+ },
+
+ /**
+ * Called when Gecko AccessibleCaret becomes hidden,
+ * ActionBar is closed by user "close" request, or as a result of object
+ * methods such as SELECT_ALL, PASTE, etc.
+ */
+ _uninit: function(clearSelection = true) {
+ // Bail if there's no active selection.
+ if (!this._selectionID) {
+ return;
+ }
+
+ // Close the ActionBar.
+ Messaging.sendRequest({
+ type: "TextSelection:ActionbarUninit",
+ });
+
+ // Clear the selection ID to complete the uninit(), but leave our reference
+ // to selectionTargets (_targetElement, _contentWindow) in case we need
+ // a final clearSelection().
+ this._selectionID = null;
+ this._boundingClientRect = null;
+
+ // Clear selection required if triggered by self, or TextSelection icon
+ // actions. If called by Gecko CaretStateChangedEvent,
+ // visibility state is already correct.
+ if (clearSelection) {
+ this._clearSelection();
+ }
+ },
+
+ /**
+ * Final UI cleanup when Actionbar is closed by icon click, or where
+ * we terminate selection state after before/after actionbar actions
+ * (Cut, Copy, Paste, Search, Share, Call).
+ */
+ _clearSelection: function(element = this._targetElement, win = this._contentWindow) {
+ // Commit edit compositions, and clear focus from editables.
+ if (element) {
+ let imeSupport = this._getEditor(element, win).QueryInterface(Ci.nsIEditorIMESupport);
+ if (imeSupport.composing) {
+ imeSupport.forceCompositionEnd();
+ }
+ element.blur();
+ }
+
+ // Remove Selection from non-editables and now-unfocused contentEditables.
+ if (!element || element.isContentEditable) {
+ this._getSelection().removeAllRanges();
+ }
+ },
+
+ /**
+ * Called to determine current ActionBar actions and send to TextSelection
+ * handler. By default we only send if current action state differs from
+ * the previous.
+ * @param By default we only send an ActionBarStatus update message if
+ * there is a change from the previous state. sendAlways can be
+ * set by init() for example, where we want to always send the
+ * current state.
+ */
+ _sendActionBarActions: function(sendAlways) {
+ let actions = this._getActionBarActions();
+
+ let actionCountUnchanged = this._actionBarActions &&
+ actions.length === this._actionBarActions.length;
+ let actionsMatch = actionCountUnchanged &&
+ this._actionBarActions.every((e,i) => {
+ return e.id === actions[i].id;
+ });
+
+ if (sendAlways || !actionsMatch) {
+ Messaging.sendRequest({
+ type: "TextSelection:ActionbarStatus",
+ selectionID: this._selectionID,
+ actions: actions,
+ x: this._boundingClientRect.x,
+ y: this._boundingClientRect.y,
+ width: this._boundingClientRect.width,
+ height: this._boundingClientRect.height
+ });
+ }
+
+ this._actionBarActions = actions;
+ },
+
+ /**
+ * Determine and return current ActionBar state.
+ */
+ _getActionBarActions: function(element = this._targetElement, win = this._contentWindow) {
+ let actions = [];
+
+ for (let type in this.actions) {
+ let action = this.actions[type];
+ if (action.selector.matches(element, win)) {
+ let a = {
+ id: action.id,
+ label: this._getActionValue(action, "label", "", element),
+ icon: this._getActionValue(action, "icon", "drawable://ic_status_logo", element),
+ order: this._getActionValue(action, "order", 0, element),
+ floatingOrder: this._getActionValue(action, "floatingOrder", 9, element),
+ showAsAction: this._getActionValue(action, "showAsAction", true, element),
+ };
+ actions.push(a);
+ }
+ }
+ actions.sort((a, b) => b.order - a.order);
+
+ return actions;
+ },
+
+ /**
+ * Provides a value from an action. If the action defines the value as a function,
+ * we return the result of calling the function. Otherwise, we return the value
+ * itself. If the value isn't defined for this action, will return a default.
+ */
+ _getActionValue: function(obj, name, defaultValue, element) {
+ if (!(name in obj))
+ return defaultValue;
+
+ if (typeof obj[name] == "function")
+ return obj[name](element);
+
+ return obj[name];
+ },
+
+ /**
+ * Actionbar callback methods.
+ */
+ actions: {
+
+ SELECT_ALL: {
+ id: "selectall_action",
+ label: Strings.browser.GetStringFromName("contextmenu.selectAll"),
+ icon: "drawable://ab_select_all",
+ order: 5,
+ floatingOrder: 5,
+
+ selector: {
+ matches: function(element, win) {
+ // For editable, check its length. For default contentWindow, assume
+ // true, else there'd been nothing to long-press to open ActionBar.
+ return (element) ? element.textLength != 0 : true;
+ },
+ },
+
+ action: function(element, win) {
+ // Some Mobile keyboards such as SwiftKeyboard, provide auto-suggest
+ // style highlights via composition selections in editables.
+ if (element) {
+ // If we have an active composition string, commit it, and
+ // ensure proper element focus.
+ let imeSupport = ActionBarHandler._getEditor(element, win).
+ QueryInterface(Ci.nsIEditorIMESupport);
+ if (imeSupport.composing) {
+ element.blur();
+ element.focus();
+ }
+ }
+
+ // Close ActionBarHandler, then selectAll, and display handles.
+ ActionBarHandler._getSelectAllController(element, win).selectAll();
+ UITelemetry.addEvent("action.1", "actionbar", null, "select_all");
+ },
+ },
+
+ CUT: {
+ id: "cut_action",
+ label: Strings.browser.GetStringFromName("contextmenu.cut"),
+ icon: "drawable://ab_cut",
+ order: 4,
+ floatingOrder: 1,
+
+ selector: {
+ matches: function(element, win) {
+ // Can cut from editable, or design-mode document.
+ if (!element && !ActionBarHandler._isInDesignMode(win)) {
+ return false;
+ }
+ // Don't allow "cut" from password fields.
+ if (element instanceof Ci.nsIDOMHTMLInputElement &&
+ !element.mozIsTextField(true)) {
+ return false;
+ }
+ // Don't allow "cut" from disabled/readonly fields.
+ if (element && (element.disabled || element.readOnly)) {
+ return false;
+ }
+ // Allow if selected text exists.
+ return (ActionBarHandler._getSelectedText().length > 0);
+ },
+ },
+
+ action: function(element, win) {
+ // First copy the selection text to the clipboard.
+ let selectedText = ActionBarHandler._getSelectedText();
+ let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
+ getService(Ci.nsIClipboardHelper);
+ clipboard.copyString(selectedText);
+
+ let msg = Strings.browser.GetStringFromName("selectionHelper.textCopied");
+ Snackbars.show(msg, Snackbars.LENGTH_LONG);
+
+ // Then cut the selection text.
+ ActionBarHandler._getSelection(element, win).deleteFromDocument();
+
+ ActionBarHandler._uninit();
+ UITelemetry.addEvent("action.1", "actionbar", null, "cut");
+ },
+ },
+
+ COPY: {
+ id: "copy_action",
+ label: Strings.browser.GetStringFromName("contextmenu.copy"),
+ icon: "drawable://ab_copy",
+ order: 3,
+ floatingOrder: 2,
+
+ selector: {
+ matches: function(element, win) {
+ // Don't allow "copy" from password fields.
+ if (element instanceof Ci.nsIDOMHTMLInputElement &&
+ !element.mozIsTextField(true)) {
+ return false;
+ }
+ // Allow if selected text exists.
+ return (ActionBarHandler._getSelectedText().length > 0);
+ },
+ },
+
+ action: function(element, win) {
+ let selectedText = ActionBarHandler._getSelectedText();
+ let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
+ getService(Ci.nsIClipboardHelper);
+ clipboard.copyString(selectedText);
+
+ let msg = Strings.browser.GetStringFromName("selectionHelper.textCopied");
+ Snackbars.show(msg, Snackbars.LENGTH_LONG);
+
+ ActionBarHandler._uninit();
+ UITelemetry.addEvent("action.1", "actionbar", null, "copy");
+ },
+ },
+
+ PASTE: {
+ id: "paste_action",
+ label: Strings.browser.GetStringFromName("contextmenu.paste"),
+ icon: "drawable://ab_paste",
+ order: 2,
+ floatingOrder: 3,
+
+ selector: {
+ matches: function(element, win) {
+ // Can paste to editable, or design-mode document.
+ if (!element && !ActionBarHandler._isInDesignMode(win)) {
+ return false;
+ }
+ // Can't paste into disabled/readonly fields.
+ if (element && (element.disabled || element.readOnly)) {
+ return false;
+ }
+ // Can't paste if Clipboard empty.
+ let flavors = ["text/unicode"];
+ return Services.clipboard.hasDataMatchingFlavors(flavors, flavors.length,
+ Ci.nsIClipboard.kGlobalClipboard);
+ },
+ },
+
+ action: function(element, win) {
+ // Paste the clipboard, then close the ActionBarHandler and ActionBar.
+ ActionBarHandler._getEditor(element, win).
+ paste(Ci.nsIClipboard.kGlobalClipboard);
+ ActionBarHandler._uninit();
+ UITelemetry.addEvent("action.1", "actionbar", null, "paste");
+ },
+ },
+
+ CALL: {
+ id: "call_action",
+ label: Strings.browser.GetStringFromName("contextmenu.call"),
+ icon: "drawable://phone",
+ order: 1,
+ floatingOrder: 0,
+
+ selector: {
+ matches: function(element, win) {
+ return (ActionBarHandler._getSelectedPhoneNumber() != null);
+ },
+ },
+
+ action: function(element, win) {
+ BrowserApp.loadURI("tel:" +
+ ActionBarHandler._getSelectedPhoneNumber());
+
+ ActionBarHandler._uninit();
+ UITelemetry.addEvent("action.1", "actionbar", null, "call");
+ },
+ },
+
+ SEARCH: {
+ id: "search_action",
+ label: () => Strings.browser.formatStringFromName("contextmenu.search",
+ [Services.search.defaultEngine.name], 1),
+ icon: "drawable://ab_search",
+ order: 1,
+ floatingOrder: 6,
+
+ selector: {
+ matches: function(element, win) {
+ // Allow if selected text exists.
+ return (ActionBarHandler._getSelectedText().length > 0);
+ },
+ },
+
+ action: function(element, win) {
+ let selectedText = ActionBarHandler._getSelectedText();
+ ActionBarHandler._uninit();
+
+ // Set current tab as parent of new tab,
+ // and set new tab as private if the parent is.
+ let searchSubmission = Services.search.defaultEngine.getSubmission(selectedText);
+ let parent = BrowserApp.selectedTab;
+ let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(parent.browser);
+ BrowserApp.addTab(searchSubmission.uri.spec,
+ { parentId: parent.id,
+ selected: true,
+ isPrivate: isPrivate,
+ }
+ );
+
+ UITelemetry.addEvent("action.1", "actionbar", null, "search");
+ },
+ },
+
+ SEARCH_ADD: {
+ id: "search_add_action",
+ label: Strings.browser.GetStringFromName("contextmenu.addSearchEngine3"),
+ icon: "drawable://ab_add_search_engine",
+ order: 0,
+ floatingOrder: 8,
+
+ selector: {
+ matches: function(element, win) {
+ if(!(element instanceof HTMLInputElement)) {
+ return false;
+ }
+ let form = element.form;
+ if (!form || element.type == "password") {
+ return false;
+ }
+
+ let method = form.method.toUpperCase();
+ let canAddEngine = (method == "GET") ||
+ (method == "POST" && (form.enctype != "text/plain" && form.enctype != "multipart/form-data"));
+ if (!canAddEngine) {
+ return false;
+ }
+
+ // If SearchEngine query finds it, then we don't want action to add displayed.
+ if (SearchEngines.visibleEngineExists(element)) {
+ return false;
+ }
+
+ return true;
+ },
+ },
+
+ action: function(element, win) {
+ UITelemetry.addEvent("action.1", "actionbar", null, "add_search_engine");
+
+ // Engines are added asynch. If required, update SelectionUI on callback.
+ SearchEngines.addEngine(element, (result) => {
+ if (result) {
+ ActionBarHandler._sendActionBarActions(true);
+ }
+ });
+ },
+ },
+
+ SHARE: {
+ id: "share_action",
+ label: Strings.browser.GetStringFromName("contextmenu.share"),
+ icon: "drawable://ic_menu_share",
+ order: 0,
+ floatingOrder: 4,
+
+ selector: {
+ matches: function(element, win) {
+ if (!ParentalControls.isAllowed(ParentalControls.SHARE)) {
+ return false;
+ }
+ // Allow if selected text exists.
+ return (ActionBarHandler._getSelectedText().length > 0);
+ },
+ },
+
+ action: function(element, win) {
+ Messaging.sendRequest({
+ type: "Share:Text",
+ text: ActionBarHandler._getSelectedText(),
+ });
+
+ ActionBarHandler._uninit();
+ UITelemetry.addEvent("action.1", "actionbar", null, "share");
+ },
+ },
+ },
+
+ /**
+ * Provides UUID service for generating action ID's.
+ */
+ get _idService() {
+ delete this._idService;
+ return this._idService = Cc["@mozilla.org/uuid-generator;1"].
+ getService(Ci.nsIUUIDGenerator);
+ },
+
+ /**
+ * The targetElement holds an editable element containing a
+ * selection or a caret.
+ */
+ get _targetElement() {
+ if (this._targetElementRef)
+ return this._targetElementRef.get();
+ return null;
+ },
+
+ set _targetElement(element) {
+ this._targetElementRef = Cu.getWeakReference(element);
+ },
+
+ /**
+ * The contentWindow holds the selection, or the targetElement
+ * if it's an editable.
+ */
+ get _contentWindow() {
+ if (this._contentWindowRef)
+ return this._contentWindowRef.get();
+ return null;
+ },
+
+ set _contentWindow(aContentWindow) {
+ this._contentWindowRef = Cu.getWeakReference(aContentWindow);
+ },
+
+ /**
+ * If we have an active selection, is it part of a designMode document?
+ */
+ _isInDesignMode: function(win) {
+ return this._selectionID && (win.document.designMode === "on");
+ },
+
+ /**
+ * Provides the currently selected text, for either an editable,
+ * or for the default contentWindow.
+ */
+ _getSelectedText: function() {
+ // Can be called from FindInPageBar "TextSelection:Get", when there
+ // is no active selection.
+ if (!this._selectionID) {
+ return "";
+ }
+
+ let selection = this._getSelection();
+
+ // Textarea can contain LF, etc.
+ if (this._targetElement instanceof Ci.nsIDOMHTMLTextAreaElement) {
+ let flags = Ci.nsIDocumentEncoder.OutputPreformatted |
+ Ci.nsIDocumentEncoder.OutputRaw;
+ return selection.QueryInterface(Ci.nsISelectionPrivate).
+ toStringWithFormat("text/plain", flags, 0);
+ }
+
+ // Return explicitly selected text.
+ return selection.toString();
+ },
+
+ /**
+ * Provides the nsISelection for either an editor, or from the
+ * default window.
+ */
+ _getSelection: function(element = this._targetElement, win = this._contentWindow) {
+ return (element instanceof Ci.nsIDOMNSEditableElement) ?
+ this._getEditor(element).selection :
+ win.getSelection();
+ },
+
+ /**
+ * Returns an nsEditor or nsHTMLEditor.
+ */
+ _getEditor: function(element = this._targetElement, win = this._contentWindow) {
+ if (element instanceof Ci.nsIDOMNSEditableElement) {
+ return element.QueryInterface(Ci.nsIDOMNSEditableElement).editor;
+ }
+
+ return win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation).
+ QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIEditingSession).
+ getEditorForWindow(win);
+ },
+
+ /**
+ * Returns a selection controller.
+ */
+ _getSelectionController: function(element = this._targetElement, win = this._contentWindow) {
+ if (element instanceof Ci.nsIDOMNSEditableElement) {
+ return this._getEditor(element, win).selectionController;
+ }
+
+ return win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation).
+ QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsISelectionDisplay).
+ QueryInterface(Ci.nsISelectionController);
+ },
+
+ /**
+ * For selectAll(), provides the editor, or the default window selection Controller.
+ */
+ _getSelectAllController: function(element = this._targetElement, win = this._contentWindow) {
+ let editor = this._getEditor(element, win);
+ return (editor) ?
+ editor : this._getSelectionController(element, win);
+ },
+
+ /**
+ * Call / Phone Helper methods.
+ */
+ _getSelectedPhoneNumber: function() {
+ let selectedText = this._getSelectedText().trim();
+ return this._isPhoneNumber(selectedText) ?
+ selectedText : null;
+ },
+
+ _isPhoneNumber: function(selectedText) {
+ return (PHONE_REGEX.test(selectedText));
+ },
+};
diff --git a/mobile/android/chrome/content/CastingApps.js b/mobile/android/chrome/content/CastingApps.js
new file mode 100644
index 000000000..76773c4d8
--- /dev/null
+++ b/mobile/android/chrome/content/CastingApps.js
@@ -0,0 +1,850 @@
+// -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "PageActions",
+ "resource://gre/modules/PageActions.jsm");
+
+// Define service devices. We should consider moving these to their respective
+// JSM files, but we left them here to allow for better lazy JSM loading.
+var rokuDevice = {
+ id: "roku:ecp",
+ target: "roku:ecp",
+ factory: function(aService) {
+ Cu.import("resource://gre/modules/RokuApp.jsm");
+ return new RokuApp(aService);
+ },
+ types: ["video/mp4"],
+ extensions: ["mp4"]
+};
+
+var mediaPlayerDevice = {
+ id: "media:router",
+ target: "media:router",
+ factory: function(aService) {
+ Cu.import("resource://gre/modules/MediaPlayerApp.jsm");
+ return new MediaPlayerApp(aService);
+ },
+ types: ["video/mp4", "video/webm", "application/x-mpegurl"],
+ extensions: ["mp4", "webm", "m3u", "m3u8"],
+ init: function() {
+ Services.obs.addObserver(this, "MediaPlayer:Added", false);
+ Services.obs.addObserver(this, "MediaPlayer:Changed", false);
+ Services.obs.addObserver(this, "MediaPlayer:Removed", false);
+ },
+ observe: function(subject, topic, data) {
+ if (topic === "MediaPlayer:Added") {
+ let service = this.toService(JSON.parse(data));
+ SimpleServiceDiscovery.addService(service);
+ } else if (topic === "MediaPlayer:Changed") {
+ let service = this.toService(JSON.parse(data));
+ SimpleServiceDiscovery.updateService(service);
+ } else if (topic === "MediaPlayer:Removed") {
+ SimpleServiceDiscovery.removeService(data);
+ }
+ },
+ toService: function(display) {
+ // Convert the native data into something matching what is created in _processService()
+ return {
+ location: display.location,
+ target: "media:router",
+ friendlyName: display.friendlyName,
+ uuid: display.uuid,
+ manufacturer: display.manufacturer,
+ modelName: display.modelName,
+ mirror: display.mirror
+ };
+ }
+};
+
+var fxOSTVDevice = {
+ id: "app://fling-player.gaiamobile.org",
+ target: "app://fling-player.gaiamobile.org/index.html",
+ factory: function(aService) {
+ Cu.import("resource://gre/modules/PresentationApp.jsm");
+ let request = new window.PresentationRequest(this.target);
+ return new PresentationApp(aService, request);
+ },
+ init: function() {
+ Services.obs.addObserver(this, "presentation-device-change", false);
+ SimpleServiceDiscovery.addExternalDiscovery(this);
+ },
+ observe: function(subject, topic, data) {
+ let device = subject.QueryInterface(Ci.nsIPresentationDevice);
+ let service = this.toService(device);
+ switch (data) {
+ case "add":
+ SimpleServiceDiscovery.addService(service);
+ break;
+ case "update":
+ SimpleServiceDiscovery.updateService(service);
+ break;
+ case "remove":
+ if(SimpleServiceDiscovery.findServiceForID(device.id)) {
+ SimpleServiceDiscovery.removeService(device.id);
+ }
+ break;
+ }
+ },
+ toService: function(device) {
+ return {
+ location: device.id,
+ target: fxOSTVDevice.target,
+ friendlyName: device.name,
+ uuid: device.id,
+ manufacturer: "Firefox OS TV",
+ modelName: "Firefox OS TV",
+ };
+ },
+ startDiscovery: function() {
+ window.navigator.mozPresentationDeviceInfo.forceDiscovery();
+
+ // need to update the lastPing time for known device.
+ window.navigator.mozPresentationDeviceInfo.getAll()
+ .then(function(devices) {
+ for (let device of devices) {
+ let service = fxOSTVDevice.toService(device);
+ SimpleServiceDiscovery.addService(service);
+ }
+ });
+ },
+ stopDiscovery: function() {
+ // do nothing
+ },
+ types: ["video/mp4", "video/webm"],
+ extensions: ["mp4", "webm"],
+};
+
+var CastingApps = {
+ _castMenuId: -1,
+ mirrorStartMenuId: -1,
+ mirrorStopMenuId: -1,
+ _blocked: null,
+ _bound: null,
+ _interval: 120 * 1000, // 120 seconds
+
+ init: function ca_init() {
+ if (!this.isCastingEnabled()) {
+ return;
+ }
+
+ // Register targets
+ SimpleServiceDiscovery.registerDevice(rokuDevice);
+
+ // MediaPlayerDevice will notify us any time the native device list changes.
+ mediaPlayerDevice.init();
+ SimpleServiceDiscovery.registerDevice(mediaPlayerDevice);
+
+ // Presentation Device will notify us any time the available device list changes.
+ if (window.PresentationRequest) {
+ fxOSTVDevice.init();
+ SimpleServiceDiscovery.registerDevice(fxOSTVDevice);
+ }
+
+ // Search for devices continuously
+ SimpleServiceDiscovery.search(this._interval);
+
+ this._castMenuId = NativeWindow.contextmenus.add(
+ Strings.browser.GetStringFromName("contextmenu.sendToDevice"),
+ this.filterCast,
+ this.handleContextMenu.bind(this)
+ );
+
+ Services.obs.addObserver(this, "Casting:Play", false);
+ Services.obs.addObserver(this, "Casting:Pause", false);
+ Services.obs.addObserver(this, "Casting:Stop", false);
+ Services.obs.addObserver(this, "Casting:Mirror", false);
+ Services.obs.addObserver(this, "ssdp-service-found", false);
+ Services.obs.addObserver(this, "ssdp-service-lost", false);
+ Services.obs.addObserver(this, "application-background", false);
+ Services.obs.addObserver(this, "application-foreground", false);
+
+ BrowserApp.deck.addEventListener("TabSelect", this, true);
+ BrowserApp.deck.addEventListener("pageshow", this, true);
+ BrowserApp.deck.addEventListener("playing", this, true);
+ BrowserApp.deck.addEventListener("ended", this, true);
+ BrowserApp.deck.addEventListener("MozAutoplayMediaBlocked", this, true);
+ // Note that the XBL binding is untrusted
+ BrowserApp.deck.addEventListener("MozNoControlsVideoBindingAttached", this, true, true);
+ },
+
+ _mirrorStarted: function(stopMirrorCallback) {
+ this.stopMirrorCallback = stopMirrorCallback;
+ NativeWindow.menu.update(this.mirrorStartMenuId, { visible: false });
+ NativeWindow.menu.update(this.mirrorStopMenuId, { visible: true });
+ },
+
+ serviceAdded: function(aService) {
+ if (this.isMirroringEnabled() && aService.mirror && this.mirrorStartMenuId == -1) {
+ this.mirrorStartMenuId = NativeWindow.menu.add({
+ name: Strings.browser.GetStringFromName("casting.mirrorTab"),
+ callback: function() {
+ let callbackFunc = function(aService) {
+ let app = SimpleServiceDiscovery.findAppForService(aService);
+ if (app) {
+ app.mirror(function() {}, window, BrowserApp.selectedTab.getViewport(), this._mirrorStarted.bind(this), window.BrowserApp.selectedBrowser.contentWindow);
+ }
+ }.bind(this);
+
+ this.prompt(callbackFunc, aService => aService.mirror);
+ }.bind(this),
+ parent: NativeWindow.menu.toolsMenuID
+ });
+
+ this.mirrorStopMenuId = NativeWindow.menu.add({
+ name: Strings.browser.GetStringFromName("casting.mirrorTabStop"),
+ callback: function() {
+ if (this.tabMirror) {
+ this.tabMirror.stop();
+ this.tabMirror = null;
+ } else if (this.stopMirrorCallback) {
+ this.stopMirrorCallback();
+ this.stopMirrorCallback = null;
+ }
+ NativeWindow.menu.update(this.mirrorStartMenuId, { visible: true });
+ NativeWindow.menu.update(this.mirrorStopMenuId, { visible: false });
+ }.bind(this),
+ });
+ }
+ if (this.mirrorStartMenuId != -1) {
+ NativeWindow.menu.update(this.mirrorStopMenuId, { visible: false });
+ }
+ },
+
+ serviceLost: function(aService) {
+ if (aService.mirror && this.mirrorStartMenuId != -1) {
+ let haveMirror = false;
+ SimpleServiceDiscovery.services.forEach(function(service) {
+ if (service.mirror) {
+ haveMirror = true;
+ }
+ });
+ if (!haveMirror) {
+ NativeWindow.menu.remove(this.mirrorStartMenuId);
+ this.mirrorStartMenuId = -1;
+ }
+ }
+ },
+
+ isCastingEnabled: function isCastingEnabled() {
+ return Services.prefs.getBoolPref("browser.casting.enabled");
+ },
+
+ isMirroringEnabled: function isMirroringEnabled() {
+ return Services.prefs.getBoolPref("browser.mirroring.enabled");
+ },
+
+ observe: function (aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "Casting:Play":
+ if (this.session && this.session.remoteMedia.status == "paused") {
+ this.session.remoteMedia.play();
+ }
+ break;
+ case "Casting:Pause":
+ if (this.session && this.session.remoteMedia.status == "started") {
+ this.session.remoteMedia.pause();
+ }
+ break;
+ case "Casting:Stop":
+ if (this.session) {
+ this.closeExternal();
+ }
+ break;
+ case "Casting:Mirror":
+ {
+ Cu.import("resource://gre/modules/TabMirror.jsm");
+ this.tabMirror = new TabMirror(aData, window);
+ NativeWindow.menu.update(this.mirrorStartMenuId, { visible: false });
+ NativeWindow.menu.update(this.mirrorStopMenuId, { visible: true });
+ }
+ break;
+ case "ssdp-service-found":
+ this.serviceAdded(SimpleServiceDiscovery.findServiceForID(aData));
+ break;
+ case "ssdp-service-lost":
+ this.serviceLost(SimpleServiceDiscovery.findServiceForID(aData));
+ break;
+ case "application-background":
+ // Turn off polling while in the background
+ this._interval = SimpleServiceDiscovery.search(0);
+ SimpleServiceDiscovery.stopSearch();
+ break;
+ case "application-foreground":
+ // Turn polling on when app comes back to foreground
+ SimpleServiceDiscovery.search(this._interval);
+ break;
+ }
+ },
+
+ handleEvent: function(aEvent) {
+ switch (aEvent.type) {
+ case "TabSelect": {
+ let tab = BrowserApp.getTabForBrowser(aEvent.target);
+ this._updatePageActionForTab(tab, aEvent);
+ break;
+ }
+ case "pageshow": {
+ let tab = BrowserApp.getTabForWindow(aEvent.originalTarget.defaultView);
+ this._updatePageActionForTab(tab, aEvent);
+ break;
+ }
+ case "playing":
+ case "ended": {
+ let video = aEvent.target;
+ if (video instanceof HTMLVideoElement) {
+ // If playing, send the <video>, but if ended we send nothing to shutdown the pageaction
+ this._updatePageActionForVideo(aEvent.type === "playing" ? video : null);
+ }
+ break;
+ }
+ case "MozAutoplayMediaBlocked": {
+ if (this._bound && this._bound.has(aEvent.target)) {
+ aEvent.target.dispatchEvent(new CustomEvent("MozNoControlsBlockedVideo"));
+ } else {
+ if (!this._blocked) {
+ this._blocked = new WeakMap;
+ }
+ this._blocked.set(aEvent.target, true);
+ }
+ break;
+ }
+ case "MozNoControlsVideoBindingAttached": {
+ if (!this._bound) {
+ this._bound = new WeakMap;
+ }
+ this._bound.set(aEvent.target, true);
+ if (this._blocked && this._blocked.has(aEvent.target)) {
+ this._blocked.delete(aEvent.target);
+ aEvent.target.dispatchEvent(new CustomEvent("MozNoControlsBlockedVideo"));
+ }
+ break;
+ }
+ }
+ },
+
+ _sendEventToVideo: function _sendEventToVideo(aElement, aData) {
+ let event = aElement.ownerDocument.createEvent("CustomEvent");
+ event.initCustomEvent("media-videoCasting", false, true, JSON.stringify(aData));
+ aElement.dispatchEvent(event);
+ },
+
+ handleVideoBindingAttached: function handleVideoBindingAttached(aTab, aEvent) {
+ // Let's figure out if we have everything needed to cast a video. The binding
+ // defaults to |false| so we only need to send an event if |true|.
+ let video = aEvent.target;
+ if (!(video instanceof HTMLVideoElement)) {
+ return;
+ }
+
+ if (SimpleServiceDiscovery.services.length == 0) {
+ return;
+ }
+
+ this.getVideo(video, 0, 0, (aBundle) => {
+ // Let the binding know casting is allowed
+ if (aBundle) {
+ this._sendEventToVideo(aBundle.element, { allow: true });
+ }
+ });
+ },
+
+ handleVideoBindingCast: function handleVideoBindingCast(aTab, aEvent) {
+ // The binding wants to start a casting session
+ let video = aEvent.target;
+ if (!(video instanceof HTMLVideoElement)) {
+ return;
+ }
+
+ // Close an existing session first. closeExternal has checks for an exsting
+ // session and handles remote and video binding shutdown.
+ this.closeExternal();
+
+ // Start the new session
+ UITelemetry.addEvent("cast.1", "button", null);
+ this.openExternal(video, 0, 0);
+ },
+
+ makeURI: function makeURI(aURL, aOriginCharset, aBaseURI) {
+ return Services.io.newURI(aURL, aOriginCharset, aBaseURI);
+ },
+
+ allowableExtension: function(aURI, aExtensions) {
+ return (aURI instanceof Ci.nsIURL) && aExtensions.indexOf(aURI.fileExtension) != -1;
+ },
+
+ allowableMimeType: function(aType, aTypes) {
+ return aTypes.indexOf(aType) != -1;
+ },
+
+ // This method will look at the aElement (or try to find a video at aX, aY) that has
+ // a castable source. If found, aCallback will be called with a JSON meta bundle. If
+ // no castable source was found, aCallback is called with null.
+ getVideo: function(aElement, aX, aY, aCallback) {
+ let extensions = SimpleServiceDiscovery.getSupportedExtensions();
+ let types = SimpleServiceDiscovery.getSupportedMimeTypes();
+
+ // Fast path: Is the given element a video element?
+ if (aElement instanceof HTMLVideoElement) {
+ // If we found a video element, no need to look further, even if no
+ // castable video source is found.
+ this._getVideo(aElement, types, extensions, aCallback);
+ return;
+ }
+
+ // Maybe this is an overlay, with the video element under it.
+ // Use the (x, y) location to guess at a <video> element.
+
+ // The context menu system will keep walking up the DOM giving us a chance
+ // to find an element we match. When it hits <html> things can go BOOM.
+ try {
+ let elements = aElement.ownerDocument.querySelectorAll("video");
+ for (let element of elements) {
+ // Look for a video element contained in the overlay bounds
+ let rect = element.getBoundingClientRect();
+ if (aY >= rect.top && aX >= rect.left && aY <= rect.bottom && aX <= rect.right) {
+ // Once we find a <video> under the overlay, we check it and exit.
+ this._getVideo(element, types, extensions, aCallback);
+ return;
+ }
+ }
+ } catch(e) {}
+ },
+
+ _getContentTypeForURI: function(aURI, aElement, aCallback) {
+ let channel;
+ try {
+ let secFlags = Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS;
+ if (aElement.crossOrigin) {
+ secFlags = Ci.nsILoadInfo.SEC_REQUIRE_CORS_DATA_INHERITS;
+ if (aElement.crossOrigin === "use-credentials") {
+ secFlags |= Ci.nsILoadInfo.SEC_COOKIES_INCLUDE;
+ }
+ }
+ channel = NetUtil.newChannel({
+ uri: aURI,
+ loadingNode: aElement,
+ securityFlags: secFlags,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_VIDEO
+ });
+ } catch(e) {
+ aCallback(null);
+ return;
+ }
+
+ let listener = {
+ onStartRequest: function(request, context) {
+ switch (channel.responseStatus) {
+ case 301:
+ case 302:
+ case 303:
+ request.cancel(0);
+ let location = channel.getResponseHeader("Location");
+ CastingApps._getContentTypeForURI(CastingApps.makeURI(location), aElement, aCallback);
+ break;
+ default:
+ aCallback(channel.contentType);
+ request.cancel(0);
+ break;
+ }
+ },
+ onStopRequest: function(request, context, statusCode) {},
+ onDataAvailable: function(request, context, stream, offset, count) {}
+ };
+
+ if (channel) {
+ channel.asyncOpen2(listener);
+ } else {
+ aCallback(null);
+ }
+ },
+
+ // Because this method uses a callback, make sure we return ASAP if we know
+ // we have a castable video source.
+ _getVideo: function(aElement, aTypes, aExtensions, aCallback) {
+ // Keep a list of URIs we need for an async mimetype check
+ let asyncURIs = [];
+
+ // Grab the poster attribute from the <video>
+ let posterURL = aElement.poster;
+
+ // First, look to see if the <video> has a src attribute
+ let sourceURL = aElement.src;
+
+ // If empty, try the currentSrc
+ if (!sourceURL) {
+ sourceURL = aElement.currentSrc;
+ }
+
+ if (sourceURL) {
+ // Use the file extension to guess the mime type
+ let sourceURI = this.makeURI(sourceURL, null, this.makeURI(aElement.baseURI));
+ if (this.allowableExtension(sourceURI, aExtensions)) {
+ aCallback({ element: aElement, source: sourceURI.spec, poster: posterURL, sourceURI: sourceURI});
+ return;
+ }
+
+ if (aElement.type) {
+ // Fast sync check
+ if (this.allowableMimeType(aElement.type, aTypes)) {
+ aCallback({ element: aElement, source: sourceURI.spec, poster: posterURL, sourceURI: sourceURI, type: aElement.type });
+ return;
+ }
+ }
+
+ // Delay the async check until we sync scan all possible URIs
+ asyncURIs.push(sourceURI);
+ }
+
+ // Next, look to see if there is a <source> child element that meets
+ // our needs
+ let sourceNodes = aElement.getElementsByTagName("source");
+ for (let sourceNode of sourceNodes) {
+ let sourceURI = this.makeURI(sourceNode.src, null, this.makeURI(sourceNode.baseURI));
+
+ // Using the type attribute is our ideal way to guess the mime type. Otherwise,
+ // fallback to using the file extension to guess the mime type
+ if (this.allowableExtension(sourceURI, aExtensions)) {
+ aCallback({ element: aElement, source: sourceURI.spec, poster: posterURL, sourceURI: sourceURI, type: sourceNode.type });
+ return;
+ }
+
+ if (sourceNode.type) {
+ // Fast sync check
+ if (this.allowableMimeType(sourceNode.type, aTypes)) {
+ aCallback({ element: aElement, source: sourceURI.spec, poster: posterURL, sourceURI: sourceURI, type: sourceNode.type });
+ return;
+ }
+ }
+
+ // Delay the async check until we sync scan all possible URIs
+ asyncURIs.push(sourceURI);
+ }
+
+ // Helper method that walks the array of possible URIs, fetching the mimetype as we go.
+ // As soon as we find a good sourceURL, avoid firing the callback any further
+ var _getContentTypeForURIs = (aURIs) => {
+ // Do an async fetch to figure out the mimetype of the source video
+ let sourceURI = aURIs.pop();
+ this._getContentTypeForURI(sourceURI, aElement, (aType) => {
+ if (this.allowableMimeType(aType, aTypes)) {
+ // We found a supported mimetype.
+ aCallback({ element: aElement, source: sourceURI.spec, poster: posterURL, sourceURI: sourceURI, type: aType });
+ } else {
+ // This URI was not a supported mimetype, so let's try the next, if we have more.
+ if (aURIs.length > 0) {
+ _getContentTypeForURIs(aURIs);
+ } else {
+ // We were not able to find a supported mimetype.
+ aCallback(null);
+ }
+ }
+ });
+ }
+
+ // If we didn't find a good URI directly, let's look using async methods.
+ if (asyncURIs.length > 0) {
+ _getContentTypeForURIs(asyncURIs);
+ }
+ },
+
+ // This code depends on handleVideoBindingAttached setting mozAllowCasting
+ // so we can quickly figure out if the video is castable
+ isVideoCastable: function(aElement, aX, aY) {
+ // Use the flag set when the <video> binding was created as the check
+ if (aElement instanceof HTMLVideoElement) {
+ return aElement.mozAllowCasting;
+ }
+
+ // This is called by the context menu system and the system will keep
+ // walking up the DOM giving us a chance to find an element we match.
+ // When it hits <html> things can go BOOM.
+ try {
+ // Maybe this is an overlay, with the video element under it
+ // Use the (x, y) location to guess at a <video> element
+ let elements = aElement.ownerDocument.querySelectorAll("video");
+ for (let element of elements) {
+ // Look for a video element contained in the overlay bounds
+ let rect = element.getBoundingClientRect();
+ if (aY >= rect.top && aX >= rect.left && aY <= rect.bottom && aX <= rect.right) {
+ // Use the flag set when the <video> binding was created as the check
+ return element.mozAllowCasting;
+ }
+ }
+ } catch(e) {}
+
+ return false;
+ },
+
+ filterCast: {
+ matches: function(aElement, aX, aY) {
+ // This behavior matches the pageaction: As long as a video is castable,
+ // we can cast it, even if it's already being cast to a device.
+ if (SimpleServiceDiscovery.services.length == 0)
+ return false;
+ return CastingApps.isVideoCastable(aElement, aX, aY);
+ }
+ },
+
+ pageAction: {
+ click: function() {
+ // Since this is a pageaction, we use the selected browser
+ let browser = BrowserApp.selectedBrowser;
+ if (!browser) {
+ return;
+ }
+
+ // Look for a castable <video> that is playing, and start casting it
+ let videos = browser.contentDocument.querySelectorAll("video");
+ for (let video of videos) {
+ if (!video.paused && video.mozAllowCasting) {
+ UITelemetry.addEvent("cast.1", "pageaction", null);
+ CastingApps.openExternal(video, 0, 0);
+ return;
+ }
+ }
+ }
+ },
+
+ _findCastableVideo: function _findCastableVideo(aBrowser) {
+ if (!aBrowser) {
+ return null;
+ }
+
+ // Scan for a <video> being actively cast. Also look for a castable <video>
+ // on the page.
+ let castableVideo = null;
+ let videos = aBrowser.contentDocument.querySelectorAll("video");
+ for (let video of videos) {
+ if (video.mozIsCasting) {
+ // This <video> is cast-active. Break out of loop.
+ return video;
+ }
+
+ if (!video.paused && video.mozAllowCasting) {
+ // This <video> is cast-ready. Keep looking so cast-active could be found.
+ castableVideo = video;
+ }
+ }
+
+ // Could be null
+ return castableVideo;
+ },
+
+ _updatePageActionForTab: function _updatePageActionForTab(aTab, aEvent) {
+ // We only care about events on the selected tab
+ if (aTab != BrowserApp.selectedTab) {
+ return;
+ }
+
+ // Update the page action, scanning for a castable <video>
+ this._updatePageAction();
+ },
+
+ _updatePageActionForVideo: function _updatePageActionForVideo(aVideo) {
+ this._updatePageAction(aVideo);
+ },
+
+ _updatePageAction: function _updatePageAction(aVideo) {
+ // Remove any exising pageaction first, in case state changes or we don't have
+ // a castable video
+ if (this.pageAction.id) {
+ PageActions.remove(this.pageAction.id);
+ delete this.pageAction.id;
+ }
+
+ if (!aVideo) {
+ aVideo = this._findCastableVideo(BrowserApp.selectedBrowser);
+ if (!aVideo) {
+ return;
+ }
+ }
+
+ // We only show pageactions if the <video> is from the selected tab
+ if (BrowserApp.selectedTab != BrowserApp.getTabForWindow(aVideo.ownerDocument.defaultView.top)) {
+ return;
+ }
+
+ // We check for two state here:
+ // 1. The video is actively being cast
+ // 2. The video is allowed to be cast and is currently playing
+ // Both states have the same action: Show the cast page action
+ if (aVideo.mozIsCasting) {
+ this.pageAction.id = PageActions.add({
+ title: Strings.browser.GetStringFromName("contextmenu.sendToDevice"),
+ icon: "drawable://casting_active",
+ clickCallback: this.pageAction.click,
+ important: true
+ });
+ } else if (aVideo.mozAllowCasting) {
+ this.pageAction.id = PageActions.add({
+ title: Strings.browser.GetStringFromName("contextmenu.sendToDevice"),
+ icon: "drawable://casting",
+ clickCallback: this.pageAction.click,
+ important: true
+ });
+ }
+ },
+
+ prompt: function(aCallback, aFilterFunc) {
+ let items = [];
+ let filteredServices = [];
+ SimpleServiceDiscovery.services.forEach(function(aService) {
+ let item = {
+ label: aService.friendlyName,
+ selected: false
+ };
+ if (!aFilterFunc || aFilterFunc(aService)) {
+ filteredServices.push(aService);
+ items.push(item);
+ }
+ });
+
+ if (items.length == 0) {
+ return;
+ }
+
+ let prompt = new Prompt({
+ title: Strings.browser.GetStringFromName("casting.sendToDevice")
+ }).setSingleChoiceItems(items).show(function(data) {
+ let selected = data.button;
+ let service = selected == -1 ? null : filteredServices[selected];
+ if (aCallback)
+ aCallback(service);
+ });
+ },
+
+ handleContextMenu: function(aElement, aX, aY) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_cast");
+ UITelemetry.addEvent("cast.1", "contextmenu", null);
+ this.openExternal(aElement, aX, aY);
+ },
+
+ openExternal: function(aElement, aX, aY) {
+ // Start a second screen media service
+ this.getVideo(aElement, aX, aY, this._openExternal.bind(this));
+ },
+
+ _openExternal: function(aVideo) {
+ if (!aVideo) {
+ return;
+ }
+
+ function filterFunc(aService) {
+ return this.allowableExtension(aVideo.sourceURI, aService.extensions) || this.allowableMimeType(aVideo.type, aService.types);
+ }
+
+ this.prompt(function(aService) {
+ if (!aService)
+ return;
+
+ // Make sure we have a player app for the given service
+ let app = SimpleServiceDiscovery.findAppForService(aService);
+ if (!app)
+ return;
+
+ if (aVideo.element) {
+ aVideo.title = aVideo.element.ownerDocument.defaultView.top.document.title;
+
+ // If the video is currently playing on the device, pause it
+ if (!aVideo.element.paused) {
+ aVideo.element.pause();
+ }
+ }
+
+ app.stop(function() {
+ app.start(function(aStarted) {
+ if (!aStarted) {
+ dump("CastingApps: Unable to start app");
+ return;
+ }
+
+ app.remoteMedia(function(aRemoteMedia) {
+ if (!aRemoteMedia) {
+ dump("CastingApps: Failed to create remotemedia");
+ return;
+ }
+
+ this.session = {
+ service: aService,
+ app: app,
+ remoteMedia: aRemoteMedia,
+ data: {
+ title: aVideo.title,
+ source: aVideo.source,
+ poster: aVideo.poster
+ },
+ videoRef: Cu.getWeakReference(aVideo.element)
+ };
+ }.bind(this), this);
+ }.bind(this));
+ }.bind(this));
+ }.bind(this), filterFunc.bind(this));
+ },
+
+ closeExternal: function() {
+ if (!this.session) {
+ return;
+ }
+
+ this.session.remoteMedia.shutdown();
+ this._shutdown();
+ },
+
+ _shutdown: function() {
+ if (!this.session) {
+ return;
+ }
+
+ this.session.app.stop();
+ let video = this.session.videoRef.get();
+ if (video) {
+ this._sendEventToVideo(video, { active: false });
+ this._updatePageAction();
+ }
+
+ delete this.session;
+ },
+
+ // RemoteMedia callback API methods
+ onRemoteMediaStart: function(aRemoteMedia) {
+ if (!this.session) {
+ return;
+ }
+
+ aRemoteMedia.load(this.session.data);
+ Messaging.sendRequest({ type: "Casting:Started", device: this.session.service.friendlyName });
+
+ let video = this.session.videoRef.get();
+ if (video) {
+ this._sendEventToVideo(video, { active: true });
+ this._updatePageAction(video);
+ }
+ },
+
+ onRemoteMediaStop: function(aRemoteMedia) {
+ Messaging.sendRequest({ type: "Casting:Stopped" });
+ this._shutdown();
+ },
+
+ onRemoteMediaStatus: function(aRemoteMedia) {
+ if (!this.session) {
+ return;
+ }
+
+ let status = aRemoteMedia.status;
+ switch (status) {
+ case "started":
+ Messaging.sendRequest({ type: "Casting:Playing" });
+ break;
+ case "paused":
+ Messaging.sendRequest({ type: "Casting:Paused" });
+ break;
+ case "completed":
+ this.closeExternal();
+ break;
+ }
+ }
+};
diff --git a/mobile/android/chrome/content/ConsoleAPI.js b/mobile/android/chrome/content/ConsoleAPI.js
new file mode 100644
index 000000000..6ba4c1195
--- /dev/null
+++ b/mobile/android/chrome/content/ConsoleAPI.js
@@ -0,0 +1,96 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var ConsoleAPI = {
+ 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:
+ 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;
+ }
+};
diff --git a/mobile/android/chrome/content/EmbedRT.js b/mobile/android/chrome/content/EmbedRT.js
new file mode 100644
index 000000000..8e35a3b63
--- /dev/null
+++ b/mobile/android/chrome/content/EmbedRT.js
@@ -0,0 +1,82 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPI",
+ "resource://gre/modules/Console.jsm");
+
+/*
+ * Collection of methods and features specific to using a GeckoView instance.
+ * The code is isolated from browser.js for code size and performance reasons.
+ */
+var EmbedRT = {
+ _scopes: {},
+
+ observe: function(subject, topic, data) {
+ switch(topic) {
+ case "GeckoView:ImportScript":
+ this.importScript(data);
+ break;
+ }
+ },
+
+ /*
+ * Loads a script file into a sandbox and calls an optional load function
+ */
+ importScript: function(scriptURL) {
+ if (scriptURL in this._scopes) {
+ return;
+ }
+
+ let principal = Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal);
+
+ let sandbox = new Cu.Sandbox(principal,
+ {
+ sandboxName: scriptURL,
+ wantGlobalProperties: ["indexedDB"]
+ }
+ );
+
+ sandbox["console"] = new ConsoleAPI({ consoleID: "script/" + scriptURL });
+ sandbox["GeckoView"] = {
+ sendRequest: function(data) {
+ if (!data) {
+ throw new Error("Invalid parameter: 'data' can't be null.");
+ }
+
+ let message = { type: "GeckoView:Message", data: data };
+ Messaging.sendRequest(message);
+ },
+ sendRequestForResult: function(data) {
+ if (!data) {
+ throw new Error("Invalid parameter: 'data' can't be null.");
+ }
+
+ let message = { type: "GeckoView:Message", data: data };
+ return Messaging.sendRequestForResult(message);
+ }
+ };
+
+ // As we don't want our caller to control the JS version used for the
+ // script file, we run loadSubScript within the context of the
+ // sandbox with the latest JS version set explicitly.
+ sandbox.__SCRIPT_URI_SPEC__ = scriptURL;
+ Cu.evalInSandbox("Components.classes['@mozilla.org/moz/jssubscript-loader;1'].createInstance(Components.interfaces.mozIJSSubScriptLoader).loadSubScript(__SCRIPT_URI_SPEC__);", sandbox, "ECMAv5");
+
+ this._scopes[scriptURL] = sandbox;
+
+ if ("load" in sandbox) {
+ let params = {
+ window: window,
+ resourceURI: scriptURL,
+ };
+
+ try {
+ sandbox["load"](params);
+ } catch(e) {
+ dump("Exception calling 'load' method in script: " + scriptURL + "\n" + e);
+ }
+ }
+ }
+};
diff --git a/mobile/android/chrome/content/FeedHandler.js b/mobile/android/chrome/content/FeedHandler.js
new file mode 100644
index 000000000..91d73ee8d
--- /dev/null
+++ b/mobile/android/chrome/content/FeedHandler.js
@@ -0,0 +1,120 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var FeedHandler = {
+ PREF_CONTENTHANDLERS_BRANCH: "browser.contentHandlers.types.",
+ TYPE_MAYBE_FEED: "application/vnd.mozilla.maybe.feed",
+
+ _contentTypes: null,
+
+ getContentHandlers: function fh_getContentHandlers(contentType) {
+ if (!this._contentTypes)
+ this.loadContentHandlers();
+
+ if (!(contentType in this._contentTypes))
+ return [];
+
+ return this._contentTypes[contentType];
+ },
+
+ loadContentHandlers: function fh_loadContentHandlers() {
+ this._contentTypes = {};
+
+ let kids = Services.prefs.getBranch(this.PREF_CONTENTHANDLERS_BRANCH).getChildList("");
+
+ // First get the numbers of the providers by getting all ###.uri prefs
+ let nums = [];
+ for (let i = 0; i < kids.length; i++) {
+ let match = /^(\d+)\.uri$/.exec(kids[i]);
+ if (!match)
+ continue;
+ else
+ nums.push(match[1]);
+ }
+
+ // Sort them, to get them back in order
+ nums.sort(function(a, b) { return a - b; });
+
+ // Now register them
+ for (let i = 0; i < nums.length; i++) {
+ let branch = Services.prefs.getBranch(this.PREF_CONTENTHANDLERS_BRANCH + nums[i] + ".");
+ let vals = branch.getChildList("");
+ if (vals.length == 0)
+ return;
+
+ try {
+ let type = branch.getCharPref("type");
+ let uri = branch.getComplexValue("uri", Ci.nsIPrefLocalizedString).data;
+ let title = branch.getComplexValue("title", Ci.nsIPrefLocalizedString).data;
+
+ if (!(type in this._contentTypes))
+ this._contentTypes[type] = [];
+ this._contentTypes[type].push({ contentType: type, uri: uri, name: title });
+ }
+ catch(ex) {}
+ }
+ },
+
+ observe: function fh_observe(aSubject, aTopic, aData) {
+ if (aTopic === "Feeds:Subscribe") {
+ let args = JSON.parse(aData);
+ let tab = BrowserApp.getTabForId(args.tabId);
+ if (!tab)
+ return;
+
+ let browser = tab.browser;
+ let feeds = browser.feeds;
+ if (feeds == null)
+ return;
+
+ // First, let's decide on which feed to subscribe
+ let feedIndex = -1;
+ if (feeds.length > 1) {
+ let p = new Prompt({
+ window: browser.contentWindow,
+ title: Strings.browser.GetStringFromName("feedHandler.chooseFeed")
+ }).setSingleChoiceItems(feeds.map(function(feed) {
+ return { label: feed.title || feed.href }
+ })).show((function(data) {
+ feedIndex = data.button;
+ if (feedIndex == -1)
+ return;
+
+ this.loadFeed(feeds[feedIndex], browser);
+ }).bind(this));
+ return;
+ }
+
+ this.loadFeed(feeds[0], browser);
+ }
+ },
+
+ loadFeed: function fh_loadFeed(aFeed, aBrowser) {
+ let feedURL = aFeed.href;
+
+ // Next, we decide on which service to send the feed
+ let handlers = this.getContentHandlers(this.TYPE_MAYBE_FEED);
+ if (handlers.length == 0)
+ return;
+
+ // JSON for Prompt
+ let p = new Prompt({
+ window: aBrowser.contentWindow,
+ title: Strings.browser.GetStringFromName("feedHandler.subscribeWith")
+ }).setSingleChoiceItems(handlers.map(function(handler) {
+ return { label: handler.name };
+ })).show(function(data) {
+ if (data.button == -1)
+ return;
+
+ // Merge the handler URL and the feed URL
+ let readerURL = handlers[data.button].uri;
+ readerURL = readerURL.replace(/%s/gi, encodeURIComponent(feedURL));
+
+ // Open the resultant URL in a new tab
+ BrowserApp.addTab(readerURL, { parentId: BrowserApp.selectedTab.id });
+ });
+ }
+};
diff --git a/mobile/android/chrome/content/Feedback.js b/mobile/android/chrome/content/Feedback.js
new file mode 100644
index 000000000..8727c46c3
--- /dev/null
+++ b/mobile/android/chrome/content/Feedback.js
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var Feedback = {
+
+ get _feedbackURL() {
+ delete this._feedbackURL;
+ return this._feedbackURL = Services.urlFormatter.formatURLPref("app.feedbackURL");
+ },
+
+ observe: function(aMessage, aTopic, aData) {
+ if (aTopic !== "Feedback:Show") {
+ return;
+ }
+
+ // Don't prompt for feedback in distribution builds.
+ try {
+ Services.prefs.getCharPref("distribution.id");
+ return;
+ } catch (e) {}
+
+ let url = this._feedbackURL;
+ let browser = BrowserApp.selectOrAddTab(url, { parentId: BrowserApp.selectedTab.id }).browser;
+
+ browser.addEventListener("FeedbackClose", this, false, true);
+ browser.addEventListener("FeedbackMaybeLater", this, false, true);
+
+ // Dispatch a custom event to the page content when feedback is prompted by the browser.
+ // This will be used by the page to determine it's being loaded directly by the browser,
+ // instead of by the user visiting the page, e.g. through browser history.
+ function loadListener(event) {
+ browser.removeEventListener("DOMContentLoaded", loadListener, false);
+ browser.contentDocument.dispatchEvent(new CustomEvent("FeedbackPrompted"));
+ }
+ browser.addEventListener("DOMContentLoaded", loadListener, false);
+ },
+
+ handleEvent: function(event) {
+ if (!this._isAllowed(event.target)) {
+ return;
+ }
+
+ switch (event.type) {
+ case "FeedbackClose":
+ // Do nothing.
+ break;
+
+ case "FeedbackMaybeLater":
+ Messaging.sendRequest({ type: "Feedback:MaybeLater" });
+ break;
+ }
+
+ let win = event.target.ownerDocument.defaultView.top;
+ BrowserApp.closeTab(BrowserApp.getTabForWindow(win));
+ },
+
+ _isAllowed: function(node) {
+ let uri = node.ownerDocument.documentURIObject;
+ let feedbackURI = Services.io.newURI(this._feedbackURL, null, null);
+ return uri.prePath === feedbackURI.prePath;
+ }
+};
diff --git a/mobile/android/chrome/content/FindHelper.js b/mobile/android/chrome/content/FindHelper.js
new file mode 100644
index 000000000..037b182d6
--- /dev/null
+++ b/mobile/android/chrome/content/FindHelper.js
@@ -0,0 +1,197 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var FindHelper = {
+ _finder: null,
+ _targetTab: null,
+ _initialViewport: null,
+ _viewportChanged: false,
+ _result: null,
+
+ // Start of nsIObserver implementation.
+
+ observe: function(aMessage, aTopic, aData) {
+ switch(aTopic) {
+ case "FindInPage:Opened": {
+ this._findOpened();
+ break;
+ }
+
+ case "Tab:Selected": {
+ // Allow for page switching.
+ this._uninit();
+ break;
+ }
+
+ case "FindInPage:Closed":
+ this._uninit();
+ this._findClosed();
+ break;
+ }
+ },
+
+ /**
+ * When the FindInPageBar opens/ becomes visible, it's time to:
+ * 1. Add listeners for other message types sent from the FindInPageBar
+ * 2. initialize the Finder instance, if necessary.
+ */
+ _findOpened: function() {
+ Messaging.addListener(data => this.doFind(data), "FindInPage:Find");
+ Messaging.addListener(data => this.findAgain(data, false), "FindInPage:Next");
+ Messaging.addListener(data => this.findAgain(data, true), "FindInPage:Prev");
+
+ // Initialize the finder component for the current page by performing a fake find.
+ this._init();
+ this._finder.requestMatchesCount("");
+ },
+
+ /**
+ * Fetch the Finder instance from the active tabs' browser and start tracking
+ * the active viewport.
+ */
+ _init: function() {
+ // If there's no find in progress, start one.
+ if (this._finder) {
+ return;
+ }
+
+ this._targetTab = BrowserApp.selectedTab;
+ try {
+ this._finder = this._targetTab.browser.finder;
+ } catch (e) {
+ throw new Error("FindHelper: " + e + "\n" +
+ "JS stack: \n" + (e.stack || Components.stack.formattedStack));
+ }
+
+ this._finder.addResultListener(this);
+ this._initialViewport = JSON.stringify(this._targetTab.getViewport());
+ this._viewportChanged = false;
+ },
+
+ /**
+ * Detach from the Finder instance (so stop listening for messages) and stop
+ * tracking the active viewport.
+ */
+ _uninit: function() {
+ // If there's no find in progress, there's nothing to clean up.
+ if (!this._finder) {
+ return;
+ }
+
+ this._finder.removeSelection();
+ this._finder.removeResultListener(this);
+ this._finder = null;
+ this._targetTab = null;
+ this._initialViewport = null;
+ this._viewportChanged = false;
+ },
+
+ /**
+ * When the FindInPageBar closes, it's time to stop listening for its messages.
+ */
+ _findClosed: function() {
+ Messaging.removeListener("FindInPage:Find");
+ Messaging.removeListener("FindInPage:Next");
+ Messaging.removeListener("FindInPage:Prev");
+ },
+
+ /**
+ * Start an asynchronous find-in-page operation, using the current Finder
+ * instance and request to count the amount of matches.
+ * If no Finder instance is currently active, we'll lazily initialize it here.
+ *
+ * @param {String} searchString Word to search for in the current document
+ * @return {Object} Echo of the current find action
+ */
+ doFind: function(searchString) {
+ if (!this._finder) {
+ this._init();
+ }
+
+ this._finder.fastFind(searchString, false);
+ return { searchString, findBackwards: false };
+ },
+
+ /**
+ * Restart the same find-in-page operation as before via `doFind()`. If we
+ * haven't called `doFind()`, we simply kick off a regular find.
+ *
+ * @param {String} searchString Word to search for in the current document
+ * @param {Boolean} findBackwards Direction to search in
+ * @return {Object} Echo of the current find action
+ */
+ findAgain: function(searchString, findBackwards) {
+ // This always happens if the user taps next/previous after re-opening the
+ // search bar, and not only forces _init() but also an initial fastFind(STRING)
+ // before any findAgain(DIRECTION).
+ if (!this._finder) {
+ return this.doFind(searchString);
+ }
+
+ this._finder.findAgain(findBackwards, false, false);
+ return { searchString, findBackwards };
+ },
+
+ // Start of Finder.jsm listener implementation.
+
+ /**
+ * Pass along the count results to FindInPageBar for display. The result that
+ * is sent to the FindInPageBar is augmented with the current find-in-page count
+ * limit.
+ *
+ * @param {Object} result Result coming from the Finder instance that contains
+ * the following properties:
+ * - {Number} total The total amount of matches found
+ * - {Number} current The index of current found range
+ * in the document
+ */
+ onMatchesCountResult: function(result) {
+ this._result = result;
+
+ Messaging.sendRequest(Object.assign({
+ type: "FindInPage:MatchesCountResult"
+ }, this._result));
+ },
+
+ /**
+ * When a find-in-page action finishes, this method is invoked. This is mainly
+ * used at the moment to detect if the current viewport has changed, which might
+ * be indicated by not finding a string in the current page.
+ *
+ * @param {Object} aData A dictionary, representing the find result, which
+ * contains the following properties:
+ * - {String} searchString Word that was searched for
+ * in the current document
+ * - {Number} result One of the following
+ * Ci.nsITypeAheadFind.* result
+ * indicators: FIND_FOUND,
+ * FIND_NOTFOUND, FIND_WRAPPED,
+ * FIND_PENDING
+ * - {Boolean} findBackwards Whether the search direction
+ * was backwards
+ * - {Boolean} findAgain Whether the previous search
+ * was repeated
+ * - {Boolean} drawOutline Whether we may (re-)draw the
+ * outline of a hyperlink
+ * - {Boolean} linksOnly Whether links-only mode was
+ * active
+ */
+ onFindResult: function(aData) {
+ if (aData.result == Ci.nsITypeAheadFind.FIND_NOTFOUND) {
+ if (this._viewportChanged) {
+ if (this._targetTab != BrowserApp.selectedTab) {
+ // this should never happen
+ Cu.reportError("Warning: selected tab changed during find!");
+ // fall through and restore viewport on the initial tab anyway
+ }
+ this._targetTab.sendViewportUpdate();
+ }
+ } else {
+ // Disabled until bug 1014113 is fixed
+ // ZoomHelper.zoomToRect(aData.rect);
+ this._viewportChanged = true;
+ }
+ }
+};
diff --git a/mobile/android/chrome/content/InputWidgetHelper.js b/mobile/android/chrome/content/InputWidgetHelper.js
new file mode 100644
index 000000000..9c753bd7b
--- /dev/null
+++ b/mobile/android/chrome/content/InputWidgetHelper.js
@@ -0,0 +1,98 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var InputWidgetHelper = {
+ _uiBusy: false,
+
+ handleEvent: function(aEvent) {
+ this.handleClick(aEvent.target);
+ },
+
+ handleClick: function(aTarget) {
+ // if we're busy looking at a InputWidget we want to eat any clicks that
+ // come to us, but not to process them
+ if (this._uiBusy || !this.hasInputWidget(aTarget) || this._isDisabledElement(aTarget))
+ return;
+
+ this._uiBusy = true;
+ this.show(aTarget);
+ this._uiBusy = false;
+ },
+
+ show: function(aElement) {
+ let type = aElement.getAttribute('type');
+ let p = new Prompt({
+ window: aElement.ownerDocument.defaultView,
+ title: Strings.browser.GetStringFromName("inputWidgetHelper." + aElement.getAttribute('type')),
+ buttons: [
+ Strings.browser.GetStringFromName("inputWidgetHelper.set"),
+ Strings.browser.GetStringFromName("inputWidgetHelper.clear"),
+ Strings.browser.GetStringFromName("inputWidgetHelper.cancel")
+ ],
+ }).addDatePicker({
+ value: aElement.value,
+ type: type,
+ min: aElement.min,
+ max: aElement.max,
+ }).show((function(data) {
+ let changed = false;
+ if (data.button == -1) {
+ // This type is not supported with this android version.
+ return;
+ }
+ if (data.button == 1) {
+ // The user cleared the value.
+ if (aElement.value != "") {
+ aElement.value = "";
+ changed = true;
+ }
+ } else if (data.button == 0) {
+ // Commit the new value.
+ if (aElement.value != data[type]) {
+ aElement.value = data[type + "0"];
+ changed = true;
+ }
+ }
+ // Else the user canceled the input.
+
+ if (changed)
+ this.fireOnChange(aElement);
+ }).bind(this));
+ },
+
+ hasInputWidget: function(aElement) {
+ if (!aElement instanceof HTMLInputElement)
+ return false;
+
+ let type = aElement.getAttribute('type');
+ if (type == "date" || type == "datetime" || type == "datetime-local" ||
+ type == "week" || type == "month" || type == "time") {
+ return true;
+ }
+
+ return false;
+ },
+
+ fireOnChange: function(aElement) {
+ let evt = aElement.ownerDocument.createEvent("Events");
+ evt.initEvent("change", true, true, aElement.defaultView, 0,
+ false, false,
+ false, false, null);
+ setTimeout(function() {
+ aElement.dispatchEvent(evt);
+ }, 0);
+ },
+
+ _isDisabledElement : function(aElement) {
+ let currentElement = aElement;
+ while (currentElement) {
+ if (currentElement.disabled)
+ return true;
+
+ currentElement = currentElement.parentElement;
+ }
+ return false;
+ }
+};
diff --git a/mobile/android/chrome/content/Linkify.js b/mobile/android/chrome/content/Linkify.js
new file mode 100644
index 000000000..3c757cc18
--- /dev/null
+++ b/mobile/android/chrome/content/Linkify.js
@@ -0,0 +1,108 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const LINKIFY_TIMEOUT = 0;
+
+function Linkifier() {
+ this._linkifyTimer = null;
+ this._phoneRegex = /(?:\s|^)[\+]?(\(?\d{1,8}\)?)?([- ]+\(?\d{1,8}\)?)+( ?(x|ext) ?\d{1,3})?(?:\s|$)/g;
+}
+
+Linkifier.prototype = {
+ _buildAnchor : function(aDoc, aNumberText) {
+ let anchorNode = aDoc.createElement("a");
+ let cleanedText = "";
+ for (let i = 0; i < aNumberText.length; i++) {
+ let c = aNumberText.charAt(i);
+ if ((c >= '0' && c <= '9') || c == '+') //assuming there is only the leading '+'.
+ cleanedText += c;
+ }
+ anchorNode.setAttribute("href", "tel:" + cleanedText);
+ let nodeText = aDoc.createTextNode(aNumberText);
+ anchorNode.appendChild(nodeText);
+ return anchorNode;
+ },
+
+ _linkifyNodeNumbers : function(aNodeToProcess, aDoc) {
+ let parent = aNodeToProcess.parentNode;
+ let nodeText = aNodeToProcess.nodeValue;
+
+ // Replacing the original text node with a sequence of
+ // |text before number|anchor with number|text after number nodes.
+ // Each step a couple of (optional) text node and anchor node are appended.
+ let anchorNode = null;
+ let m = null;
+ let startIndex = 0;
+ let prevNode = null;
+ while (m = this._phoneRegex.exec(nodeText)) {
+ anchorNode = this._buildAnchor(aDoc, nodeText.substr(m.index, m[0].length));
+
+ let textExistsBeforeNumber = (m.index > startIndex);
+ let nodeToAdd = null;
+ if (textExistsBeforeNumber)
+ nodeToAdd = aDoc.createTextNode(nodeText.substr(startIndex, m.index - startIndex));
+ else
+ nodeToAdd = anchorNode;
+
+ if (!prevNode) // first time, need to replace the whole node with the first new one.
+ parent.replaceChild(nodeToAdd, aNodeToProcess);
+ else
+ parent.insertBefore(nodeToAdd, prevNode.nextSibling); //inserts after.
+
+ if (textExistsBeforeNumber) // if we added the text node before the anchor, we still need to add the anchor node.
+ parent.insertBefore(anchorNode, nodeToAdd.nextSibling);
+
+ // next nodes need to be appended to this node.
+ prevNode = anchorNode;
+ startIndex = m.index + m[0].length;
+ }
+
+ // if some text is remaining after the last anchor.
+ if (startIndex > 0 && startIndex < nodeText.length) {
+ let lastNode = aDoc.createTextNode(nodeText.substr(startIndex));
+ parent.insertBefore(lastNode, prevNode.nextSibling);
+ return lastNode;
+ }
+ return anchorNode;
+ },
+
+ linkifyNumbers: function(aDoc) {
+ // Removing any installed timer in case the page has changed and a previous timer is still running.
+ if (this._linkifyTimer) {
+ clearTimeout(this._linkifyTimer);
+ this._linkifyTimer = null;
+ }
+
+ let filterNode = function (node) {
+ if (node.parentNode.tagName != 'A' &&
+ node.parentNode.tagName != 'SCRIPT' &&
+ node.parentNode.tagName != 'NOSCRIPT' &&
+ node.parentNode.tagName != 'STYLE' &&
+ node.parentNode.tagName != 'APPLET' &&
+ node.parentNode.tagName != 'TEXTAREA')
+ return NodeFilter.FILTER_ACCEPT;
+ else
+ return NodeFilter.FILTER_REJECT;
+ }
+
+ let nodeWalker = aDoc.createTreeWalker(aDoc.body, NodeFilter.SHOW_TEXT, filterNode, false);
+ let parseNode = function() {
+ let node = nodeWalker.nextNode();
+ if (!node) {
+ this._linkifyTimer = null;
+ return;
+ }
+ let lastAddedNode = this._linkifyNodeNumbers(node, aDoc);
+ // we assign a different timeout whether the node was processed or not.
+ if (lastAddedNode) {
+ nodeWalker.currentNode = lastAddedNode;
+ this._linkifyTimer = setTimeout(parseNode, LINKIFY_TIMEOUT);
+ } else {
+ this._linkifyTimer = setTimeout(parseNode, LINKIFY_TIMEOUT);
+ }
+ }.bind(this);
+
+ this._linkifyTimer = setTimeout(parseNode, LINKIFY_TIMEOUT);
+ }
+};
diff --git a/mobile/android/chrome/content/MasterPassword.js b/mobile/android/chrome/content/MasterPassword.js
new file mode 100644
index 000000000..d85fa928d
--- /dev/null
+++ b/mobile/android/chrome/content/MasterPassword.js
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
+
+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);
+
+ 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, "");
+ return true;
+ }
+ } catch(e) {
+ dump("MasterPassword.removePassword: " + e + "\n");
+ }
+ Snackbars.show(Strings.browser.GetStringFromName("masterPassword.incorrect"), Snackbars.LENGTH_LONG);
+ return false;
+ }
+};
diff --git a/mobile/android/chrome/content/MemoryObserver.js b/mobile/android/chrome/content/MemoryObserver.js
new file mode 100644
index 000000000..2bb3ae842
--- /dev/null
+++ b/mobile/android/chrome/content/MemoryObserver.js
@@ -0,0 +1,88 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var MemoryObserver = {
+ observe: function mo_observe(aSubject, aTopic, aData) {
+ if (aTopic == "memory-pressure") {
+ if (aData != "heap-minimize") {
+ this.handleLowMemory();
+ }
+ // The JS engine would normally GC on this notification, but since we
+ // disabled that in favor of this method (bug 669346), we should gc here.
+ // See bug 784040 for when this code was ported from XUL to native Fennec.
+ this.gc();
+ } else if (aTopic == "Memory:Dump") {
+ this.dumpMemoryStats(aData);
+ }
+ },
+
+ handleLowMemory: function() {
+ // do things to reduce memory usage here
+ if (!Services.prefs.getBoolPref("browser.tabs.disableBackgroundZombification")) {
+ let tabs = BrowserApp.tabs;
+ let selected = BrowserApp.selectedTab;
+ for (let i = 0; i < tabs.length; i++) {
+ if (tabs[i] != selected && !tabs[i].playingAudio) {
+ this.zombify(tabs[i]);
+ }
+ }
+ }
+
+ // Change some preferences temporarily for only this session
+ let defaults = Services.prefs.getDefaultBranch(null);
+
+ // Reduce the amount of decoded image data we keep around
+ defaults.setIntPref("image.mem.max_decoded_image_kb", 0);
+
+ // Stop using the bfcache
+ if (!Services.prefs.getBoolPref("browser.sessionhistory.bfcacheIgnoreMemoryPressure")) {
+ defaults.setIntPref("browser.sessionhistory.max_total_viewers", 0);
+ }
+ },
+
+ zombify: function(tab) {
+ let browser = tab.browser;
+ let data = browser.__SS_data;
+ let extra = browser.__SS_extdata;
+
+ // Notify any interested parties (e.g. the session store)
+ // that the original tab object is going to be destroyed
+ let evt = document.createEvent("UIEvents");
+ evt.initUIEvent("TabPreZombify", true, false, window, null);
+ browser.dispatchEvent(evt);
+
+ // We need this data to correctly create and position the new browser
+ // If this browser is already a zombie, fallback to the session data
+ let currentURL = browser.__SS_restore ? data.entries[0].url : browser.currentURI.spec;
+ let sibling = browser.nextSibling;
+ let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(browser);
+
+ tab.destroy();
+ tab.create(currentURL, { sibling: sibling, zombifying: true, delayLoad: true, isPrivate: isPrivate });
+
+ // Reattach session store data and flag this browser so it is restored on select
+ browser = tab.browser;
+ browser.__SS_data = data;
+ browser.__SS_extdata = extra;
+ browser.__SS_restore = true;
+ browser.setAttribute("pending", "true");
+
+ // Notify the session store to reattach its listeners to the new tab object
+ evt = document.createEvent("UIEvents");
+ evt.initUIEvent("TabPostZombify", true, false, window, null);
+ browser.dispatchEvent(evt);
+ },
+
+ gc: function() {
+ window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).garbageCollect();
+ Cu.forceGC();
+ },
+
+ dumpMemoryStats: function(aLabel) {
+ let memDumper = Cc["@mozilla.org/memory-info-dumper;1"].getService(Ci.nsIMemoryInfoDumper);
+ memDumper.dumpMemoryInfoToTempDir(aLabel, /* anonymize = */ false,
+ /* minimize = */ false);
+ },
+};
diff --git a/mobile/android/chrome/content/OfflineApps.js b/mobile/android/chrome/content/OfflineApps.js
new file mode 100644
index 000000000..e11b3c645
--- /dev/null
+++ b/mobile/android/chrome/content/OfflineApps.js
@@ -0,0 +1,77 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var OfflineApps = {
+ 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.dontAllow2"),
+ callback: function(aChecked) {
+ if (aChecked)
+ OfflineApps.disallowSite(aContentWindow.document);
+ }
+ },
+ {
+ label: strings.GetStringFromName("offlineApps.allow"),
+ callback: function() {
+ OfflineApps.allowSite(aContentWindow.document);
+ },
+ positive: true
+ }];
+
+ let requestor = BrowserApp.manifest ? "'" + BrowserApp.manifest.name + "'" : host;
+ let message = strings.formatStringFromName("offlineApps.ask", [requestor], 1);
+ let options = { checkbox: Strings.browser.GetStringFromName("offlineApps.dontAskAgain") };
+ NativeWindow.doorhanger.show(message, notificationID, buttons, tab.id, options);
+ },
+
+ 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, aDocument.nodePrincipal, window);
+ }
+};
diff --git a/mobile/android/chrome/content/PermissionsHelper.js b/mobile/android/chrome/content/PermissionsHelper.js
new file mode 100644
index 000000000..ad1eb760a
--- /dev/null
+++ b/mobile/android/chrome/content/PermissionsHelper.js
@@ -0,0 +1,188 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var PermissionsHelper = {
+ _permissonTypes: ["password", "geolocation", "popup", "indexedDB",
+ "offline-app", "desktop-notification", "plugins", "native-intent",
+ "flyweb-publish-server"],
+ _permissionStrings: {
+ "password": {
+ label: "password.logins",
+ allowed: "password.save",
+ denied: "password.dontSave"
+ },
+ "geolocation": {
+ label: "geolocation.location",
+ allowed: "geolocation.allow",
+ denied: "geolocation.dontAllow"
+ },
+ "flyweb-publish-server": {
+ label: "flyWebPublishServer.publishServer",
+ allowed: "flyWebPublishServer.allow",
+ denied: "flyWebPublishServer.dontAllow"
+ },
+ "popup": {
+ label: "blockPopups.label2",
+ allowed: "popup.show",
+ denied: "popup.dontShow"
+ },
+ "indexedDB": {
+ label: "offlineApps.offlineData",
+ allowed: "offlineApps.allow",
+ denied: "offlineApps.dontAllow2"
+ },
+ "offline-app": {
+ label: "offlineApps.offlineData",
+ allowed: "offlineApps.allow",
+ denied: "offlineApps.dontAllow2"
+ },
+ "desktop-notification": {
+ label: "desktopNotification.notifications",
+ allowed: "desktopNotification2.allow",
+ denied: "desktopNotification2.dontAllow"
+ },
+ "plugins": {
+ label: "clickToPlayPlugins.plugins",
+ allowed: "clickToPlayPlugins.activate",
+ denied: "clickToPlayPlugins.dontActivate"
+ },
+ "native-intent": {
+ label: "helperapps.openWithList2",
+ allowed: "helperapps.always",
+ denied: "helperapps.never"
+ }
+ },
+
+ observe: function observe(aSubject, aTopic, aData) {
+ let uri = BrowserApp.selectedBrowser.currentURI;
+ let check = false;
+
+ switch (aTopic) {
+ case "Permissions:Check":
+ check = true;
+ 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;
+
+ if (check) {
+ Messaging.sendRequest({
+ type: "Permissions:CheckResult",
+ hasPermissions: true
+ });
+ return;
+ }
+ // 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]);
+
+ permissions.push({
+ type: type,
+ setting: label,
+ value: valueString
+ });
+ }
+
+ if (check) {
+ Messaging.sendRequest({
+ type: "Permissions:CheckResult",
+ hasPermissions: false
+ });
+ return;
+ }
+
+ // Keep track of permissions, so we know which ones to clear
+ this._currentPermissions = permissions;
+
+ Messaging.sendRequest({
+ type: "Permissions:Data",
+ permissions: permissions
+ });
+ break;
+
+ case "Permissions:Clear":
+ // An array of the indices of the permissions we want to clear
+ let permissionsToClear = JSON.parse(aData);
+ let privacyContext = BrowserApp.selectedBrowser.docShell
+ .QueryInterface(Ci.nsILoadContext);
+
+ for (let i = 0; i < permissionsToClear.length; i++) {
+ let indexToClear = permissionsToClear[i];
+ let permissionType = this._currentPermissions[indexToClear]["type"];
+ this.clearPermission(uri, permissionType, privacyContext);
+ }
+ 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, aContext) {
+ // 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, aType);
+ // Clear content prefs set in ContentPermissionPrompt.js
+ Cc["@mozilla.org/content-pref/service;1"]
+ .getService(Ci.nsIContentPrefService2)
+ .removeByDomainAndName(aURI.spec, aType + ".request.remember", aContext);
+ }
+ }
+};
diff --git a/mobile/android/chrome/content/PluginHelper.js b/mobile/android/chrome/content/PluginHelper.js
new file mode 100644
index 000000000..59d87fa7c
--- /dev/null
+++ b/mobile/android/chrome/content/PluginHelper.js
@@ -0,0 +1,221 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var PluginHelper = {
+ showDoorHanger: function(aTab) {
+ if (!aTab.browser)
+ return;
+
+ // Even though we may not end up showing a doorhanger, this flag
+ // lets us know that we've tried to show a doorhanger.
+ aTab.shouldShowPluginDoorhanger = false;
+
+ let uri = aTab.browser.currentURI;
+
+ // If the user has previously set a plugins permission for this website,
+ // either play or don't play the plugins instead of showing a doorhanger.
+ let permValue = Services.perms.testPermission(uri, "plugins");
+ if (permValue != Services.perms.UNKNOWN_ACTION) {
+ if (permValue == Services.perms.ALLOW_ACTION)
+ PluginHelper.playAllPlugins(aTab.browser.contentWindow);
+
+ return;
+ }
+
+ let message = Strings.browser.formatStringFromName("clickToPlayPlugins.message2",
+ [uri.host], 1);
+ let buttons = [
+ {
+ label: Strings.browser.GetStringFromName("clickToPlayPlugins.dontActivate"),
+ callback: function(aChecked) {
+ // If the user checked "Don't ask again", make a permanent exception
+ if (aChecked)
+ Services.perms.add(uri, "plugins", Ci.nsIPermissionManager.DENY_ACTION);
+
+ // Other than that, do nothing
+ }
+ },
+ {
+ label: Strings.browser.GetStringFromName("clickToPlayPlugins.activate"),
+ callback: function(aChecked) {
+ // If the user checked "Don't ask again", make a permanent exception
+ if (aChecked)
+ Services.perms.add(uri, "plugins", Ci.nsIPermissionManager.ALLOW_ACTION);
+
+ PluginHelper.playAllPlugins(aTab.browser.contentWindow);
+ },
+ positive: true
+ }
+ ];
+
+ // Add a checkbox with a "Don't ask again" message if the uri contains a
+ // host. Adding a permanent exception will fail if host is not present.
+ let options = uri.host ? { checkbox: Strings.browser.GetStringFromName("clickToPlayPlugins.dontAskAgain") } : {};
+
+ NativeWindow.doorhanger.show(message, "ask-to-play-plugins", buttons, aTab.id, options);
+ },
+
+ delayAndShowDoorHanger: function(aTab) {
+ // To avoid showing the doorhanger if there are also visible plugin
+ // overlays on the page, delay showing the doorhanger to check if
+ // visible plugins get added in the near future.
+ if (!aTab.pluginDoorhangerTimeout) {
+ aTab.pluginDoorhangerTimeout = setTimeout(function() {
+ if (this.shouldShowPluginDoorhanger) {
+ PluginHelper.showDoorHanger(this);
+ }
+ }.bind(aTab), 500);
+ }
+ },
+
+ playAllPlugins: function(aContentWindow) {
+ let cwu = aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ // XXX not sure if we should enable plugins for the parent documents...
+ let plugins = cwu.plugins;
+ if (!plugins || !plugins.length)
+ return;
+
+ plugins.forEach(this.playPlugin);
+ },
+
+ playPlugin: function(plugin) {
+ let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+ if (!objLoadingContent.activated)
+ objLoadingContent.playPlugin();
+ },
+
+ getPluginPreference: function getPluginPreference() {
+ let pluginDisable = Services.prefs.getBoolPref("plugin.disable");
+ if (pluginDisable)
+ return "0";
+
+ let state = Services.prefs.getIntPref("plugin.default.state");
+ return state == Ci.nsIPluginTag.STATE_CLICKTOPLAY ? "2" : "1";
+ },
+
+ setPluginPreference: function setPluginPreference(aValue) {
+ switch (aValue) {
+ case "0": // Enable Plugins = No
+ Services.prefs.setBoolPref("plugin.disable", true);
+ Services.prefs.clearUserPref("plugin.default.state");
+ break;
+ case "1": // Enable Plugins = Yes
+ Services.prefs.clearUserPref("plugin.disable");
+ Services.prefs.setIntPref("plugin.default.state", Ci.nsIPluginTag.STATE_ENABLED);
+ break;
+ case "2": // Enable Plugins = Tap to Play (default)
+ Services.prefs.clearUserPref("plugin.disable");
+ Services.prefs.clearUserPref("plugin.default.state");
+ break;
+ }
+ },
+
+ // Copied from /browser/base/content/browser.js
+ isTooSmall : function (plugin, overlay) {
+ // Is the <object>'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;
+ },
+
+ getPluginMimeType: function (plugin) {
+ var tagMimetype = plugin.actualType;
+
+ if (tagMimetype == "") {
+ tagMimetype = plugin.type;
+ }
+
+ return tagMimetype;
+ },
+
+ handlePluginBindingAttached: function (aTab, aEvent) {
+ let plugin = aEvent.target;
+ let doc = plugin.ownerDocument;
+ let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
+ if (!overlay || overlay._bindingHandled) {
+ return;
+ }
+ overlay._bindingHandled = true;
+
+ let eventType = PluginHelper._getBindingType(plugin);
+ if (!eventType) {
+ // Not all bindings have handlers
+ return;
+ }
+
+ switch (eventType) {
+ case "PluginClickToPlay": {
+ // Check if plugins have already been activated for this page, or if
+ // the user has set a permission to always play plugins on the site
+ if (aTab.clickToPlayPluginsActivated ||
+ Services.perms.testPermission(aTab.browser.currentURI, "plugins") ==
+ Services.perms.ALLOW_ACTION) {
+ PluginHelper.playPlugin(plugin);
+ return;
+ }
+
+ // If the plugin is hidden, or if the overlay is too small, show a
+ // doorhanger notification
+ if (PluginHelper.isTooSmall(plugin, overlay)) {
+ PluginHelper.delayAndShowDoorHanger(aTab);
+ } else {
+ // There's a large enough visible overlay that we don't need to show
+ // the doorhanger.
+ aTab.shouldShowPluginDoorhanger = false;
+ overlay.classList.add("visible");
+ }
+
+ // Add click to play listener to the overlay
+ overlay.addEventListener("click", function(e) {
+ if (!e.isTrusted)
+ return;
+ e.preventDefault();
+ let win = e.target.ownerDocument.defaultView.top;
+ let tab = BrowserApp.getTabForWindow(win);
+ tab.clickToPlayPluginsActivated = true;
+ PluginHelper.playAllPlugins(win);
+
+ NativeWindow.doorhanger.hide("ask-to-play-plugins", tab.id);
+ }, true);
+
+ // Add handlers for over- and underflow in case the plugin gets resized
+ plugin.addEventListener("overflow", function(event) {
+ overlay.classList.remove("visible");
+ PluginHelper.delayAndShowDoorHanger(aTab);
+ });
+ plugin.addEventListener("underflow", function(event) {
+ // This is also triggered if only one dimension underflows,
+ // the other dimension might still overflow
+ if (!PluginHelper.isTooSmall(plugin, overlay)) {
+ overlay.classList.add("visible");
+ }
+ });
+
+ break;
+ }
+ }
+ },
+
+ // Helper to get the binding handler type from a plugin object
+ _getBindingType: function(plugin) {
+ if (!(plugin instanceof Ci.nsIObjectLoadingContent))
+ return null;
+
+ switch (plugin.pluginFallbackType) {
+ case Ci.nsIObjectLoadingContent.PLUGIN_UNSUPPORTED:
+ return "PluginNotFound";
+ case Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY:
+ return "PluginClickToPlay";
+ default:
+ // Not all states map to a handler
+ return null;
+ }
+ }
+};
diff --git a/mobile/android/chrome/content/PresentationView.js b/mobile/android/chrome/content/PresentationView.js
new file mode 100644
index 000000000..4f7e02870
--- /dev/null
+++ b/mobile/android/chrome/content/PresentationView.js
@@ -0,0 +1,63 @@
+/* -*- Mode: tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+const TOPIC_PRESENTATION_VIEW_READY = "presentation-view-ready";
+const TOPIC_PRESENTATION_RECEIVER_LAUNCH = "presentation-receiver:launch";
+const TOPIC_PRESENTATION_RECEIVER_LAUNCH_RESPONSE = "presentation-receiver:launch:response";
+
+// globals Services
+Cu.import("resource://gre/modules/Services.jsm");
+
+function log(str) {
+ // dump("-*- PresentationView.js -*-: " + str + "\n");
+}
+
+let PresentationView = {
+ _id: null,
+
+ startup: function startup() {
+ // use hash as the ID of this top level window
+ this._id = window.location.hash.substr(1);
+
+ // Listen "presentation-receiver:launch" sent from
+ // PresentationRequestUIGlue.
+ Services.obs.addObserver(this,TOPIC_PRESENTATION_RECEIVER_LAUNCH, false);
+
+ // Notify PresentationView is ready.
+ Services.obs.notifyObservers(null, TOPIC_PRESENTATION_VIEW_READY, this._id);
+ },
+
+ stop: function stop() {
+ Services.obs.removeObserver(this, TOPIC_PRESENTATION_RECEIVER_LAUNCH);
+ },
+
+ observe: function observe(aSubject, aTopic, aData) {
+ log("Got observe: aTopic=" + aTopic);
+
+ let requestData = JSON.parse(aData);
+ if (this._id != requestData.windowId) {
+ return;
+ }
+
+ let browser = document.getElementById("content");
+ browser.setAttribute("mozpresentation", requestData.url);
+ try {
+ browser.loadURI(requestData.url);
+ Services.obs.notifyObservers(browser,
+ TOPIC_PRESENTATION_RECEIVER_LAUNCH_RESPONSE,
+ JSON.stringify({ result: "success",
+ requestId: requestData.requestId }));
+ } catch (e) {
+ Services.obs.notifyObservers(null,
+ TOPIC_PRESENTATION_RECEIVER_LAUNCH_RESPONSE,
+ JSON.stringify({ result: "error",
+ reason: e.message }));
+ }
+ }
+};
diff --git a/mobile/android/chrome/content/PresentationView.xul b/mobile/android/chrome/content/PresentationView.xul
new file mode 100644
index 000000000..00440453c
--- /dev/null
+++ b/mobile/android/chrome/content/PresentationView.xul
@@ -0,0 +1,15 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<window id="presentation-window"
+ onload="PresentationView.startup();"
+ onunload="PresentationView.stop();"
+ windowtype="navigator:browser"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <browser id="content" type="content-targetable" src="about:blank" flex="1"/>
+
+ <script type="application/javascript" src="chrome://browser/content/PresentationView.js"/>
+</window>
diff --git a/mobile/android/chrome/content/PrintHelper.js b/mobile/android/chrome/content/PrintHelper.js
new file mode 100644
index 000000000..9b071ee92
--- /dev/null
+++ b/mobile/android/chrome/content/PrintHelper.js
@@ -0,0 +1,73 @@
+// -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
+
+var PrintHelper = {
+ init: function() {
+ Services.obs.addObserver(this, "Print:PDF", false);
+ },
+
+ observe: function (aSubject, aTopic, aData) {
+ let browser = BrowserApp.selectedBrowser;
+
+ switch (aTopic) {
+ case "Print:PDF":
+ Messaging.handleRequest(aTopic, aData, (data) => {
+ return this.generatePDF(browser);
+ });
+ break;
+ }
+ },
+
+ generatePDF: function(aBrowser) {
+ // Create the final destination file location
+ let fileName = ContentAreaUtils.getDefaultFileName(aBrowser.contentTitle, aBrowser.currentURI, null, null);
+ fileName = fileName.trim() + ".pdf";
+
+ let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ 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 = false;
+ printSettings.printBGColors = false;
+ printSettings.printToFile = true;
+ printSettings.toFileName = file.path;
+ printSettings.printFrameType = Ci.nsIPrintSettings.kFramesAsIs;
+ printSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF;
+
+ let webBrowserPrint = aBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebBrowserPrint);
+
+ return new Promise((resolve, reject) => {
+ webBrowserPrint.print(printSettings, {
+ onStateChange: function(webProgress, request, stateFlags, status) {
+ // We get two STATE_START calls, one for STATE_IS_DOCUMENT and one for STATE_IS_NETWORK
+ if (stateFlags & Ci.nsIWebProgressListener.STATE_START && stateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
+ // Let the user know something is happening. Generating the PDF can take some time.
+ Snackbars.show(Strings.browser.GetStringFromName("alertPrintjobToast"), Snackbars.LENGTH_LONG);
+ }
+
+ // We get two STATE_STOP calls, one for STATE_IS_DOCUMENT and one for STATE_IS_NETWORK
+ if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP && stateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
+ if (Components.isSuccessCode(status)) {
+ // Send the details to Java
+ resolve({ file: file.path, title: fileName });
+ } else {
+ reject();
+ }
+ }
+ },
+ onProgressChange: function () {},
+ onLocationChange: function () {},
+ onStatusChange: function () {},
+ onSecurityChange: function () {},
+ });
+ });
+ }
+};
diff --git a/mobile/android/chrome/content/Reader.js b/mobile/android/chrome/content/Reader.js
new file mode 100644
index 000000000..d0f3d7801
--- /dev/null
+++ b/mobile/android/chrome/content/Reader.js
@@ -0,0 +1,290 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
+
+/*globals MAX_URI_LENGTH, MAX_TITLE_LENGTH */
+
+var Reader = {
+ // These values should match those defined in BrowserContract.java.
+ STATUS_UNFETCHED: 0,
+ STATUS_FETCH_FAILED_TEMPORARY: 1,
+ STATUS_FETCH_FAILED_PERMANENT: 2,
+ STATUS_FETCH_FAILED_UNSUPPORTED_FORMAT: 3,
+ STATUS_FETCHED_ARTICLE: 4,
+
+ get _hasUsedToolbar() {
+ delete this._hasUsedToolbar;
+ return this._hasUsedToolbar = Services.prefs.getBoolPref("reader.has_used_toolbar");
+ },
+
+ /**
+ * BackPressListener (listeners / ReaderView Ids).
+ */
+ _backPressListeners: [],
+ _backPressViewIds: [],
+
+ /**
+ * Set a backPressListener for this tabId / ReaderView Id pair.
+ */
+ _addBackPressListener: function(tabId, viewId, listener) {
+ this._backPressListeners[tabId] = listener;
+ this._backPressViewIds[viewId] = tabId;
+ },
+
+ /**
+ * Remove a backPressListener for this ReaderView Id.
+ */
+ _removeBackPressListener: function(viewId) {
+ let tabId = this._backPressViewIds[viewId];
+ if (tabId != undefined) {
+ this._backPressListeners[tabId] = null;
+ delete this._backPressViewIds[viewId];
+ }
+ },
+
+ /**
+ * If the requested tab has a backPress listener, return its results, else false.
+ */
+ onBackPress: function(tabId) {
+ let listener = this._backPressListeners[tabId];
+ return { handled: (listener ? listener() : false) };
+ },
+
+ observe: function Reader_observe(aMessage, aTopic, aData) {
+ switch (aTopic) {
+ case "Reader:RemoveFromCache": {
+ ReaderMode.removeArticleFromCache(aData).catch(e => Cu.reportError("Error removing article from cache: " + e));
+ break;
+ }
+
+ case "Reader:AddToCache": {
+ let tab = BrowserApp.getTabForId(aData);
+ if (!tab) {
+ throw new Error("No tab for tabID = " + aData + " when trying to save reader view article");
+ }
+
+ // If the article is coming from reader mode, we must have fetched it already.
+ this._getArticleData(tab.browser).then((article) => {
+ ReaderMode.storeArticleInCache(article);
+ }).catch(e => Cu.reportError("Error storing article in cache: " + e));
+ break;
+ }
+ }
+ },
+
+ receiveMessage: function(message) {
+ switch (message.name) {
+ case "Reader:ArticleGet":
+ this._getArticle(message.data.url).then((article) => {
+ // Make sure the target browser is still alive before trying to send data back.
+ if (message.target.messageManager) {
+ message.target.messageManager.sendAsyncMessage("Reader:ArticleData", { article: article });
+ }
+ }, e => {
+ if (e && e.newURL) {
+ message.target.loadURI("about:reader?url=" + encodeURIComponent(e.newURL));
+ }
+ });
+ break;
+
+ // On DropdownClosed in ReaderView, we cleanup / clear existing BackPressListener.
+ case "Reader:DropdownClosed": {
+ this._removeBackPressListener(message.data);
+ break;
+ }
+
+ // On DropdownOpened in ReaderView, we add BackPressListener to handle a subsequent BACK request.
+ case "Reader:DropdownOpened": {
+ let tabId = BrowserApp.selectedTab.id;
+ this._addBackPressListener(tabId, message.data, () => {
+ // User hit BACK key while ReaderView has the banner font-dropdown opened.
+ // Close it and return prevent-default.
+ if (message.target.messageManager) {
+ message.target.messageManager.sendAsyncMessage("Reader:CloseDropdown");
+ return true;
+ }
+ // We can assume ReaderView banner's font-dropdown doesn't need to be closed.
+ return false;
+ });
+
+ break;
+ }
+
+ case "Reader:FaviconRequest": {
+ Messaging.sendRequestForResult({
+ type: "Reader:FaviconRequest",
+ url: message.data.url
+ }).then(data => {
+ message.target.messageManager.sendAsyncMessage("Reader:FaviconReturn", JSON.parse(data));
+ });
+ break;
+ }
+
+ case "Reader:SystemUIVisibility":
+ Messaging.sendRequest({
+ type: "SystemUI:Visibility",
+ visible: message.data.visible
+ });
+ break;
+
+ case "Reader:ToolbarHidden":
+ if (!this._hasUsedToolbar) {
+ Snackbars.show(Strings.browser.GetStringFromName("readerMode.toolbarTip"), Snackbars.LENGTH_LONG);
+ Services.prefs.setBoolPref("reader.has_used_toolbar", true);
+ this._hasUsedToolbar = true;
+ }
+ break;
+
+ case "Reader:UpdateReaderButton": {
+ let tab = BrowserApp.getTabForBrowser(message.target);
+ tab.browser.isArticle = message.data.isArticle;
+ this.updatePageAction(tab);
+ break;
+ }
+ }
+ },
+
+ pageAction: {
+ readerModeCallback: function(browser) {
+ let url = browser.currentURI.spec;
+ if (url.startsWith("about:reader")) {
+ UITelemetry.addEvent("action.1", "button", null, "reader_exit");
+ } else {
+ UITelemetry.addEvent("action.1", "button", null, "reader_enter");
+ }
+ browser.messageManager.sendAsyncMessage("Reader:ToggleReaderMode");
+ },
+ },
+
+ updatePageAction: function(tab) {
+ if (!tab.getActive()) {
+ return;
+ }
+
+ if (this.pageAction.id) {
+ PageActions.remove(this.pageAction.id);
+ delete this.pageAction.id;
+ }
+
+ let showPageAction = (icon, title) => {
+ this.pageAction.id = PageActions.add({
+ icon: icon,
+ title: title,
+ clickCallback: () => this.pageAction.readerModeCallback(browser),
+ important: true
+ });
+ };
+
+ let browser = tab.browser;
+ if (browser.currentURI.spec.startsWith("about:reader")) {
+ showPageAction("drawable://reader_active", Strings.reader.GetStringFromName("readerView.close"));
+ // Only start a reader session if the viewer is in the foreground. We do
+ // not track background reader viewers.
+ UITelemetry.startSession("reader.1", null);
+ return;
+ }
+
+ // Only stop a reader session if the foreground viewer is not visible.
+ UITelemetry.stopSession("reader.1", "", null);
+
+ if (browser.isArticle) {
+ showPageAction("drawable://reader", Strings.reader.GetStringFromName("readerView.enter"));
+ UITelemetry.addEvent("show.1", "button", null, "reader_available");
+ } else {
+ UITelemetry.addEvent("show.1", "button", null, "reader_unavailable");
+ }
+ },
+
+ /**
+ * Gets an article for a given URL. This method will download and parse a document
+ * if it does not find the article in the cache.
+ *
+ * @param url The article URL.
+ * @return {Promise}
+ * @resolves JS object representing the article, or null if no article is found.
+ */
+ _getArticle: Task.async(function* (url) {
+ // First try to find a parsed article in the cache.
+ let article = yield ReaderMode.getArticleFromCache(url);
+ if (article) {
+ return article;
+ }
+
+ // Article hasn't been found in the cache, we need to
+ // download the page and parse the article out of it.
+ return yield ReaderMode.downloadAndParseDocument(url).catch(e => {
+ if (e && e.newURL) {
+ // Pass up the error so we can navigate the browser in question to the new URL:
+ throw e;
+ }
+ Cu.reportError("Error downloading and parsing document: " + e);
+ return null;
+ });
+ }),
+
+ _getArticleData: function(browser) {
+ return new Promise((resolve, reject) => {
+ if (browser == null) {
+ reject("_getArticleData needs valid browser");
+ }
+
+ let mm = browser.messageManager;
+ let listener = (message) => {
+ mm.removeMessageListener("Reader:StoredArticleData", listener);
+ resolve(message.data.article);
+ };
+ mm.addMessageListener("Reader:StoredArticleData", listener);
+ mm.sendAsyncMessage("Reader:GetStoredArticleData");
+ });
+ },
+
+
+ /**
+ * Migrates old indexedDB reader mode cache to new JSON cache.
+ */
+ migrateCache: Task.async(function* () {
+ let cacheDB = yield new Promise((resolve, reject) => {
+ let request = window.indexedDB.open("about:reader", 1);
+ request.onsuccess = event => resolve(event.target.result);
+ request.onerror = event => reject(request.error);
+
+ // If there is no DB to migrate, don't do anything.
+ request.onupgradeneeded = event => resolve(null);
+ });
+
+ if (!cacheDB) {
+ return;
+ }
+
+ let articles = yield new Promise((resolve, reject) => {
+ let articles = [];
+
+ let transaction = cacheDB.transaction(cacheDB.objectStoreNames);
+ let store = transaction.objectStore(cacheDB.objectStoreNames[0]);
+
+ let request = store.openCursor();
+ request.onsuccess = event => {
+ let cursor = event.target.result;
+ if (!cursor) {
+ resolve(articles);
+ } else {
+ articles.push(cursor.value);
+ cursor.continue();
+ }
+ };
+ request.onerror = event => reject(request.error);
+ });
+
+ for (let article of articles) {
+ yield ReaderMode.storeArticleInCache(article);
+ }
+
+ // Delete the database.
+ window.indexedDB.deleteDatabase("about:reader");
+ }),
+};
diff --git a/mobile/android/chrome/content/RemoteDebugger.js b/mobile/android/chrome/content/RemoteDebugger.js
new file mode 100644
index 000000000..a5a3a43de
--- /dev/null
+++ b/mobile/android/chrome/content/RemoteDebugger.js
@@ -0,0 +1,355 @@
+// -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* globals DebuggerServer */
+"use strict";
+
+XPCOMUtils.defineLazyGetter(this, "DebuggerServer", () => {
+ let { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+ let { DebuggerServer } = require("devtools/server/main");
+ return DebuggerServer;
+});
+
+var RemoteDebugger = {
+ init() {
+ USBRemoteDebugger.init();
+ WiFiRemoteDebugger.init();
+ },
+
+ get isAnyEnabled() {
+ return USBRemoteDebugger.isEnabled || WiFiRemoteDebugger.isEnabled;
+ },
+
+ /**
+ * Prompt the user to accept or decline the incoming connection.
+ *
+ * @param session object
+ * The session object will contain at least the following fields:
+ * {
+ * authentication,
+ * client: {
+ * host,
+ * port
+ * },
+ * server: {
+ * host,
+ * port
+ * }
+ * }
+ * Specific authentication modes may include additional fields. Check
+ * the different |allowConnection| methods in
+ * devtools/shared/security/auth.js.
+ * @return An AuthenticationResult value.
+ * A promise that will be resolved to the above is also allowed.
+ */
+ allowConnection(session) {
+ if (this._promptingForAllow) {
+ // Don't stack connection prompts if one is already open
+ return DebuggerServer.AuthenticationResult.DENY;
+ }
+
+ if (!session.server.port) {
+ this._promptingForAllow = this._promptForUSB(session);
+ } else {
+ this._promptingForAllow = this._promptForTCP(session);
+ }
+ this._promptingForAllow.then(() => this._promptingForAllow = null);
+
+ return this._promptingForAllow;
+ },
+
+ _promptForUSB(session) {
+ if (session.authentication !== 'PROMPT') {
+ // This dialog is not prepared for any other authentication method at
+ // this time.
+ return DebuggerServer.AuthenticationResult.DENY;
+ }
+
+ return new Promise(resolve => {
+ let title = Strings.browser.GetStringFromName("remoteIncomingPromptTitle");
+ let msg = Strings.browser.GetStringFromName("remoteIncomingPromptUSB");
+ let allow = Strings.browser.GetStringFromName("remoteIncomingPromptAllow");
+ let deny = Strings.browser.GetStringFromName("remoteIncomingPromptDeny");
+
+ // Make prompt. Note: button order is in reverse.
+ let prompt = new Prompt({
+ window: null,
+ hint: "remotedebug",
+ title: title,
+ message: msg,
+ buttons: [ allow, deny ],
+ priority: 1
+ });
+
+ prompt.show(data => {
+ let result = data.button;
+ if (result === 0) {
+ resolve(DebuggerServer.AuthenticationResult.ALLOW);
+ } else {
+ resolve(DebuggerServer.AuthenticationResult.DENY);
+ }
+ });
+ });
+ },
+
+ _promptForTCP(session) {
+ if (session.authentication !== 'OOB_CERT' || !session.client.cert) {
+ // This dialog is not prepared for any other authentication method at
+ // this time.
+ return DebuggerServer.AuthenticationResult.DENY;
+ }
+
+ return new Promise(resolve => {
+ let title = Strings.browser.GetStringFromName("remoteIncomingPromptTitle");
+ let msg = Strings.browser.formatStringFromName("remoteIncomingPromptTCP", [
+ session.client.host,
+ session.client.port
+ ], 2);
+ let scan = Strings.browser.GetStringFromName("remoteIncomingPromptScan");
+ let scanAndRemember = Strings.browser.GetStringFromName("remoteIncomingPromptScanAndRemember");
+ let deny = Strings.browser.GetStringFromName("remoteIncomingPromptDeny");
+
+ // Make prompt. Note: button order is in reverse.
+ let prompt = new Prompt({
+ window: null,
+ hint: "remotedebug",
+ title: title,
+ message: msg,
+ buttons: [ scan, scanAndRemember, deny ],
+ priority: 1
+ });
+
+ prompt.show(data => {
+ let result = data.button;
+ if (result === 0) {
+ resolve(DebuggerServer.AuthenticationResult.ALLOW);
+ } else if (result === 1) {
+ resolve(DebuggerServer.AuthenticationResult.ALLOW_PERSIST);
+ } else {
+ resolve(DebuggerServer.AuthenticationResult.DENY);
+ }
+ });
+ });
+ },
+
+ /**
+ * During OOB_CERT authentication, the user must transfer some data through
+ * some out of band mechanism from the client to the server to authenticate
+ * the devices.
+ *
+ * This implementation instructs Fennec to invoke a QR decoder and return the
+ * the data it contains back here.
+ *
+ * @return An object containing:
+ * * sha256: hash(ClientCert)
+ * * k : K(random 128-bit number)
+ * A promise that will be resolved to the above is also allowed.
+ */
+ receiveOOB() {
+ if (this._receivingOOB) {
+ return this._receivingOOB;
+ }
+
+ this._receivingOOB = Messaging.sendRequestForResult({
+ type: "DevToolsAuth:Scan"
+ }).then(data => {
+ return JSON.parse(data);
+ }, () => {
+ let title = Strings.browser.GetStringFromName("remoteQRScanFailedPromptTitle");
+ let msg = Strings.browser.GetStringFromName("remoteQRScanFailedPromptMessage");
+ let ok = Strings.browser.GetStringFromName("remoteQRScanFailedPromptOK");
+ let prompt = new Prompt({
+ window: null,
+ hint: "remotedebug",
+ title: title,
+ message: msg,
+ buttons: [ ok ],
+ priority: 1
+ });
+ prompt.show();
+ });
+
+ this._receivingOOB.then(() => this._receivingOOB = null);
+
+ return this._receivingOOB;
+ },
+
+ initServer: function() {
+ if (DebuggerServer.initialized) {
+ return;
+ }
+
+ DebuggerServer.init();
+
+ // Add browser and Fennec specific actors
+ DebuggerServer.addBrowserActors();
+ DebuggerServer.registerModule("resource://gre/modules/dbg-browser-actors.js");
+
+ // Allow debugging of chrome for any process
+ DebuggerServer.allowChromeProcess = true;
+ }
+};
+
+RemoteDebugger.allowConnection =
+ RemoteDebugger.allowConnection.bind(RemoteDebugger);
+RemoteDebugger.receiveOOB =
+ RemoteDebugger.receiveOOB.bind(RemoteDebugger);
+
+var USBRemoteDebugger = {
+
+ init() {
+ Services.prefs.addObserver("devtools.", this, false);
+
+ if (this.isEnabled) {
+ this.start();
+ }
+ },
+
+ observe(subject, topic, data) {
+ if (topic != "nsPref:changed") {
+ return;
+ }
+
+ switch (data) {
+ case "devtools.remote.usb.enabled":
+ Services.prefs.setBoolPref("devtools.debugger.remote-enabled",
+ RemoteDebugger.isAnyEnabled);
+ if (this.isEnabled) {
+ this.start();
+ } else {
+ this.stop();
+ }
+ break;
+
+ case "devtools.debugger.remote-port":
+ case "devtools.debugger.unix-domain-socket":
+ if (this.isEnabled) {
+ this.stop();
+ this.start();
+ }
+ break;
+ }
+ },
+
+ get isEnabled() {
+ return Services.prefs.getBoolPref("devtools.remote.usb.enabled");
+ },
+
+ start: function() {
+ if (this._listener) {
+ return;
+ }
+
+ RemoteDebugger.initServer();
+
+ let portOrPath =
+ Services.prefs.getCharPref("devtools.debugger.unix-domain-socket") ||
+ Services.prefs.getIntPref("devtools.debugger.remote-port");
+
+ try {
+ dump("Starting USB debugger on " + portOrPath);
+ let AuthenticatorType = DebuggerServer.Authenticators.get("PROMPT");
+ let authenticator = new AuthenticatorType.Server();
+ authenticator.allowConnection = RemoteDebugger.allowConnection;
+ this._listener = DebuggerServer.createListener();
+ this._listener.portOrPath = portOrPath;
+ this._listener.authenticator = authenticator;
+ this._listener.open();
+ } catch (e) {
+ dump("Unable to start USB debugger server: " + e);
+ }
+ },
+
+ stop: function() {
+ if (!this._listener) {
+ return;
+ }
+
+ try {
+ this._listener.close();
+ this._listener = null;
+ } catch (e) {
+ dump("Unable to stop USB debugger server: " + e);
+ }
+ }
+
+};
+
+var WiFiRemoteDebugger = {
+
+ init() {
+ Services.prefs.addObserver("devtools.", this, false);
+
+ if (this.isEnabled) {
+ this.start();
+ }
+ },
+
+ observe(subject, topic, data) {
+ if (topic != "nsPref:changed") {
+ return;
+ }
+
+ switch (data) {
+ case "devtools.remote.wifi.enabled":
+ Services.prefs.setBoolPref("devtools.debugger.remote-enabled",
+ RemoteDebugger.isAnyEnabled);
+ // Allow remote debugging on non-local interfaces when WiFi debug is
+ // enabled
+ // TODO: Bug 1034411: Lock down to WiFi interface only
+ Services.prefs.setBoolPref("devtools.debugger.force-local",
+ !this.isEnabled);
+ if (this.isEnabled) {
+ this.start();
+ } else {
+ this.stop();
+ }
+ break;
+ }
+ },
+
+ get isEnabled() {
+ return Services.prefs.getBoolPref("devtools.remote.wifi.enabled");
+ },
+
+ start: function() {
+ if (this._listener) {
+ return;
+ }
+
+ RemoteDebugger.initServer();
+
+ try {
+ dump("Starting WiFi debugger");
+ let AuthenticatorType = DebuggerServer.Authenticators.get("OOB_CERT");
+ let authenticator = new AuthenticatorType.Server();
+ authenticator.allowConnection = RemoteDebugger.allowConnection;
+ authenticator.receiveOOB = RemoteDebugger.receiveOOB;
+ this._listener = DebuggerServer.createListener();
+ this._listener.portOrPath = -1 /* any available port */;
+ this._listener.authenticator = authenticator;
+ this._listener.discoverable = true;
+ this._listener.encryption = true;
+ this._listener.open();
+ let port = this._listener.port;
+ dump("Started WiFi debugger on " + port);
+ } catch (e) {
+ dump("Unable to start WiFi debugger server: " + e);
+ }
+ },
+
+ stop: function() {
+ if (!this._listener) {
+ return;
+ }
+
+ try {
+ this._listener.close();
+ this._listener = null;
+ } catch (e) {
+ dump("Unable to stop WiFi debugger server: " + e);
+ }
+ }
+
+};
diff --git a/mobile/android/chrome/content/SelectHelper.js b/mobile/android/chrome/content/SelectHelper.js
new file mode 100644
index 000000000..41d0193d4
--- /dev/null
+++ b/mobile/android/chrome/content/SelectHelper.js
@@ -0,0 +1,161 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var SelectHelper = {
+ _uiBusy: false,
+
+ handleEvent: function(event) {
+ this.handleClick(event.target);
+ },
+
+ handleClick: function(target) {
+ // if we're busy looking at a select we want to eat any clicks that
+ // come to us, but not to process them
+ if (this._uiBusy || !this._isMenu(target) || this._isDisabledElement(target)) {
+ return;
+ }
+
+ this._uiBusy = true;
+ this.show(target);
+ this._uiBusy = false;
+ },
+
+ // This is a callback function to be provided to prompt.show(callBack).
+ // It will update which Option elements in a Select have been selected
+ // or unselected and fire the onChange event.
+ _promptCallBack: function(data, element) {
+ let selected = data.list;
+
+ if (element instanceof Ci.nsIDOMXULMenuListElement) {
+ if (element.selectedIndex != selected[0]) {
+ element.selectedIndex = selected[0];
+ this.fireOnCommand(element);
+ }
+ } else if (element instanceof HTMLSelectElement) {
+ let changed = false;
+ let i = 0; // The index for the element from `data.list` that we are currently examining.
+ this.forVisibleOptions(element, function(node) {
+ if (node.selected && selected.indexOf(i) == -1) {
+ changed = true;
+ node.selected = false;
+ } else if (!node.selected && selected.indexOf(i) != -1) {
+ changed = true;
+ node.selected = true;
+ }
+ i++;
+ });
+
+ if (changed) {
+ this.fireOnChange(element);
+ }
+ }
+ },
+
+ show: function(element) {
+ let list = this.getListForElement(element);
+ let p = new Prompt({
+ window: element.ownerDocument.defaultView
+ });
+
+ if (element.multiple) {
+ p.addButton({
+ label: Strings.browser.GetStringFromName("selectHelper.closeMultipleSelectDialog")
+ }).setMultiChoiceItems(list);
+ } else {
+ p.setSingleChoiceItems(list);
+ }
+
+ p.show((data) => {
+ this._promptCallBack(data,element)
+ });
+ },
+
+ _isMenu: function(element) {
+ return (element instanceof HTMLSelectElement || element instanceof Ci.nsIDOMXULMenuListElement);
+ },
+
+ // Return a list of Option elements within a Select excluding
+ // any that were not visible.
+ getListForElement: function(element) {
+ let index = 0;
+ let items = [];
+ this.forVisibleOptions(element, function(node, options,parent) {
+ let item = {
+ label: node.text || node.label,
+ header: options.isGroup,
+ disabled: node.disabled,
+ id: index,
+ selected: node.selected,
+ };
+
+ if (parent) {
+ item.child = true;
+ item.disabled = item.disabled || parent.disabled;
+ }
+ items.push(item);
+ index++;
+ });
+ return items;
+ },
+
+ // Apply a function to all visible Option elements in a Select
+ forVisibleOptions: function(element, aFunction, parent = null) {
+ if (element instanceof Ci.nsIDOMXULMenuListElement) {
+ element = element.menupopup;
+ }
+ let children = element.children;
+ let numChildren = children.length;
+
+
+ // if there are no children in this select, we add a dummy row so that at least something appears
+ if (numChildren == 0) {
+ aFunction.call(this, {label: ""}, {isGroup: false}, parent);
+ }
+
+ for (let i = 0; i < numChildren; i++) {
+ let child = children[i];
+ let style = window.getComputedStyle(child, null);
+ if (style.display !== "none") {
+ if (child instanceof HTMLOptionElement ||
+ child instanceof Ci.nsIDOMXULSelectControlItemElement) {
+ aFunction.call(this, child, {isGroup: false}, parent);
+ } else if (child instanceof HTMLOptGroupElement) {
+ aFunction.call(this, child, {isGroup: true});
+ this.forVisibleOptions(child, aFunction, child);
+ }
+ }
+ }
+ },
+
+ fireOnChange: function(element) {
+ let event = element.ownerDocument.createEvent("Events");
+ event.initEvent("change", true, true, element.defaultView, 0,
+ false, false, false, false, null);
+ setTimeout(function() {
+ element.dispatchEvent(event);
+ }, 0);
+ },
+
+ fireOnCommand: function(element) {
+ let event = element.ownerDocument.createEvent("XULCommandEvent");
+ event.initCommandEvent("command", true, true, element.defaultView, 0,
+ false, false, false, false, null);
+ setTimeout(function() {
+ element.dispatchEvent(event);
+ }, 0);
+ },
+
+ _isDisabledElement : function(element) {
+ let currentElement = element;
+ while (currentElement) {
+ // Must test with === in case a form has a field named "disabled". See bug 1263589.
+ if (currentElement.disabled === true) {
+ return true;
+ }
+ currentElement = currentElement.parentElement;
+ }
+ return false;
+ }
+};
diff --git a/mobile/android/chrome/content/WebcompatReporter.js b/mobile/android/chrome/content/WebcompatReporter.js
new file mode 100644
index 000000000..66aefdda0
--- /dev/null
+++ b/mobile/android/chrome/content/WebcompatReporter.js
@@ -0,0 +1,144 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
+
+var WebcompatReporter = {
+ menuItem: null,
+ menuItemEnabled: null,
+ init: function() {
+ Services.obs.addObserver(this, "DesktopMode:Change", false);
+ Services.obs.addObserver(this, "chrome-document-global-created", false);
+ Services.obs.addObserver(this, "content-document-global-created", false);
+
+ let visible = true;
+ if ("@mozilla.org/parental-controls-service;1" in Cc) {
+ let pc = Cc["@mozilla.org/parental-controls-service;1"].createInstance(Ci.nsIParentalControlsService);
+ visible = !pc.parentalControlsEnabled;
+ }
+
+ this.addMenuItem(visible);
+ },
+
+ observe: function(subject, topic, data) {
+ if (topic == "content-document-global-created" || topic == "chrome-document-global-created") {
+ let win = subject;
+ let currentURI = win.document.documentURI;
+
+ // Ignore non top-level documents
+ if (currentURI !== win.top.location.href) {
+ return;
+ }
+
+ if (!this.menuItemEnabled && this.isReportableUrl(currentURI)) {
+ NativeWindow.menu.update(this.menuItem, {enabled: true});
+ this.menuItemEnabled = true;
+ } else if (this.menuItemEnabled && !this.isReportableUrl(currentURI)) {
+ NativeWindow.menu.update(this.menuItem, {enabled: false});
+ this.menuItemEnabled = false;
+ }
+ } else if (topic === "DesktopMode:Change") {
+ let args = JSON.parse(data);
+ let tab = BrowserApp.getTabForId(args.tabId);
+ let currentURI = tab.browser.currentURI.spec;
+ if (args.desktopMode && this.isReportableUrl(currentURI)) {
+ this.reportDesktopModePrompt(tab);
+ }
+ }
+ },
+
+ addMenuItem: function(visible) {
+ this.menuItem = NativeWindow.menu.add({
+ name: this.strings.GetStringFromName("webcompat.menu.name"),
+ callback: () => {
+ Promise.resolve(BrowserApp.selectedTab).then(this.getScreenshot)
+ .then(this.reportIssue)
+ .catch(Cu.reportError);
+ },
+ enabled: false,
+ visible: visible,
+ });
+ },
+
+ getScreenshot: (tab) => {
+ return new Promise((resolve) => {
+ try {
+ let win = tab.window;
+ let dpr = win.devicePixelRatio;
+ let canvas = win.document.createElement("canvas");
+ let ctx = canvas.getContext("2d");
+ // Grab the visible viewport coordinates
+ let x = win.document.documentElement.scrollLeft;
+ let y = win.document.documentElement.scrollTop;
+ let w = win.innerWidth;
+ let h = win.innerHeight;
+ // Scale according to devicePixelRatio and coordinates
+ canvas.width = dpr * w;
+ canvas.height = dpr * h;
+ ctx.scale(dpr, dpr);
+ ctx.drawWindow(win, x, y, w, h, '#ffffff');
+ let screenshot = canvas.toDataURL();
+ resolve({tab: tab, data: screenshot});
+ } catch (e) {
+ // drawWindow can fail depending on memory or surface size. Rather than reject here,
+ // we resolve the URL so the user can continue to file an issue without a screenshot.
+ Cu.reportError("WebCompatReporter: getting a screenshot failed: " + e);
+ resolve({tab: tab});
+ }
+ });
+ },
+
+ isReportableUrl: function(url) {
+ return url && !(url.startsWith("about") ||
+ url.startsWith("chrome") ||
+ url.startsWith("file") ||
+ url.startsWith("resource"));
+ },
+
+ reportDesktopModePrompt: function(tab) {
+ let message = this.strings.GetStringFromName("webcompat.reportDesktopMode.message");
+ let options = {
+ action: {
+ label: this.strings.GetStringFromName("webcompat.reportDesktopModeYes.label"),
+ callback: () => this.reportIssue({tab: tab})
+ }
+ };
+ Snackbars.show(message, Snackbars.LENGTH_LONG, options);
+ },
+
+ reportIssue: (tabData) => {
+ return new Promise((resolve) => {
+ const WEBCOMPAT_ORIGIN = "https://webcompat.com";
+ let url = tabData.tab.browser.currentURI.spec
+ let webcompatURL = `${WEBCOMPAT_ORIGIN}/issues/new?url=${url}`;
+
+ if (tabData.data && typeof tabData.data === "string") {
+ BrowserApp.deck.addEventListener("DOMContentLoaded", function sendDataToTab(event) {
+ BrowserApp.deck.removeEventListener("DOMContentLoaded", sendDataToTab, false);
+
+ if (event.target.defaultView.location.origin === WEBCOMPAT_ORIGIN) {
+ // Waive Xray vision so event.origin is not chrome://browser on the other side.
+ let win = Cu.waiveXrays(event.target.defaultView);
+ win.postMessage(tabData.data, WEBCOMPAT_ORIGIN);
+ }
+ }, false);
+ }
+
+ let isPrivateTab = PrivateBrowsingUtils.isBrowserPrivate(tabData.tab.browser);
+ BrowserApp.addTab(webcompatURL, {parentId: tabData.tab.id, isPrivate: isPrivateTab});
+ resolve();
+ });
+ }
+};
+
+XPCOMUtils.defineLazyGetter(WebcompatReporter, "strings", function() {
+ return Services.strings.createBundle("chrome://browser/locale/webcompatReporter.properties");
+});
diff --git a/mobile/android/chrome/content/WebrtcUI.js b/mobile/android/chrome/content/WebrtcUI.js
new file mode 100644
index 000000000..475d05bd2
--- /dev/null
+++ b/mobile/android/chrome/content/WebrtcUI.js
@@ -0,0 +1,302 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["WebrtcUI"];
+
+XPCOMUtils.defineLazyModuleGetter(this, "Notifications", "resource://gre/modules/Notifications.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "ParentalControls", "@mozilla.org/parental-controls-service;1", "nsIParentalControlsService");
+XPCOMUtils.defineLazyModuleGetter(this, "RuntimePermissions", "resource://gre/modules/RuntimePermissions.jsm");
+
+var WebrtcUI = {
+ _notificationId: null,
+
+ // Add-ons can override stock permission behavior by doing:
+ //
+ // var stockObserve = WebrtcUI.observe;
+ //
+ // webrtcUI.observe = function(aSubject, aTopic, aData) {
+ // switch (aTopic) {
+ // case "PeerConnection:request": {
+ // // new code.
+ // break;
+ // ...
+ // default:
+ // return stockObserve.call(this, aSubject, aTopic, aData);
+ //
+ // See browser/modules/webrtcUI.jsm for details.
+
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic === "getUserMedia:request") {
+ RuntimePermissions
+ .waitForPermissions(this._determineNeededRuntimePermissions(aSubject))
+ .then((permissionGranted) => {
+ if (permissionGranted) {
+ WebrtcUI.handleGumRequest(aSubject, aTopic, aData);
+ } else {
+ Services.obs.notifyObservers(null, "getUserMedia:response:deny", aSubject.callID);
+ }});
+ } else if (aTopic === "PeerConnection:request") {
+ this.handlePCRequest(aSubject, aTopic, aData);
+ } else if (aTopic === "recording-device-events") {
+ switch (aData) {
+ case "shutdown":
+ case "starting":
+ this.notify();
+ break;
+ }
+ } else if (aTopic === "VideoCapture:Paused") {
+ if (this._notificationId) {
+ Notifications.cancel(this._notificationId);
+ this._notificationId = null;
+ }
+ } else if (aTopic === "VideoCapture:Resumed") {
+ this.notify();
+ }
+ },
+
+ notify: function() {
+ let windows = MediaManagerService.activeMediaCaptureWindows;
+ let count = windows.length;
+ let msg = {};
+ if (count == 0) {
+ if (this._notificationId) {
+ Notifications.cancel(this._notificationId);
+ this._notificationId = null;
+ }
+ } else {
+ let notificationOptions = {
+ title: Strings.brand.GetStringFromName("brandShortName"),
+ when: null, // hide the date row
+ light: [0xFF9500FF, 1000, 1000],
+ ongoing: true
+ };
+
+ let cameraActive = false;
+ let audioActive = false;
+ for (let i = 0; i < count; i++) {
+ let win = windows.queryElementAt(i, Ci.nsIDOMWindow);
+ let hasAudio = {};
+ let hasVideo = {};
+ MediaManagerService.mediaCaptureWindowState(win, hasVideo, hasAudio);
+ if (hasVideo.value) cameraActive = true;
+ if (hasAudio.value) audioActive = true;
+ }
+
+ if (cameraActive && audioActive) {
+ notificationOptions.message = Strings.browser.GetStringFromName("getUserMedia.sharingCameraAndMicrophone.message2");
+ notificationOptions.icon = "drawable:alert_mic_camera";
+ } else if (cameraActive) {
+ notificationOptions.message = Strings.browser.GetStringFromName("getUserMedia.sharingCamera.message2");
+ notificationOptions.icon = "drawable:alert_camera";
+ } else if (audioActive) {
+ notificationOptions.message = Strings.browser.GetStringFromName("getUserMedia.sharingMicrophone.message2");
+ notificationOptions.icon = "drawable:alert_mic";
+ } else {
+ // somethings wrong. lets throw
+ throw "Couldn't find any cameras or microphones being used"
+ }
+
+ if (this._notificationId)
+ Notifications.update(this._notificationId, notificationOptions);
+ else
+ this._notificationId = Notifications.create(notificationOptions);
+ if (count > 1)
+ msg.count = count;
+ }
+ },
+
+ handlePCRequest: function handlePCRequest(aSubject, aTopic, aData) {
+ aSubject = aSubject.wrappedJSObject;
+ let { callID } = aSubject;
+ // Also available: windowID, isSecure, innerWindowID. For contentWindow do:
+ //
+ // let contentWindow = Services.wm.getOuterWindowWithId(windowID);
+
+ Services.obs.notifyObservers(null, "PeerConnection:response:allow", callID);
+ },
+
+ handleGumRequest: function handleGumRequest(aSubject, aTopic, aData) {
+ let constraints = aSubject.getConstraints();
+ let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID);
+
+ contentWindow.navigator.mozGetUserMediaDevices(
+ constraints,
+ function (devices) {
+ if (!ParentalControls.isAllowed(ParentalControls.CAMERA_MICROPHONE)) {
+ Services.obs.notifyObservers(null, "getUserMedia:response:deny", aSubject.callID);
+ WebrtcUI.showBlockMessage(devices);
+ return;
+ }
+
+ WebrtcUI.prompt(contentWindow, aSubject.callID, constraints.audio,
+ constraints.video, devices);
+ },
+ function (error) {
+ Cu.reportError(error);
+ },
+ aSubject.innerWindowID,
+ aSubject.callID);
+ },
+
+ getDeviceButtons: function(audioDevices, videoDevices, aCallID, aUri) {
+ return [{
+ label: Strings.browser.GetStringFromName("getUserMedia.denyRequest.label"),
+ callback: function() {
+ Services.obs.notifyObservers(null, "getUserMedia:response:deny", aCallID);
+ }
+ },
+ {
+ label: Strings.browser.GetStringFromName("getUserMedia.shareRequest.label"),
+ callback: function(checked /* ignored */, inputs) {
+ let allowedDevices = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+
+ let audioId = 0;
+ if (inputs && inputs.audioDevice != undefined)
+ audioId = inputs.audioDevice;
+ if (audioDevices[audioId])
+ allowedDevices.appendElement(audioDevices[audioId], /*weak =*/ false);
+
+ let videoId = 0;
+ if (inputs && inputs.videoSource != undefined)
+ videoId = inputs.videoSource;
+ if (videoDevices[videoId]) {
+ allowedDevices.appendElement(videoDevices[videoId], /*weak =*/ false);
+ let perms = Services.perms;
+ // Although the lifetime is "session" it will be removed upon
+ // use so it's more of a one-shot.
+ perms.add(aUri, "MediaManagerVideo", perms.ALLOW_ACTION, perms.EXPIRE_SESSION);
+ }
+
+ Services.obs.notifyObservers(allowedDevices, "getUserMedia:response:allow", aCallID);
+ },
+ positive: true
+ }];
+ },
+
+ _determineNeededRuntimePermissions: function(aSubject) {
+ let permissions = [];
+
+ let constraints = aSubject.getConstraints();
+ if (constraints.video) {
+ permissions.push(RuntimePermissions.CAMERA);
+ }
+ if (constraints.audio) {
+ permissions.push(RuntimePermissions.RECORD_AUDIO);
+ }
+
+ return permissions;
+ },
+
+ // Get a list of string names for devices. Ensures that none of the strings are blank
+ _getList: function(aDevices, aType) {
+ let defaultCount = 0;
+ return aDevices.map(function(device) {
+ // if this is a Camera input, convert the name to something readable
+ let res = /Camera\ \d+,\ Facing (front|back)/.exec(device.name);
+ if (res)
+ return Strings.browser.GetStringFromName("getUserMedia." + aType + "." + res[1] + "Camera");
+
+ if (device.name.startsWith("&") && device.name.endsWith(";"))
+ return Strings.browser.GetStringFromName(device.name.substring(1, device.name.length -1));
+
+ if (device.name.trim() == "") {
+ defaultCount++;
+ return Strings.browser.formatStringFromName("getUserMedia." + aType + ".default", [defaultCount], 1);
+ }
+ return device.name
+ }, this);
+ },
+
+ _addDevicesToOptions: function(aDevices, aType, aOptions) {
+ if (aDevices.length) {
+
+ // Filter out empty items from the list
+ let list = this._getList(aDevices, aType);
+
+ if (list.length > 0) {
+ aOptions.inputs.push({
+ id: aType,
+ type: "menulist",
+ label: Strings.browser.GetStringFromName("getUserMedia." + aType + ".prompt"),
+ values: list
+ });
+
+ }
+ }
+ },
+
+ showBlockMessage: function(aDevices) {
+ let microphone = false;
+ let camera = false;
+
+ for (let device of aDevices) {
+ device = device.QueryInterface(Ci.nsIMediaDevice);
+ if (device.type == "audio") {
+ microphone = true;
+ } else if (device.type == "video") {
+ camera = true;
+ }
+ }
+
+ let message;
+ if (microphone && !camera) {
+ message = Strings.browser.GetStringFromName("getUserMedia.blockedMicrophoneAccess");
+ } else if (camera && !microphone) {
+ message = Strings.browser.GetStringFromName("getUserMedia.blockedCameraAccess");
+ } else {
+ message = Strings.browser.GetStringFromName("getUserMedia.blockedCameraAndMicrophoneAccess");
+ }
+
+ NativeWindow.doorhanger.show(message, "webrtc-blocked", [], BrowserApp.selectedTab.id, {});
+ },
+
+ prompt: function prompt(aContentWindow, aCallID, aAudioRequested,
+ aVideoRequested, aDevices) {
+ let audioDevices = [];
+ let videoDevices = [];
+ for (let device of aDevices) {
+ device = device.QueryInterface(Ci.nsIMediaDevice);
+ switch (device.type) {
+ case "audio":
+ if (aAudioRequested)
+ audioDevices.push(device);
+ break;
+ case "video":
+ if (aVideoRequested)
+ videoDevices.push(device);
+ break;
+ }
+ }
+
+ let requestType;
+ if (audioDevices.length && videoDevices.length)
+ requestType = "CameraAndMicrophone";
+ else if (audioDevices.length)
+ requestType = "Microphone";
+ else if (videoDevices.length)
+ requestType = "Camera";
+ else
+ return;
+
+ let uri = aContentWindow.document.documentURIObject;
+ let host = uri.host;
+ let requestor = BrowserApp.manifest ? "'" + BrowserApp.manifest.name + "'" : host;
+ let message = Strings.browser.formatStringFromName("getUserMedia.share" + requestType + ".message", [ requestor ], 1);
+
+ let options = { inputs: [] };
+ if (videoDevices.length > 1 || audioDevices.length > 0) {
+ // videoSource is both the string used for l10n lookup and the object that will be returned
+ this._addDevicesToOptions(videoDevices, "videoSource", options);
+ }
+
+ if (audioDevices.length > 1 || videoDevices.length > 0) {
+ this._addDevicesToOptions(audioDevices, "audioDevice", options);
+ }
+
+ let buttons = this.getDeviceButtons(audioDevices, videoDevices, aCallID, uri);
+
+ NativeWindow.doorhanger.show(message, "webrtc-request", buttons, BrowserApp.selectedTab.id, options, "WEBRTC");
+ }
+}
diff --git a/mobile/android/chrome/content/about.js b/mobile/android/chrome/content/about.js
new file mode 100644
index 000000000..8c9acdf8a
--- /dev/null
+++ b/mobile/android/chrome/content/about.js
@@ -0,0 +1,151 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils, Cr = Components.results;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function init() {
+ // Include the build date and a warning about Telemetry
+ // if this is an "a#" (nightly or aurora) build
+#expand const version = "__MOZ_APP_VERSION_DISPLAY__";
+ if (/a\d+$/.test(version)) {
+ let buildID = Services.appinfo.appBuildID;
+ let buildDate = buildID.slice(0, 4) + "-" + buildID.slice(4, 6) + "-" + buildID.slice(6, 8);
+ let br = document.createElement("br");
+ let versionPara = document.getElementById("version");
+ versionPara.appendChild(br);
+ let date = document.createTextNode("(" + buildDate + ")");
+ versionPara.appendChild(date);
+ document.getElementById("telemetry").hidden = false;
+ }
+
+ // Include the Distribution information if available
+ try {
+ let distroId = Services.prefs.getCharPref("distribution.id");
+ if (distroId) {
+ let distroVersion = Services.prefs.getCharPref("distribution.version");
+ let distroIdField = document.getElementById("distributionID");
+ distroIdField.textContent = distroId + " - " + distroVersion;
+ distroIdField.hidden = false;
+
+ let distroAbout = Services.prefs.getComplexValue("distribution.about", Ci.nsISupportsString);
+ let distroField = document.getElementById("distributionAbout");
+ distroField.textContent = distroAbout;
+ distroField.hidden = false;
+ }
+ } catch (e) {
+ // Pref is unset
+ }
+
+ // get URLs from prefs
+ try {
+ let formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].getService(Ci.nsIURLFormatter);
+
+ let links = [
+ {id: "releaseNotesURL", pref: "app.releaseNotesURL"},
+ {id: "supportURL", pref: "app.supportURL"},
+ {id: "faqURL", pref: "app.faqURL"},
+ {id: "privacyURL", pref: "app.privacyURL"},
+ {id: "creditsURL", pref: "app.creditsURL"},
+ ];
+
+ links.forEach(function(link) {
+ let url = formatter.formatURLPref(link.pref);
+ let element = document.getElementById(link.id);
+ element.setAttribute("href", url);
+ });
+ } catch (ex) {}
+
+#ifdef MOZ_UPDATER
+ let Updater = {
+ update: null,
+
+ init: function() {
+ Services.obs.addObserver(this, "Update:CheckResult", false);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == "Update:CheckResult") {
+ showUpdateMessage(aData);
+ }
+ },
+ };
+
+ Updater.init();
+
+ function checkForUpdates() {
+ showCheckingMessage();
+
+ Services.androidBridge.handleGeckoMessage({ type: "Update:Check" });
+ }
+
+ function downloadUpdate() {
+ Services.androidBridge.handleGeckoMessage({ type: "Update:Download" });
+ }
+
+ function installUpdate() {
+ showCheckAction();
+
+ Services.androidBridge.handleGeckoMessage({ type: "Update:Install" });
+ }
+
+ let updateLink = document.getElementById("updateLink");
+ let checkingSpan = document.getElementById("update-message-checking");
+ let noneSpan = document.getElementById("update-message-none");
+ let foundSpan = document.getElementById("update-message-found");
+ let downloadingSpan = document.getElementById("update-message-downloading");
+ let downloadedSpan = document.getElementById("update-message-downloaded");
+
+ updateLink.onclick = checkForUpdates;
+ foundSpan.onclick = downloadUpdate;
+ downloadedSpan.onclick = installUpdate;
+
+ function showCheckAction() {
+ checkingSpan.style.display = "none";
+ noneSpan.style.display = "none";
+ foundSpan.style.display = "none";
+ downloadingSpan.style.display = "none";
+ downloadedSpan.style.display = "none";
+ updateLink.style.display = "block";
+ }
+
+ function showCheckingMessage() {
+ updateLink.style.display = "none";
+ noneSpan.style.display = "none";
+ foundSpan.style.display = "none";
+ downloadingSpan.style.display = "none";
+ downloadedSpan.style.display = "none";
+ checkingSpan.style.display = "block";
+ }
+
+ function showUpdateMessage(aResult) {
+ updateLink.style.display = "none";
+ checkingSpan.style.display = "none";
+ noneSpan.style.display = "none";
+ foundSpan.style.display = "none";
+ downloadingSpan.style.display = "none";
+ downloadedSpan.style.display = "none";
+
+ // the aResult values come from mobile/android/base/UpdateServiceHelper.java
+ switch (aResult) {
+ case "NOT_AVAILABLE":
+ noneSpan.style.display = "block";
+ setTimeout(showCheckAction, 2000);
+ break;
+ case "AVAILABLE":
+ foundSpan.style.display = "block";
+ break;
+ case "DOWNLOADING":
+ downloadingSpan.style.display = "block";
+ break;
+ case "DOWNLOADED":
+ downloadedSpan.style.display = "block";
+ break;
+ }
+ }
+#endif
+}
+
+document.addEventListener("DOMContentLoaded", init, false);
diff --git a/mobile/android/chrome/content/about.xhtml b/mobile/android/chrome/content/about.xhtml
new file mode 100644
index 000000000..8a4c28357
--- /dev/null
+++ b/mobile/android/chrome/content/about.xhtml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE html [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
+%globalDTD;
+<!ENTITY % fennecDTD SYSTEM "chrome://browser/locale/about.dtd">
+%fennecDTD;
+]>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <meta name="viewport" content="width=480; initial-scale=.6667; user-scalable=no"/>
+ <title>&aboutPage.title;</title>
+ <link rel="stylesheet" href="chrome://browser/skin/aboutPage.css" type="text/css"/>
+ <link rel="icon" type="image/png" sizes="64x64" href="chrome://branding/content/favicon64.png" />
+</head>
+
+<body dir="&locale.dir;">
+ <div id="header">
+ <div id="wordmark"></div>
+#expand <p id="version">__MOZ_APP_VERSION_DISPLAY__</p>
+ </div>
+
+ <div id="banner">
+ <div id="logo"/>
+#ifdef MOZ_UPDATER
+ <div id="updateBox">
+ <a id="updateLink" href="">&aboutPage.checkForUpdates.link;</a>
+ <span id="update-message-checking">&aboutPage.checkForUpdates.checking;</span>
+ <span id="update-message-none">&aboutPage.checkForUpdates.none;</span>
+ <span id="update-message-found">&aboutPage.checkForUpdates.available2;</span>
+ <span id="update-message-downloading">&aboutPage.checkForUpdates.downloading;</span>
+ <span id="update-message-downloaded">&aboutPage.checkForUpdates.downloaded2;</span>
+ </div>
+#endif
+
+ <div id="messages">
+ <p id="distributionAbout" hidden="true"/>
+ <p id="distributionID" hidden="true"/>
+ <p id="telemetry" hidden="true">
+ &aboutPage.warningVersion;
+#ifdef MOZ_TELEMETRY_ON_BY_DEFAULT
+ &aboutPage.telemetryStart;<a href="https://www.mozilla.org/">&aboutPage.telemetryMozillaLink;</a>&aboutPage.telemetryEnd;
+#endif
+ </p>
+ </div>
+
+ </div>
+
+ <ul id="aboutLinks">
+ <div class="top-border"></div>
+ <li><a id="faqURL">&aboutPage.faq.label;</a></li>
+ <li><a id="supportURL">&aboutPage.support.label;</a></li>
+ <li><a id="privacyURL">&aboutPage.privacyPolicy.label;</a></li>
+ <li><a href="about:rights">&aboutPage.rights.label;</a></li>
+ <li><a id="releaseNotesURL">&aboutPage.relNotes.label;</a></li>
+ <li><a id="creditsURL">&aboutPage.credits.label;</a></li>
+ <li><a href="about:license">&aboutPage.license.label;</a></li>
+ <div class="bottom-border"></div>
+ </ul>
+
+#ifdef RELEASE_OR_BETA
+ <div id="aboutDetails">
+ <p>&aboutPage.logoTrademark;</p>
+ </div>
+#endif
+
+ <script type="application/javascript;version=1.8" src="chrome://browser/content/about.js" />
+
+</body>
+</html>
diff --git a/mobile/android/chrome/content/aboutAccounts.js b/mobile/android/chrome/content/aboutAccounts.js
new file mode 100644
index 000000000..4801a76a1
--- /dev/null
+++ b/mobile/android/chrome/content/aboutAccounts.js
@@ -0,0 +1,351 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Wrap a remote fxa-content-server.
+ *
+ * An about:accounts tab loads and displays an fxa-content-server page,
+ * depending on the current Android Account status and an optional 'action'
+ * parameter.
+ *
+ * We show a spinner while the remote iframe is loading. We expect the
+ * WebChannel message listening to the fxa-content-server to send this tab's
+ * <browser>'s messageManager a LOADED message when the remote iframe provides
+ * the WebChannel LOADED message. See the messageManager registration and the
+ * |loadedDeferred| promise. This loosely couples the WebChannel implementation
+ * and about:accounts! (We need this coupling in order to distinguish
+ * WebChannel LOADED messages produced by multiple about:accounts tabs.)
+ *
+ * We capture error conditions by accessing the inner nsIWebNavigation of the
+ * iframe directly.
+ */
+
+"use strict";
+
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components; /*global Components */
+
+Cu.import("resource://gre/modules/Accounts.jsm"); /*global Accounts */
+Cu.import("resource://gre/modules/PromiseUtils.jsm"); /*global PromiseUtils */
+Cu.import("resource://gre/modules/Services.jsm"); /*global Services */
+Cu.import("resource://gre/modules/XPCOMUtils.jsm"); /*global XPCOMUtils */
+
+const ACTION_URL_PARAM = "action";
+
+const COMMAND_LOADED = "fxaccounts:loaded";
+
+const log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("FxAccounts");
+
+XPCOMUtils.defineLazyServiceGetter(this, "ParentalControls",
+ "@mozilla.org/parental-controls-service;1", "nsIParentalControlsService");
+
+// Shows the toplevel element with |id| to be shown - all other top-level
+// elements are hidden.
+// If |id| is 'spinner', then 'remote' is also shown, with opacity 0.
+function show(id) {
+ let allTop = document.querySelectorAll(".toplevel");
+ for (let elt of allTop) {
+ if (elt.getAttribute("id") == id) {
+ elt.style.display = 'block';
+ } else {
+ elt.style.display = 'none';
+ }
+ }
+ if (id == 'spinner') {
+ document.getElementById('remote').style.display = 'block';
+ document.getElementById('remote').style.opacity = 0;
+ }
+}
+
+// Each time we try to load the remote <iframe>, loadedDeferred is replaced. It
+// is resolved by a LOADED message, and rejected by a failure to load.
+var loadedDeferred = null;
+
+// We have a new load starting. Replace the existing promise with a new one,
+// and queue up the transition to remote content.
+function deferTransitionToRemoteAfterLoaded() {
+ log.d('Waiting for LOADED message.');
+
+ loadedDeferred = PromiseUtils.defer();
+ loadedDeferred.promise.then(() => {
+ log.d('Got LOADED message!');
+ document.getElementById("remote").style.opacity = 0;
+ show("remote");
+ document.getElementById("remote").style.opacity = 1;
+ })
+ .catch((e) => {
+ log.w('Did not get LOADED message: ' + e.toString());
+ });
+}
+
+function handleLoadedMessage(message) {
+ loadedDeferred.resolve();
+};
+
+var wrapper = {
+ iframe: null,
+
+ url: null,
+
+ init: function (url) {
+ this.url = url;
+ deferTransitionToRemoteAfterLoaded();
+
+ let iframe = document.getElementById("remote");
+ this.iframe = iframe;
+ this.iframe.QueryInterface(Ci.nsIFrameLoaderOwner);
+ let docShell = this.iframe.frameLoader.docShell;
+ docShell.QueryInterface(Ci.nsIWebProgress);
+ docShell.addProgressListener(this.iframeListener, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
+
+ // Set the iframe's location with loadURI/LOAD_FLAGS_BYPASS_HISTORY to
+ // avoid having a new history entry being added.
+ let webNav = iframe.frameLoader.docShell.QueryInterface(Ci.nsIWebNavigation);
+ webNav.loadURI(url, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, null, null, null);
+ },
+
+ retry: function () {
+ deferTransitionToRemoteAfterLoaded();
+
+ let webNav = this.iframe.frameLoader.docShell.QueryInterface(Ci.nsIWebNavigation);
+ webNav.loadURI(this.url, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, null, null, null);
+ },
+
+ iframeListener: {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference,
+ Ci.nsISupports]),
+
+ onStateChange: function(aWebProgress, aRequest, aState, aStatus) {
+ let failure = false;
+
+ // Captive portals sometimes redirect users
+ if ((aState & Ci.nsIWebProgressListener.STATE_REDIRECTING)) {
+ failure = true;
+ } else if ((aState & Ci.nsIWebProgressListener.STATE_STOP)) {
+ if (aRequest instanceof Ci.nsIHttpChannel) {
+ try {
+ failure = aRequest.responseStatus != 200;
+ } catch (e) {
+ failure = aStatus != Components.results.NS_OK;
+ }
+ }
+ }
+
+ // Calling cancel() will raise some OnStateChange notifications by itself,
+ // so avoid doing that more than once
+ if (failure && aStatus != Components.results.NS_BINDING_ABORTED) {
+ aRequest.cancel(Components.results.NS_BINDING_ABORTED);
+ // Since after a promise is fulfilled, subsequent fulfillments are
+ // treated as no-ops, we don't care that we might see multiple failures
+ // due to multiple listener callbacks. (It's not easy to extract this
+ // from the Promises spec, but it is widely quoted. Start with
+ // http://stackoverflow.com/a/18218542.)
+ loadedDeferred.reject(new Error("Failed in onStateChange!"));
+ show("networkError");
+ }
+ },
+
+ onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) {
+ if (aRequest && aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
+ aRequest.cancel(Components.results.NS_BINDING_ABORTED);
+ // As above, we're not concerned by multiple listener callbacks.
+ loadedDeferred.reject(new Error("Failed in onLocationChange!"));
+ show("networkError");
+ }
+ },
+
+ onProgressChange: function() {},
+ onStatusChange: function() {},
+ onSecurityChange: function() {},
+ },
+};
+
+
+function retry() {
+ log.i("Retrying.");
+ show("spinner");
+ wrapper.retry();
+}
+
+function openPrefs() {
+ log.i("Opening Sync preferences.");
+ // If an Android Account exists, this will open the Status Activity.
+ // Otherwise, it will begin the Get Started flow. This should only be shown
+ // when an Account actually exists.
+ Accounts.launchSetup();
+}
+
+function getURLForAction(action, urlParams) {
+ let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.webchannel.uri");
+ url = url + (url.endsWith("/") ? "" : "/") + action;
+ const CONTEXT = "fx_fennec_v1";
+ // The only service managed by Fennec, to date, is Firefox Sync.
+ const SERVICE = "sync";
+ urlParams = urlParams || new URLSearchParams("");
+ urlParams.set('service', SERVICE);
+ urlParams.set('context', CONTEXT);
+ // Ideally we'd just merge urlParams with new URL(url).searchParams, but our
+ // URLSearchParams implementation doesn't support iteration (bug 1085284).
+ let urlParamStr = urlParams.toString();
+ if (urlParamStr) {
+ url += (url.includes("?") ? "&" : "?") + urlParamStr;
+ }
+ return url;
+}
+
+function updateDisplayedEmail(user) {
+ let emailDiv = document.getElementById("email");
+ if (emailDiv && user) {
+ emailDiv.textContent = user.email;
+ }
+}
+
+function init() {
+ // Test for restrictions before getFirefoxAccount(), since that will fail if
+ // we are restricted.
+ if (!ParentalControls.isAllowed(ParentalControls.MODIFY_ACCOUNTS)) {
+ // It's better to log and show an error message than to invite user
+ // confusion by removing about:accounts entirely. That is, if the user is
+ // restricted, this way they'll discover as much and may be able to get
+ // out of their restricted profile. If we remove about:accounts entirely,
+ // it will look like Fennec is buggy, and the user will be very confused.
+ log.e("This profile cannot connect to Firefox Accounts: showing restricted error.");
+ show("restrictedError");
+ return;
+ }
+
+ Accounts.getFirefoxAccount().then(user => {
+ // It's possible for the window to start closing before getting the user
+ // completes. Tests in particular can cause this.
+ if (window.closed) {
+ return;
+ }
+
+ updateDisplayedEmail(user);
+
+ // Ideally we'd use new URL(document.URL).searchParams, but for about: URIs,
+ // searchParams is empty.
+ let urlParams = new URLSearchParams(document.URL.split("?")[1] || "");
+ let action = urlParams.get(ACTION_URL_PARAM);
+ urlParams.delete(ACTION_URL_PARAM);
+
+ switch (action) {
+ case "signup":
+ if (user) {
+ // Asking to sign-up when already signed in just shows prefs.
+ show("prefs");
+ } else {
+ show("spinner");
+ wrapper.init(getURLForAction("signup", urlParams));
+ }
+ break;
+ case "signin":
+ if (user) {
+ // Asking to sign-in when already signed in just shows prefs.
+ show("prefs");
+ } else {
+ show("spinner");
+ wrapper.init(getURLForAction("signin", urlParams));
+ }
+ break;
+ case "force_auth":
+ if (user) {
+ show("spinner");
+ urlParams.set("email", user.email); // In future, pin using the UID.
+ wrapper.init(getURLForAction("force_auth", urlParams));
+ } else {
+ show("spinner");
+ wrapper.init(getURLForAction("signup", urlParams));
+ }
+ break;
+ case "manage":
+ if (user) {
+ show("spinner");
+ urlParams.set("email", user.email); // In future, pin using the UID.
+ wrapper.init(getURLForAction("settings", urlParams));
+ } else {
+ show("spinner");
+ wrapper.init(getURLForAction("signup", urlParams));
+ }
+ break;
+ case "avatar":
+ if (user) {
+ show("spinner");
+ urlParams.set("email", user.email); // In future, pin using the UID.
+ wrapper.init(getURLForAction("settings/avatar/change", urlParams));
+ } else {
+ show("spinner");
+ wrapper.init(getURLForAction("signup", urlParams));
+ }
+ break;
+ default:
+ // Unrecognized or no action specified.
+ if (action) {
+ log.w("Ignoring unrecognized action: " + action);
+ }
+ if (user) {
+ show("prefs");
+ } else {
+ show("spinner");
+ wrapper.init(getURLForAction("signup", urlParams));
+ }
+ break;
+ }
+ }).catch(e => {
+ log.e("Failed to get the signed in user: " + e.toString());
+ });
+}
+
+document.addEventListener("DOMContentLoaded", function onload() {
+ document.removeEventListener("DOMContentLoaded", onload, true);
+ init();
+ var buttonRetry = document.getElementById('buttonRetry');
+ buttonRetry.addEventListener('click', retry);
+
+ var buttonOpenPrefs = document.getElementById('buttonOpenPrefs');
+ buttonOpenPrefs.addEventListener('click', openPrefs);
+}, true);
+
+// This window is contained in a XUL <browser> element. Return the
+// messageManager of that <browser> element, or null.
+function getBrowserMessageManager() {
+ let browser = window
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow)
+ .QueryInterface(Ci.nsIDOMChromeWindow)
+ .BrowserApp
+ .getBrowserForDocument(document);
+ if (browser) {
+ return browser.messageManager;
+ }
+ return null;
+}
+
+// Add a single listener for 'loaded' messages from the iframe in this
+// <browser>. These 'loaded' messages are ferried from the WebChannel to just
+// this <browser>.
+var mm = getBrowserMessageManager();
+if (mm) {
+ mm.addMessageListener(COMMAND_LOADED, handleLoadedMessage);
+} else {
+ log.e('No messageManager, not listening for LOADED message!');
+}
+
+window.addEventListener("unload", function(event) {
+ try {
+ let mm = getBrowserMessageManager();
+ if (mm) {
+ mm.removeMessageListener(COMMAND_LOADED, handleLoadedMessage);
+ }
+ } catch (e) {
+ // This could fail if the page is being torn down, the tab is being
+ // destroyed, etc.
+ log.w('Not removing listener for LOADED message: ' + e.toString());
+ }
+});
diff --git a/mobile/android/chrome/content/aboutAccounts.xhtml b/mobile/android/chrome/content/aboutAccounts.xhtml
new file mode 100644
index 000000000..b988741d5
--- /dev/null
+++ b/mobile/android/chrome/content/aboutAccounts.xhtml
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" >
+%globalDTD;
+<!ENTITY % aboutDTD SYSTEM "chrome://browser/locale/aboutAccounts.dtd">
+%aboutDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml" dir="&locale.dir;">
+ <head>
+ <title>Firefox Sync</title>
+ <meta name="viewport" content="width=device-width; user-scalable=0" />
+ <link rel="icon" type="image/png" sizes="64x64" href="chrome://branding/content/favicon64.png" />
+ <link rel="stylesheet" href="chrome://browser/skin/spinner.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/skin/aboutBase.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/skin/aboutAccounts.css" type="text/css"/>
+ </head>
+ <body>
+ <div id="spinner" class="toplevel">
+ <div class="container flex-column">
+ <!-- Empty text-container for spacing. -->
+ <div class="text-container flex-column" />
+
+ <div class="mui-refresh-main">
+ <div class="mui-refresh-wrapper">
+ <div class="mui-spinner-wrapper">
+ <div class="mui-spinner-main">
+ <div class="mui-spinner-left">
+ <div class="mui-half-circle-left" />
+ </div>
+ <div class="mui-spinner-right">
+ <div class="mui-half-circle-right" />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ </div>
+ </div>
+
+ <iframe mozframetype="content" id="remote" class="toplevel" />
+
+ <div id="prefs" class="toplevel">
+ <div class="container flex-column">
+ <div class="text-container flex-column">
+ <div class="text">&aboutAccounts.connected.title;</div>
+ <div class="hint">&aboutAccounts.connected.description;</div>
+ <div id="email" class="hint"></div>
+ </div>
+ <a id="buttonOpenPrefs" tabindex="0" href="#">&aboutAccounts.syncPreferences.label;</a>
+ </div>
+ </div>
+
+ <div id="networkError" class="toplevel">
+ <div class="container flex-column">
+ <div class="text-container flex-column">
+ <div class="text">&aboutAccounts.noConnection.title;</div>
+ </div>
+ <div class="button-row">
+ <button id="buttonRetry" class="button" tabindex="1">&aboutAccounts.retry.label;</button>
+ </div>
+ </div>
+ </div>
+
+ <div id="restrictedError" class="toplevel">
+ <div class="container flex-column">
+ <div class="text-container flex-column">
+ <div class="text">&aboutAccounts.restrictedError.title;</div>
+ <div class="hint">&aboutAccounts.restrictedError.description;</div>
+ </div>
+ </div>
+ </div>
+
+ <script type="application/javascript;version=1.8" src="chrome://browser/content/aboutAccounts.js"></script>
+ </body>
+</html>
diff --git a/mobile/android/chrome/content/aboutAddons.js b/mobile/android/chrome/content/aboutAddons.js
new file mode 100644
index 000000000..becf56a32
--- /dev/null
+++ b/mobile/android/chrome/content/aboutAddons.js
@@ -0,0 +1,609 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/*globals gChromeWin */
+
+var Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm")
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const AMO_ICON = "chrome://browser/skin/images/amo-logo.png";
+
+var gStringBundle = Services.strings.createBundle("chrome://browser/locale/aboutAddons.properties");
+
+XPCOMUtils.defineLazyGetter(window, "gChromeWin", function() {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow)
+ .QueryInterface(Ci.nsIDOMChromeWindow);
+});
+XPCOMUtils.defineLazyModuleGetter(window, "Preferences",
+ "resource://gre/modules/Preferences.jsm");
+
+var ContextMenus = {
+ target: null,
+
+ init: function() {
+ document.addEventListener("contextmenu", this, false);
+
+ document.getElementById("contextmenu-enable").addEventListener("click", ContextMenus.enable.bind(this), false);
+ document.getElementById("contextmenu-disable").addEventListener("click", ContextMenus.disable.bind(this), false);
+ document.getElementById("contextmenu-uninstall").addEventListener("click", ContextMenus.uninstall.bind(this), false);
+
+ // XXX - Hack to fix bug 985867 for now
+ document.addEventListener("touchstart", function() { });
+ },
+
+ handleEvent: function(event) {
+ // store the target of context menu events so that we know which app to act on
+ this.target = event.target;
+ while (!this.target.hasAttribute("contextmenu")) {
+ this.target = this.target.parentNode;
+ }
+
+ if (!this.target) {
+ document.getElementById("contextmenu-enable").setAttribute("hidden", "true");
+ document.getElementById("contextmenu-disable").setAttribute("hidden", "true");
+ document.getElementById("contextmenu-uninstall").setAttribute("hidden", "true");
+ return;
+ }
+
+ let addon = this.target.addon;
+ if (addon.scope == AddonManager.SCOPE_APPLICATION) {
+ document.getElementById("contextmenu-uninstall").setAttribute("hidden", "true");
+ } else {
+ document.getElementById("contextmenu-uninstall").removeAttribute("hidden");
+ }
+
+ // Hide the enable/disable context menu items if the add-on was disabled by
+ // Firefox (e.g. unsigned or blocklisted add-on).
+ if (addon.appDisabled) {
+ document.getElementById("contextmenu-enable").setAttribute("hidden", "true");
+ document.getElementById("contextmenu-disable").setAttribute("hidden", "true");
+ return;
+ }
+
+ let enabled = this.target.getAttribute("isDisabled") != "true";
+ if (enabled) {
+ document.getElementById("contextmenu-enable").setAttribute("hidden", "true");
+ document.getElementById("contextmenu-disable").removeAttribute("hidden");
+ } else {
+ document.getElementById("contextmenu-enable").removeAttribute("hidden");
+ document.getElementById("contextmenu-disable").setAttribute("hidden", "true");
+ }
+ },
+
+ enable: function(event) {
+ Addons.setEnabled(true, this.target.addon);
+ this.target = null;
+ },
+
+ disable: function (event) {
+ Addons.setEnabled(false, this.target.addon);
+ this.target = null;
+ },
+
+ uninstall: function (event) {
+ Addons.uninstall(this.target.addon);
+ this.target = null;
+ }
+}
+
+function init() {
+ window.addEventListener("popstate", onPopState, false);
+
+ AddonManager.addInstallListener(Addons);
+ AddonManager.addAddonListener(Addons);
+ Addons.init();
+ showList();
+ ContextMenus.init();
+}
+
+
+function uninit() {
+ AddonManager.removeInstallListener(Addons);
+ AddonManager.removeAddonListener(Addons);
+}
+
+function openLink(url) {
+ let BrowserApp = gChromeWin.BrowserApp;
+ BrowserApp.addTab(url, { selected: true, parentId: BrowserApp.selectedTab.id });
+}
+
+function onPopState(aEvent) {
+ // Called when back/forward is used to change the state of the page
+ if (aEvent.state) {
+ // Show the detail page for an addon
+ Addons.showDetails(Addons._getElementForAddon(aEvent.state.id));
+ } else {
+ // Clear any previous detail addon
+ let detailItem = document.querySelector("#addons-details > .addon-item");
+ detailItem.addon = null;
+
+ showList();
+ }
+}
+
+function showList() {
+ // Hide the detail page and show the list
+ let details = document.querySelector("#addons-details");
+ details.style.display = "none";
+ let list = document.querySelector("#addons-list");
+ list.style.display = "block";
+ document.documentElement.removeAttribute("details");
+}
+
+var Addons = {
+ _restartCount: 0,
+
+ _createItem: function _createItem(aAddon) {
+ let outer = document.createElement("div");
+ outer.setAttribute("addonID", aAddon.id);
+ outer.className = "addon-item list-item";
+ outer.setAttribute("role", "button");
+ outer.setAttribute("contextmenu", "addonmenu");
+ outer.addEventListener("click", function() {
+ this.showDetails(outer);
+ history.pushState({ id: aAddon.id }, document.title);
+ }.bind(this), true);
+
+ let img = document.createElement("img");
+ img.className = "icon";
+ img.setAttribute("src", aAddon.iconURL || AMO_ICON);
+ outer.appendChild(img);
+
+ let inner = document.createElement("div");
+ inner.className = "inner";
+
+ let details = document.createElement("div");
+ details.className = "details";
+ inner.appendChild(details);
+
+ let titlePart = document.createElement("div");
+ titlePart.textContent = aAddon.name;
+ titlePart.className = "title";
+ details.appendChild(titlePart);
+
+ let versionPart = document.createElement("div");
+ versionPart.textContent = aAddon.version;
+ versionPart.className = "version";
+ details.appendChild(versionPart);
+
+ if ("description" in aAddon) {
+ let descPart = document.createElement("div");
+ descPart.textContent = aAddon.description;
+ descPart.className = "description";
+ inner.appendChild(descPart);
+ }
+
+ outer.appendChild(inner);
+ return outer;
+ },
+
+ _createBrowseItem: function _createBrowseItem() {
+ let outer = document.createElement("div");
+ outer.className = "addon-item list-item";
+ outer.setAttribute("role", "button");
+ outer.addEventListener("click", function(event) {
+ try {
+ let formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].getService(Ci.nsIURLFormatter);
+ openLink(formatter.formatURLPref("extensions.getAddons.browseAddons"));
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }, true);
+
+ let img = document.createElement("img");
+ img.className = "icon";
+ img.setAttribute("src", AMO_ICON);
+ outer.appendChild(img);
+
+ let inner = document.createElement("div");
+ inner.className = "inner";
+
+ let title = document.createElement("div");
+ title.id = "browse-title";
+ title.className = "title";
+ title.textContent = gStringBundle.GetStringFromName("addons.browseAll");
+ inner.appendChild(title);
+
+ outer.appendChild(inner);
+ return outer;
+ },
+
+ _createItemForAddon: function _createItemForAddon(aAddon) {
+ let appManaged = (aAddon.scope == AddonManager.SCOPE_APPLICATION);
+ let opType = this._getOpTypeForOperations(aAddon.pendingOperations);
+ let updateable = (aAddon.permissions & AddonManager.PERM_CAN_UPGRADE) > 0;
+ let uninstallable = (aAddon.permissions & AddonManager.PERM_CAN_UNINSTALL) > 0;
+
+ // TODO(matt): Add support for OPTIONS_TYPE_INLINE_BROWSER once bug 1302504 lands.
+ let optionsURL;
+ switch (aAddon.optionsType) {
+ case AddonManager.OPTIONS_TYPE_INLINE:
+ optionsURL = aAddon.optionsURL || "";
+ break;
+ default:
+ optionsURL = "";
+ }
+
+ let blocked = "";
+ switch(aAddon.blocklistState) {
+ case Ci.nsIBlocklistService.STATE_BLOCKED:
+ blocked = "blocked";
+ break;
+ case Ci.nsIBlocklistService.STATE_SOFTBLOCKED:
+ blocked = "softBlocked";
+ break;
+ case Ci.nsIBlocklistService.STATE_OUTDATED:
+ blocked = "outdated";
+ break;
+ }
+
+ let item = this._createItem(aAddon);
+ item.setAttribute("isDisabled", !aAddon.isActive);
+ item.setAttribute("isUnsigned", aAddon.signedState <= AddonManager.SIGNEDSTATE_MISSING);
+ item.setAttribute("opType", opType);
+ item.setAttribute("updateable", updateable);
+ if (blocked)
+ item.setAttribute("blockedStatus", blocked);
+ item.setAttribute("optionsURL", optionsURL);
+ item.addon = aAddon;
+
+ return item;
+ },
+
+ _getElementForAddon: function(aKey) {
+ let list = document.getElementById("addons-list");
+ let element = list.querySelector("div[addonID=\"" + CSS.escape(aKey) + "\"]");
+ return element;
+ },
+
+ init: function init() {
+ let self = this;
+ AddonManager.getAllAddons(function(aAddons) {
+ // Clear all content before filling the addons
+ let list = document.getElementById("addons-list");
+ list.innerHTML = "";
+
+ aAddons.sort(function(a,b) {
+ return a.name.localeCompare(b.name);
+ });
+ for (let i=0; i<aAddons.length; i++) {
+ // Don't create item for system add-ons.
+ if (aAddons[i].isSystem)
+ continue;
+
+ let item = self._createItemForAddon(aAddons[i]);
+ list.appendChild(item);
+ }
+
+ // Add a "Browse all Firefox Add-ons" item to the bottom of the list.
+ let browseItem = self._createBrowseItem();
+ list.appendChild(browseItem);
+ });
+
+ document.getElementById("uninstall-btn").addEventListener("click", Addons.uninstallCurrent.bind(this), false);
+ document.getElementById("cancel-btn").addEventListener("click", Addons.cancelUninstall.bind(this), false);
+ document.getElementById("disable-btn").addEventListener("click", Addons.disable.bind(this), false);
+ document.getElementById("enable-btn").addEventListener("click", Addons.enable.bind(this), false);
+
+ document.getElementById("unsigned-learn-more").addEventListener("click", function() {
+ openLink(Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons");
+ }, false);
+ },
+
+ _getOpTypeForOperations: function _getOpTypeForOperations(aOperations) {
+ if (aOperations & AddonManager.PENDING_UNINSTALL)
+ return "needs-uninstall";
+ if (aOperations & AddonManager.PENDING_ENABLE)
+ return "needs-enable";
+ if (aOperations & AddonManager.PENDING_DISABLE)
+ return "needs-disable";
+ return "";
+ },
+
+ showDetails: function showDetails(aListItem) {
+ // This function removes and returns the text content of aNode without
+ // removing any child elements. Removing the text nodes ensures any XBL
+ // bindings apply properly.
+ function stripTextNodes(aNode) {
+ var text = "";
+ for (var i = 0; i < aNode.childNodes.length; i++) {
+ if (aNode.childNodes[i].nodeType != document.ELEMENT_NODE) {
+ text += aNode.childNodes[i].textContent;
+ aNode.removeChild(aNode.childNodes[i--]);
+ } else {
+ text += stripTextNodes(aNode.childNodes[i]);
+ }
+ }
+ return text;
+ }
+
+ let detailItem = document.querySelector("#addons-details > .addon-item");
+ detailItem.setAttribute("isDisabled", aListItem.getAttribute("isDisabled"));
+ detailItem.setAttribute("isUnsigned", aListItem.getAttribute("isUnsigned"));
+ detailItem.setAttribute("opType", aListItem.getAttribute("opType"));
+ detailItem.setAttribute("optionsURL", aListItem.getAttribute("optionsURL"));
+ let addon = detailItem.addon = aListItem.addon;
+
+ let favicon = document.querySelector("#addons-details > .addon-item .icon");
+ favicon.setAttribute("src", addon.iconURL || AMO_ICON);
+
+ detailItem.querySelector(".title").textContent = addon.name;
+ detailItem.querySelector(".version").textContent = addon.version;
+ detailItem.querySelector(".description-full").textContent = addon.description;
+ detailItem.querySelector(".status-uninstalled").textContent =
+ gStringBundle.formatStringFromName("addonStatus.uninstalled", [addon.name], 1);
+
+ let enableBtn = document.getElementById("enable-btn");
+ if (addon.appDisabled) {
+ enableBtn.setAttribute("disabled", "true");
+ } else {
+ enableBtn.removeAttribute("disabled");
+ }
+
+ let uninstallBtn = document.getElementById("uninstall-btn");
+ if (addon.scope == AddonManager.SCOPE_APPLICATION) {
+ uninstallBtn.setAttribute("disabled", "true");
+ } else {
+ uninstallBtn.removeAttribute("disabled");
+ }
+
+ let box = document.querySelector("#addons-details > .addon-item .options-box");
+ box.innerHTML = "";
+
+ // Retrieve the extensions preferences
+ try {
+ let optionsURL = aListItem.getAttribute("optionsURL");
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", optionsURL, true);
+ xhr.onload = function(e) {
+ if (xhr.responseXML) {
+ // Only allow <setting> for now
+ let settings = xhr.responseXML.querySelectorAll(":root > setting");
+ if (settings.length > 0) {
+ for (let i = 0; i < settings.length; i++) {
+ var setting = settings[i];
+ var desc = stripTextNodes(setting).trim();
+ if (!setting.hasAttribute("desc")) {
+ setting.setAttribute("desc", desc);
+ }
+ box.appendChild(setting);
+ }
+ // Send an event so add-ons can prepopulate any non-preference based
+ // settings
+ let event = document.createEvent("Events");
+ event.initEvent("AddonOptionsLoad", true, false);
+ window.dispatchEvent(event);
+ } else {
+ // Reset the options URL to hide the options header if there are no
+ // valid settings to show.
+ detailItem.setAttribute("optionsURL", "");
+ }
+
+ // Also send a notification to match the behavior of desktop Firefox
+ let id = aListItem.getAttribute("addonID");
+ Services.obs.notifyObservers(document, AddonManager.OPTIONS_NOTIFICATION_DISPLAYED, id);
+ }
+ }
+ xhr.send(null);
+ } catch (e) { }
+
+ let list = document.querySelector("#addons-list");
+ list.style.display = "none";
+ let details = document.querySelector("#addons-details");
+ details.style.display = "block";
+ document.documentElement.setAttribute("details", "true");
+ },
+
+ setEnabled: function setEnabled(aValue, aAddon) {
+ let detailItem = document.querySelector("#addons-details > .addon-item");
+ let addon = aAddon || detailItem.addon;
+ if (!addon)
+ return;
+
+ let listItem = this._getElementForAddon(addon.id);
+
+ let opType;
+ if (addon.type == "theme") {
+ if (aValue) {
+ // We can have only one theme enabled, so disable the current one if any
+ let list = document.getElementById("addons-list");
+ let item = list.firstElementChild;
+ while (item) {
+ if (item.addon && (item.addon.type == "theme") && (item.addon.isActive)) {
+ item.addon.userDisabled = true;
+ item.setAttribute("isDisabled", true);
+ break;
+ }
+ item = item.nextSibling;
+ }
+ }
+ addon.userDisabled = !aValue;
+ } else if (addon.type == "locale") {
+ addon.userDisabled = !aValue;
+ } else {
+ addon.userDisabled = !aValue;
+ opType = this._getOpTypeForOperations(addon.pendingOperations);
+
+ if ((addon.pendingOperations & AddonManager.PENDING_ENABLE) ||
+ (addon.pendingOperations & AddonManager.PENDING_DISABLE)) {
+ this.showRestart();
+ } else if (listItem && /needs-(enable|disable)/.test(listItem.getAttribute("opType"))) {
+ this.hideRestart();
+ }
+ }
+
+ if (addon == detailItem.addon) {
+ detailItem.setAttribute("isDisabled", !aValue);
+ if (opType)
+ detailItem.setAttribute("opType", opType);
+ else
+ detailItem.removeAttribute("opType");
+ }
+
+ // Sync to the list item
+ if (listItem) {
+ listItem.setAttribute("isDisabled", !aValue);
+ if (opType)
+ listItem.setAttribute("opType", opType);
+ else
+ listItem.removeAttribute("opType");
+ }
+ },
+
+ enable: function enable() {
+ this.setEnabled(true);
+ },
+
+ disable: function disable() {
+ this.setEnabled(false);
+ },
+
+ uninstallCurrent: function uninstallCurrent() {
+ let detailItem = document.querySelector("#addons-details > .addon-item");
+
+ let addon = detailItem.addon;
+ if (!addon)
+ return;
+
+ this.uninstall(addon);
+ },
+
+ uninstall: function uninstall(aAddon) {
+ if (!aAddon) {
+ return;
+ }
+
+ let listItem = this._getElementForAddon(aAddon.id);
+ aAddon.uninstall();
+
+ if (aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL) {
+ this.showRestart();
+
+ // A disabled addon doesn't need a restart so it has no pending ops and
+ // can't be cancelled
+ let opType = this._getOpTypeForOperations(aAddon.pendingOperations);
+ if (!aAddon.isActive && opType == "")
+ opType = "needs-uninstall";
+
+ detailItem.setAttribute("opType", opType);
+ listItem.setAttribute("opType", opType);
+ }
+ },
+
+ cancelUninstall: function ev_cancelUninstall() {
+ let detailItem = document.querySelector("#addons-details > .addon-item");
+ let addon = detailItem.addon;
+ if (!addon)
+ return;
+
+ addon.cancelUninstall();
+ this.hideRestart();
+
+ let opType = this._getOpTypeForOperations(addon.pendingOperations);
+ detailItem.setAttribute("opType", opType);
+
+ let listItem = this._getElementForAddon(addon.id);
+ listItem.setAttribute("opType", opType);
+ },
+
+ showRestart: function showRestart() {
+ this._restartCount++;
+ gChromeWin.XPInstallObserver.showRestartPrompt();
+ },
+
+ hideRestart: function hideRestart() {
+ this._restartCount--;
+ if (this._restartCount == 0)
+ gChromeWin.XPInstallObserver.hideRestartPrompt();
+ },
+
+ onEnabled: function(aAddon) {
+ let listItem = this._getElementForAddon(aAddon.id);
+ if (!listItem)
+ return;
+
+ // Reload the details to pick up any options now that it's enabled.
+ listItem.setAttribute("optionsURL", aAddon.optionsURL || "");
+ let detailItem = document.querySelector("#addons-details > .addon-item");
+ if (aAddon == detailItem.addon)
+ this.showDetails(listItem);
+ },
+
+ 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;
+
+ let list = document.getElementById("addons-list");
+ let element = this._getElementForAddon(aAddon.id);
+ if (!element) {
+ element = this._createItemForAddon(aAddon);
+ list.insertBefore(element, list.firstElementChild);
+ }
+
+ if (needsRestart)
+ element.setAttribute("opType", "needs-restart");
+ },
+
+ onInstalled: function(aAddon) {
+ let list = document.getElementById("addons-list");
+ let element = this._getElementForAddon(aAddon.id);
+ if (!element) {
+ element = this._createItemForAddon(aAddon);
+
+ // Themes aren't considered active on install, so set existing as disabled, and new one enabled.
+ if (aAddon.type == "theme") {
+ let item = list.firstElementChild;
+ while (item) {
+ if (item.addon && (item.addon.type == "theme")) {
+ item.setAttribute("isDisabled", true);
+ }
+ item = item.nextSibling;
+ }
+ element.setAttribute("isDisabled", false);
+ }
+
+ list.insertBefore(element, list.firstElementChild);
+ }
+ },
+
+ onUninstalled: function(aAddon) {
+ let list = document.getElementById("addons-list");
+ let element = this._getElementForAddon(aAddon.id);
+ list.removeChild(element);
+
+ // Go back if we're in the detail view of the add-on that was uninstalled.
+ let detailItem = document.querySelector("#addons-details > .addon-item");
+ if (detailItem.addon.id == aAddon.id) {
+ history.back();
+ }
+ },
+
+ onInstallFailed: function(aInstall) {
+ },
+
+ onDownloadProgress: function xpidm_onDownloadProgress(aInstall) {
+ },
+
+ onDownloadFailed: function(aInstall) {
+ },
+
+ onDownloadCancelled: function(aInstall) {
+ }
+}
+
+window.addEventListener("load", init, false);
+window.addEventListener("unload", uninit, false);
diff --git a/mobile/android/chrome/content/aboutAddons.xhtml b/mobile/android/chrome/content/aboutAddons.xhtml
new file mode 100644
index 000000000..42d9cfa9c
--- /dev/null
+++ b/mobile/android/chrome/content/aboutAddons.xhtml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" >
+%globalDTD;
+<!ENTITY % aboutDTD SYSTEM "chrome://browser/locale/aboutAddons.dtd" >
+%aboutDTD;
+]>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>&aboutAddons.title2;</title>
+ <meta name="viewport" content="width=device-width; user-scalable=0" />
+ <link rel="icon" type="image/png" sizes="64x64" href="chrome://branding/content/favicon64.png" />
+ <link rel="stylesheet" href="chrome://browser/skin/aboutBase.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/skin/aboutAddons.css" type="text/css"/>
+</head>
+
+<body dir="&locale.dir;">
+ <menu type="context" id="addonmenu">
+ <menuitem id="contextmenu-enable" label="&addonAction.enable;"></menuitem>
+ <menuitem id="contextmenu-disable" label="&addonAction.disable;" ></menuitem>
+ <menuitem id="contextmenu-uninstall" label="&addonAction.uninstall;" ></menuitem>
+ </menu>
+
+ <div id="addons-header" class="header">
+ <div>&aboutAddons.header2;</div>
+ </div>
+ <div id="addons-list" class="list">
+ </div>
+
+ <div id="addons-details" class="list">
+ <div class="addon-item list-item">
+ <img class="icon"/>
+ <div class="inner">
+ <div class="details">
+ <div class="title"></div><div class="version"></div>
+ </div>
+ <div class="description-full"></div>
+ <div class="options-header">&aboutAddons.options;</div>
+ <div class="options-box"></div>
+ </div>
+ <div class="warn-unsigned">&addonUnsigned.message; <a id="unsigned-learn-more">&addonUnsigned.learnMore;</a></div>
+ <div class="status status-uninstalled show-on-uninstall"></div>
+ <div class="buttons">
+ <button id="enable-btn" class="show-on-disable hide-on-enable hide-on-uninstall" >&addonAction.enable;</button>
+ <button id="disable-btn" class="show-on-enable hide-on-disable hide-on-uninstall" >&addonAction.disable;</button>
+ <button id="uninstall-btn" class="hide-on-uninstall" >&addonAction.uninstall;</button>
+ <button id="cancel-btn" class="show-on-uninstall" >&addonAction.undo;</button>
+ </div>
+ </div>
+ </div>
+
+ <script type="application/javascript;version=1.8" src="chrome://browser/content/aboutAddons.js"></script>
+</body>
+</html>
diff --git a/mobile/android/chrome/content/aboutCertError.xhtml b/mobile/android/chrome/content/aboutCertError.xhtml
new file mode 100644
index 000000000..c5922e2fe
--- /dev/null
+++ b/mobile/android/chrome/content/aboutCertError.xhtml
@@ -0,0 +1,264 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD
+ PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % globalDTD
+ SYSTEM "chrome://global/locale/global.dtd">
+ %globalDTD;
+ <!ENTITY % certerrorDTD
+ SYSTEM "chrome://browser/locale/aboutCertError.dtd">
+ %certerrorDTD;
+]>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>&certerror.pagetitle;</title>
+ <meta name="viewport" content="width=device-width; user-scalable=false" />
+ <link rel="stylesheet" href="chrome://global/skin/netError.css" type="text/css" media="all" />
+ <!-- This page currently uses the same favicon as neterror.xhtml.
+ If the location of the favicon is changed for both pages, the
+ FAVICON_ERRORPAGE_URL symbol in toolkit/components/places/src/nsFaviconService.h
+ should be updated. If this page starts using a different favicon
+ than neterrorm nsFaviconService->SetAndLoadFaviconForPage
+ should be updated to ignore this one as well. -->
+ <link rel="icon" type="image/png" id="favicon" sizes="64x64" href="chrome://browser/skin/images/certerror-warning.png"/>
+
+ <script type="application/javascript"><![CDATA[
+ // Error url MUST be formatted like this:
+ // about:certerror?e=error&u=url&d=desc
+
+ // Note that this file uses document.documentURI to get
+ // the URL (with the format from above). This is because
+ // document.location.href gets the current URI off the docshell,
+ // which is the URL displayed in the location bar, i.e.
+ // the URI that the user attempted to load.
+
+ function getCSSClass()
+ {
+ var url = document.documentURI;
+ var matches = url.match(/s\=([^&]+)\&/);
+ // s is optional, if no match just return nothing
+ if (!matches || matches.length < 2)
+ return "";
+
+ // parenthetical match is the second entry
+ return decodeURIComponent(matches[1]);
+ }
+
+ function getDescription()
+ {
+ var url = document.documentURI;
+ var desc = url.search(/d\=/);
+
+ // desc == -1 if not found; if so, return an empty string
+ // instead of what would turn out to be portions of the URI
+ if (desc == -1)
+ return "";
+
+ return decodeURIComponent(url.slice(desc + 2));
+ }
+
+ function initPage()
+ {
+ // Replace the "#1" string in the intro with the hostname. Trickier
+ // than it might seem since we want to preserve the <b> tags, but
+ // not allow for any injection by just using innerHTML. Instead,
+ // just find the right target text node.
+ var intro = document.getElementById('introContentP1');
+ function replaceWithHost(node) {
+ if (node.textContent == "#1")
+ node.textContent = location.host;
+ else
+ for(var i = 0; i < node.childNodes.length; i++)
+ replaceWithHost(node.childNodes[i]);
+ };
+ replaceWithHost(intro);
+
+ if (getCSSClass() == "expertBadCert") {
+ toggle('technicalContent');
+ toggle('expertContent');
+ }
+
+ // Disallow overrides if this is a Strict-Transport-Security
+ // host and the cert is bad (STS Spec section 7.3) or if the
+ // certerror is in a frame (bug 633691).
+ if (getCSSClass() == "badStsCert" || window != top)
+ document.getElementById("expertContent").setAttribute("hidden", "true");
+
+ var tech = document.getElementById("technicalContentText");
+ if (tech)
+ tech.textContent = getDescription();
+
+ addDomainErrorLinks();
+ }
+
+ /* Try to preserve the links contained in the error description, like
+ the error code.
+
+ Also, in the case of SSL error pages about domain mismatch, see if
+ we can hyperlink the user to the correct site. We don't want
+ to do this generically since it allows MitM attacks to redirect
+ users to a site under attacker control, but in certain cases
+ it is safe (and helpful!) to do so. Bug 402210
+ */
+ function addDomainErrorLinks() {
+ // Rather than textContent, we need to treat description as HTML
+ var sd = document.getElementById("technicalContentText");
+ if (sd) {
+ var desc = getDescription();
+
+ // sanitize description text - see bug 441169
+
+ // First, find the index of the <a> tags we care about, being
+ // careful not to use an over-greedy regex.
+ var codeRe = /<a id="errorCode" title="([^"]+)">/;
+ var codeResult = codeRe.exec(desc);
+ var domainRe = /<a id="cert_domain_link" title="([^"]+)">/;
+ var domainResult = domainRe.exec(desc);
+
+ // The order of these links in the description is fixed in
+ // TransportSecurityInfo.cpp:formatOverridableCertErrorMessage.
+ var firstResult = domainResult;
+ if (!domainResult)
+ firstResult = codeResult;
+ if (!firstResult)
+ return;
+
+ // Remove sd's existing children
+ sd.textContent = "";
+
+ // Everything up to the first link should be text content.
+ sd.appendChild(document.createTextNode(desc.slice(0, firstResult.index)));
+
+ // Now create the actual links.
+ if (domainResult) {
+ createLink(sd, "cert_domain_link", domainResult[1])
+ // Append text for anything between the two links.
+ sd.appendChild(document.createTextNode(desc.slice(desc.indexOf("</a>") + "</a>".length, codeResult.index)));
+ }
+ createLink(sd, "errorCode", codeResult[1])
+
+ // Finally, append text for anything after the last closing </a>.
+ sd.appendChild(document.createTextNode(desc.slice(desc.lastIndexOf("</a>") + "</a>".length)));
+ }
+
+ // Then initialize the cert domain link.
+ var link = document.getElementById('cert_domain_link');
+ if (!link)
+ return;
+
+ var okHost = link.getAttribute("title");
+ var thisHost = document.location.hostname;
+ var proto = document.location.protocol;
+
+ // If okHost is a wildcard domain ("*.example.com") let's
+ // use "www" instead. "*.example.com" isn't going to
+ // get anyone anywhere useful. bug 432491
+ okHost = okHost.replace(/^\*\./, "www.");
+
+ /* case #1:
+ * example.com uses an invalid security certificate.
+ *
+ * The certificate is only valid for www.example.com
+ *
+ * Make sure to include the "." ahead of thisHost so that
+ * a MitM attack on paypal.com doesn't hyperlink to "notpaypal.com"
+ *
+ * We'd normally just use a RegExp here except that we lack a
+ * library function to escape them properly (bug 248062), and
+ * domain names are famous for having '.' characters in them,
+ * which would allow spurious and possibly hostile matches.
+ */
+ if (okHost.endsWith("." + thisHost))
+ link.href = proto + okHost;
+
+ /* case #2:
+ * browser.garage.maemo.org uses an invalid security certificate.
+ *
+ * The certificate is only valid for garage.maemo.org
+ */
+ if (thisHost.endsWith("." + okHost))
+ link.href = proto + okHost;
+
+ // If we set a link, meaning there's something helpful for
+ // the user here, expand the section by default
+ if (link.href && getCSSClass() != "expertBadCert")
+ toggle("technicalContent");
+ }
+
+ function createLink(el, id, text) {
+ var anchorEl = document.createElement("a");
+ anchorEl.setAttribute("id", id);
+ anchorEl.setAttribute("title", text);
+ anchorEl.appendChild(document.createTextNode(text));
+ el.appendChild(anchorEl);
+ }
+
+ function toggle(id) {
+ var el = document.getElementById(id);
+ if (el.hasAttribute("collapsed"))
+ el.removeAttribute("collapsed");
+ else
+ el.setAttribute("collapsed", true);
+ }
+ ]]></script>
+ </head>
+
+ <body id="errorPage" class="certerror" dir="&locale.dir;">
+
+ <!-- PAGE CONTAINER (for styling purposes only) -->
+ <div id="errorPageContainer">
+
+ <!-- Error Title -->
+ <div id="errorTitle">
+ <h1 class="errorTitleText">&certerror.longpagetitle;</h1>
+ </div>
+
+ <!-- LONG CONTENT (the section most likely to require scrolling) -->
+ <div id="errorLongContent">
+ <div id="introContent">
+ <p id="introContentP1">&certerror.introPara1;</p>
+ </div>
+
+ <div id="whatShouldIDoContent">
+ <h2>&certerror.whatShouldIDo.heading;</h2>
+ <div id="whatShouldIDoContentText">
+ <p>&certerror.whatShouldIDo.content;</p>
+ <button id="getMeOutOfHereButton">&certerror.getMeOutOfHere.label;</button>
+ </div>
+ </div>
+
+ <!-- The following sections can be unhidden by default by setting the
+ "browser.xul.error_pages.expert_bad_cert" pref to true -->
+ <div id="technicalContent" collapsed="true">
+ <h2 class="expander" onclick="toggle('technicalContent');" id="technicalContentHeading">&certerror.technical.heading;</h2>
+ <p id="technicalContentText"/>
+ </div>
+
+ <div id="expertContent" collapsed="true">
+ <h2 class="expander" onclick="toggle('expertContent');" id="expertContentHeading">&certerror.expert.heading;</h2>
+ <div>
+ <p>&certerror.expert.content;</p>
+ <p>&certerror.expert.contentPara2;</p>
+ <button id="temporaryExceptionButton">&certerror.addTemporaryException.label;</button>
+ <button id="permanentExceptionButton">&certerror.addPermanentException.label;</button>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!--
+ - Note: It is important to run the script this way, instead of using
+ - an onload handler. This is because error pages are loaded as
+ - LOAD_BACKGROUND, which means that onload handlers will not be executed.
+ -->
+ <script type="application/javascript">initPage();</script>
+
+ </body>
+</html>
diff --git a/mobile/android/chrome/content/aboutDownloads.js b/mobile/android/chrome/content/aboutDownloads.js
new file mode 100644
index 000000000..add0a48e6
--- /dev/null
+++ b/mobile/android/chrome/content/aboutDownloads.js
@@ -0,0 +1,373 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads", "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils", "resource://gre/modules/DownloadUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
+
+var gStrings = Services.strings.createBundle("chrome://browser/locale/aboutDownloads.properties");
+XPCOMUtils.defineLazyGetter(this, "strings",
+ () => Services.strings.createBundle("chrome://browser/locale/aboutDownloads.properties"));
+
+function deleteDownload(download) {
+ download.finalize(true).then(null, Cu.reportError);
+ OS.File.remove(download.target.path).then(null, ex => {
+ if (!(ex instanceof OS.File.Error && ex.becauseNoSuchFile)) {
+ Cu.reportError(ex);
+ }
+ });
+}
+
+var contextMenu = {
+ _items: [],
+ _targetDownload: null,
+
+ init: function () {
+ let element = document.getElementById("downloadmenu");
+ element.addEventListener("click",
+ event => event.download = this._targetDownload,
+ true);
+ this._items = [
+ new ContextMenuItem("open",
+ download => download.succeeded,
+ download => download.launch().then(null, Cu.reportError)),
+ new ContextMenuItem("retry",
+ download => download.error ||
+ (download.canceled && !download.hasPartialData),
+ download => download.start().then(null, Cu.reportError)),
+ new ContextMenuItem("remove",
+ download => download.stopped,
+ download => {
+ Downloads.getList(Downloads.ALL)
+ .then(list => list.remove(download))
+ .then(null, Cu.reportError);
+ deleteDownload(download);
+ }),
+ new ContextMenuItem("pause",
+ download => !download.stopped && download.hasPartialData,
+ download => download.cancel().then(null, Cu.reportError)),
+ new ContextMenuItem("resume",
+ download => download.canceled && download.hasPartialData,
+ download => download.start().then(null, Cu.reportError)),
+ new ContextMenuItem("cancel",
+ download => !download.stopped ||
+ (download.canceled && download.hasPartialData),
+ download => {
+ download.cancel().then(null, Cu.reportError);
+ download.removePartialData().then(null, Cu.reportError);
+ }),
+ // following menu item is a global action
+ new ContextMenuItem("removeall",
+ () => downloadLists.finished.length > 0,
+ () => downloadLists.removeFinished())
+ ];
+ },
+
+ addContextMenuEventListener: function (element) {
+ element.addEventListener("contextmenu", this.onContextMenu.bind(this));
+ },
+
+ onContextMenu: function (event) {
+ let target = event.target;
+ while (target && !target.download) {
+ target = target.parentNode;
+ }
+ if (!target) {
+ Cu.reportError("No download found for context menu target");
+ event.preventDefault();
+ return;
+ }
+
+ // capture the target download for menu items to use in a click event
+ this._targetDownload = target.download;
+ for (let item of this._items) {
+ item.updateVisibility(target.download);
+ }
+ }
+};
+
+function ContextMenuItem(name, isVisible, action) {
+ this.element = document.getElementById("contextmenu-" + name);
+ this.isVisible = isVisible;
+
+ this.element.addEventListener("click", event => action(event.download));
+}
+
+ContextMenuItem.prototype = {
+ updateVisibility: function (download) {
+ this.element.hidden = !this.isVisible(download);
+ }
+};
+
+function DownloadListView(type, listElementId) {
+ this.listElement = document.getElementById(listElementId);
+ contextMenu.addContextMenuEventListener(this.listElement);
+
+ this.items = new Map();
+
+ Downloads.getList(type)
+ .then(list => list.addView(this))
+ .then(null, Cu.reportError);
+
+ window.addEventListener("unload", event => {
+ Downloads.getList(type)
+ .then(list => list.removeView(this))
+ .then(null, Cu.reportError);
+ });
+}
+
+DownloadListView.prototype = {
+ get finished() {
+ let finished = [];
+ for (let download of this.items.keys()) {
+ if (download.stopped && (!download.hasPartialData || download.error)) {
+ finished.push(download);
+ }
+ }
+
+ return finished;
+ },
+
+ insertOrMoveItem: function (item) {
+ var compare = (a, b) => {
+ // active downloads always before stopped downloads
+ if (a.stopped != b.stopped) {
+ return b.stopped ? -1 : 1
+ }
+ // most recent downloads first
+ return b.startTime - a.startTime;
+ };
+
+ let insertLocation = this.listElement.firstChild;
+ while (insertLocation && compare(item.download, insertLocation.download) > 0) {
+ insertLocation = insertLocation.nextElementSibling;
+ }
+ this.listElement.insertBefore(item.element, insertLocation);
+ },
+
+ onDownloadAdded: function (download) {
+ let item = new DownloadItem(download);
+ this.items.set(download, item);
+ this.insertOrMoveItem(item);
+ },
+
+ onDownloadChanged: function (download) {
+ let item = this.items.get(download);
+ if (!item) {
+ Cu.reportError("No DownloadItem found for download");
+ return;
+ }
+
+ if (item.stateChanged) {
+ this.insertOrMoveItem(item);
+ }
+
+ item.onDownloadChanged();
+ },
+
+ onDownloadRemoved: function (download) {
+ let item = this.items.get(download);
+ if (!item) {
+ Cu.reportError("No DownloadItem found for download");
+ return;
+ }
+
+ this.items.delete(download);
+ this.listElement.removeChild(item.element);
+
+ Messaging.sendRequest({
+ type: "Download:Remove",
+ path: download.target.path
+ });
+ }
+};
+
+var downloadLists = {
+ init: function () {
+ this.publicDownloads = new DownloadListView(Downloads.PUBLIC, "public-downloads-list");
+ this.privateDownloads = new DownloadListView(Downloads.PRIVATE, "private-downloads-list");
+ },
+
+ get finished() {
+ return this.publicDownloads.finished.concat(this.privateDownloads.finished);
+ },
+
+ removeFinished: function () {
+ let finished = this.finished;
+ if (finished.length == 0) {
+ return;
+ }
+
+ let title = strings.GetStringFromName("downloadAction.deleteAll");
+ let messageForm = strings.GetStringFromName("downloadMessage.deleteAll");
+ let message = PluralForm.get(finished.length, messageForm).replace("#1", finished.length);
+
+ if (Services.prompt.confirm(null, title, message)) {
+ Downloads.getList(Downloads.ALL)
+ .then(list => {
+ for (let download of finished) {
+ list.remove(download).then(null, Cu.reportError);
+ deleteDownload(download);
+ }
+ }, Cu.reportError);
+ }
+ }
+};
+
+function DownloadItem(download) {
+ this._download = download;
+ this._updateFromDownload();
+
+ this._domain = DownloadUtils.getURIHost(download.source.url)[0];
+ this._fileName = this._htmlEscape(OS.Path.basename(download.target.path));
+ this._iconUrl = "moz-icon://" + this._fileName + "?size=64";
+ this._startDate = this._htmlEscape(DownloadUtils.getReadableDates(download.startTime)[0]);
+
+ this._element = this.createElement();
+}
+
+const kDownloadStatePropertyNames = [
+ "stopped",
+ "succeeded",
+ "canceled",
+ "error",
+ "startTime"
+];
+
+DownloadItem.prototype = {
+ _htmlEscape : function (s) {
+ s = s.replace(/&/g, "&amp;");
+ s = s.replace(/>/g, "&gt;");
+ s = s.replace(/</g, "&lt;");
+ s = s.replace(/"/g, "&quot;");
+ s = s.replace(/'/g, "&apos;");
+ return s;
+ },
+
+ _updateFromDownload: function () {
+ this._state = {};
+ kDownloadStatePropertyNames.forEach(
+ name => this._state[name] = this._download[name],
+ this);
+ },
+
+ get stateChanged() {
+ return kDownloadStatePropertyNames.some(
+ name => this._state[name] != this._download[name],
+ this);
+ },
+
+ get download() {
+ return this._download;
+ },
+ get element() {
+ return this._element;
+ },
+
+ createElement: function() {
+ let template = document.getElementById("download-item");
+ // TODO: use this once <template> is working
+ // let element = document.importNode(template.content, true);
+
+ // simulate a <template> node...
+ let element = template.cloneNode(true);
+ element.removeAttribute("id");
+ element.removeAttribute("style");
+
+ // launch the download if clicked
+ element.addEventListener("click", this.onClick.bind(this));
+
+ // set download as an expando property for the context menu
+ element.download = this.download;
+
+ // fill in template placeholders
+ this.updateElement(element);
+
+ return element;
+ },
+
+ updateElement: function (element) {
+ element.querySelector(".date").textContent = this.startDate;
+ element.querySelector(".domain").textContent = this.domain;
+ element.querySelector(".icon").src = this.iconUrl;
+ element.querySelector(".size").textContent = this.size;
+ element.querySelector(".state").textContent = this.stateDescription;
+ element.querySelector(".title").setAttribute("value", this.fileName);
+ },
+
+ onClick: function (event) {
+ if (this.download.succeeded) {
+ this.download.launch().then(null, Cu.reportError);
+ }
+ },
+
+ onDownloadChanged: function () {
+ this._updateFromDownload();
+ this.updateElement(this.element);
+ },
+
+ // template properties below
+ get domain() {
+ return this._domain;
+ },
+ get fileName() {
+ return this._fileName;
+ },
+ get id() {
+ return this._id;
+ },
+ get iconUrl() {
+ return this._iconUrl;
+ },
+
+ get size() {
+ if (this.download.succeeded && this.download.target.exists) {
+ return DownloadUtils.convertByteUnits(this.download.target.size).join("");
+ } else if (this.download.hasProgress) {
+ return DownloadUtils.convertByteUnits(this.download.totalBytes).join("");
+ }
+ return strings.GetStringFromName("downloadState.unknownSize");
+ },
+
+ get startDate() {
+ return this._startDate;
+ },
+
+ get stateDescription() {
+ let name;
+ if (this.download.error) {
+ name = "downloadState.failed";
+ } else if (this.download.canceled) {
+ if (this.download.hasPartialData) {
+ name = "downloadState.paused";
+ } else {
+ name = "downloadState.canceled";
+ }
+ } else if (!this.download.stopped) {
+ if (this.download.currentBytes > 0) {
+ name = "downloadState.downloading";
+ } else {
+ name = "downloadState.starting";
+ }
+ }
+
+ if (name) {
+ return strings.GetStringFromName(name);
+ }
+ return "";
+ }
+};
+
+window.addEventListener("DOMContentLoaded", event => {
+ contextMenu.init();
+ downloadLists.init()
+}); \ No newline at end of file
diff --git a/mobile/android/chrome/content/aboutDownloads.xhtml b/mobile/android/chrome/content/aboutDownloads.xhtml
new file mode 100644
index 000000000..6b9025694
--- /dev/null
+++ b/mobile/android/chrome/content/aboutDownloads.xhtml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" >
+%globalDTD;
+<!ENTITY % downloadsDTD SYSTEM "chrome://browser/locale/aboutDownloads.dtd" >
+%downloadsDTD;
+]>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ - You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<head>
+ <title>&aboutDownloads.title;</title>
+ <meta name="viewport" content="width=device-width; user-scalable=0" />
+ <link rel="icon" type="image/png" sizes="64x64" href="chrome://branding/content/favicon64.png" />
+ <link rel="stylesheet" href="chrome://browser/skin/aboutBase.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/skin/aboutDownloads.css" type="text/css"/>
+</head>
+
+<body dir="&locale.dir;">
+ <menu type="context" id="downloadmenu">
+ <menuitem id="contextmenu-open" label="&aboutDownloads.open;"></menuitem>
+ <menuitem id="contextmenu-retry" label="&aboutDownloads.retry;"></menuitem>
+ <menuitem id="contextmenu-remove" label="&aboutDownloads.remove;"></menuitem>
+ <menuitem id="contextmenu-pause" label="&aboutDownloads.pause;"></menuitem>
+ <menuitem id="contextmenu-resume" label="&aboutDownloads.resume;"></menuitem>
+ <menuitem id="contextmenu-cancel" label="&aboutDownloads.cancel;"></menuitem>
+ <menuitem id="contextmenu-removeall" label="&aboutDownloads.removeAll;"></menuitem>
+ </menu>
+
+ <!--template id="download-item"-->
+ <li id="download-item" class="list-item" role="button" contextmenu="downloadmenu" style="display: none">
+ <img class="icon" src=""/>
+ <div class="details">
+ <div class="row">
+ <!-- This is a hack so that we can crop this label in its center -->
+ <xul:label class="title" crop="center" value=""/>
+ <div class="date"></div>
+ </div>
+ <div class="size"></div>
+ <div class="domain"></div>
+ <div class="state"></div>
+ </div>
+ </li>
+ <!--/template-->
+
+ <div class="header">
+ <div>&aboutDownloads.header;</div>
+ </div>
+ <ul id="private-downloads-list" class="list"></ul>
+ <ul id="public-downloads-list" class="list"></ul>
+ <span id="no-downloads-indicator">&aboutDownloads.empty;</span>
+ <script type="application/javascript;version=1.8" src="chrome://browser/content/aboutDownloads.js"/>
+</body>
+</html>
diff --git a/mobile/android/chrome/content/aboutHealthReport.js b/mobile/android/chrome/content/aboutHealthReport.js
new file mode 100644
index 000000000..070eb821d
--- /dev/null
+++ b/mobile/android/chrome/content/aboutHealthReport.js
@@ -0,0 +1,192 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm");
+Cu.import("resource://gre/modules/SharedPreferences.jsm");
+
+// Name of Android SharedPreference controlling whether to upload
+// health reports.
+const PREF_UPLOAD_ENABLED = "android.not_a_preference.healthreport.uploadEnabled";
+
+// Name of Gecko Pref specifying report content location.
+const PREF_REPORTURL = "datareporting.healthreport.about.reportUrl";
+
+// Monotonically increasing wrapper API version number.
+const WRAPPER_VERSION = 1;
+
+const EVENT_HEALTH_REQUEST = "HealthReport:Request";
+const EVENT_HEALTH_RESPONSE = "HealthReport:Response";
+
+// about:healthreport prefs are stored in Firefox's default Android
+// SharedPreferences.
+var sharedPrefs = SharedPreferences.forApp();
+
+var healthReportWrapper = {
+ init: function () {
+ let iframe = document.getElementById("remote-report");
+ iframe.addEventListener("load", healthReportWrapper.initRemotePage, false);
+ let report = this._getReportURI();
+ iframe.src = report.spec;
+ console.log("AboutHealthReport: loading content from " + report.spec);
+
+ sharedPrefs.addObserver(PREF_UPLOAD_ENABLED, this, false);
+ Services.obs.addObserver(this, EVENT_HEALTH_RESPONSE, false);
+ },
+
+ observe: function (subject, topic, data) {
+ if (topic == PREF_UPLOAD_ENABLED) {
+ this.updatePrefState();
+ } else if (topic == EVENT_HEALTH_RESPONSE) {
+ this.updatePayload(data);
+ }
+ },
+
+ uninit: function () {
+ sharedPrefs.removeObserver(PREF_UPLOAD_ENABLED, this);
+ Services.obs.removeObserver(this, EVENT_HEALTH_RESPONSE);
+ },
+
+ _getReportURI: function () {
+ let url = Services.urlFormatter.formatURLPref(PREF_REPORTURL);
+ // This handles URLs that already have query parameters.
+ let uri = Services.io.newURI(url, null, null).QueryInterface(Ci.nsIURL);
+ uri.query += ((uri.query != "") ? "&v=" : "v=") + WRAPPER_VERSION;
+ return uri;
+ },
+
+ onOptIn: function () {
+ console.log("AboutHealthReport: page sent opt-in command.");
+ sharedPrefs.setBoolPref(PREF_UPLOAD_ENABLED, true);
+ this.updatePrefState();
+ },
+
+ onOptOut: function () {
+ console.log("AboutHealthReport: page sent opt-out command.");
+ sharedPrefs.setBoolPref(PREF_UPLOAD_ENABLED, false);
+ this.updatePrefState();
+ },
+
+ updatePrefState: function () {
+ console.log("AboutHealthReport: sending pref state to page.");
+ try {
+ let prefs = {
+ enabled: sharedPrefs.getBoolPref(PREF_UPLOAD_ENABLED),
+ };
+ this.injectData("prefs", prefs);
+ } catch (e) {
+ this.reportFailure(this.ERROR_PREFS_FAILED);
+ }
+ },
+
+ refreshPayload: function () {
+ console.log("AboutHealthReport: page requested fresh payload.");
+ Messaging.sendRequest({
+ type: EVENT_HEALTH_REQUEST,
+ });
+ },
+
+ updatePayload: function (data) {
+ healthReportWrapper.injectData("payload", data);
+ // Data is supposed to be a string, so the length should be
+ // defined. Just in case, we do this after injecting the data.
+ console.log("AboutHealthReport: sending payload to page " +
+ "(" + typeof(data) + " of length " + data.length + ").");
+ },
+
+ injectData: function (type, content) {
+ let report = this._getReportURI();
+
+ // file: URIs can't be used for targetOrigin, so we use "*" for
+ // this special case. In all other cases, pass in the URL to the
+ // report so we properly restrict the message dispatch.
+ let reportUrl = (report.scheme == "file") ? "*" : report.spec;
+
+ let data = {
+ type: type,
+ content: content,
+ };
+
+ let iframe = document.getElementById("remote-report");
+ iframe.contentWindow.postMessage(data, reportUrl);
+ },
+
+ showSettings: function () {
+ console.log("AboutHealthReport: showing settings.");
+ Messaging.sendRequest({
+ type: "Settings:Show",
+ resource: "preferences_vendor",
+ });
+ },
+
+ launchUpdater: function () {
+ console.log("AboutHealthReport: launching updater.");
+ Messaging.sendRequest({
+ type: "Updater:Launch",
+ });
+ },
+
+ handleRemoteCommand: function (evt) {
+ switch (evt.detail.command) {
+ case "DisableDataSubmission":
+ this.onOptOut();
+ break;
+ case "EnableDataSubmission":
+ this.onOptIn();
+ break;
+ case "RequestCurrentPrefs":
+ this.updatePrefState();
+ break;
+ case "RequestCurrentPayload":
+ this.refreshPayload();
+ break;
+ case "ShowSettings":
+ this.showSettings();
+ break;
+ case "LaunchUpdater":
+ this.launchUpdater();
+ break;
+ default:
+ Cu.reportError("Unexpected remote command received: " + evt.detail.command +
+ ". Ignoring command.");
+ break;
+ }
+ },
+
+ initRemotePage: function () {
+ let iframe = document.getElementById("remote-report").contentDocument;
+ iframe.addEventListener("RemoteHealthReportCommand",
+ function onCommand(e) {healthReportWrapper.handleRemoteCommand(e);},
+ false);
+ healthReportWrapper.injectData("begin", null);
+ },
+
+ // error handling
+ ERROR_INIT_FAILED: 1,
+ ERROR_PAYLOAD_FAILED: 2,
+ ERROR_PREFS_FAILED: 3,
+
+ reportFailure: function (error) {
+ let details = {
+ errorType: error,
+ };
+ healthReportWrapper.injectData("error", details);
+ },
+
+ handleInitFailure: function () {
+ healthReportWrapper.reportFailure(healthReportWrapper.ERROR_INIT_FAILED);
+ },
+
+ handlePayloadFailure: function () {
+ healthReportWrapper.reportFailure(healthReportWrapper.ERROR_PAYLOAD_FAILED);
+ },
+};
+
+window.addEventListener("load", healthReportWrapper.init.bind(healthReportWrapper), false);
+window.addEventListener("unload", healthReportWrapper.uninit.bind(healthReportWrapper), false);
diff --git a/mobile/android/chrome/content/aboutHealthReport.xhtml b/mobile/android/chrome/content/aboutHealthReport.xhtml
new file mode 100644
index 000000000..73dae0380
--- /dev/null
+++ b/mobile/android/chrome/content/aboutHealthReport.xhtml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
+%globalDTD;
+<!ENTITY % aboutHealthReportDTD SYSTEM "chrome://browser/locale/aboutHealthReport.dtd" >
+%aboutHealthReportDTD;
+]>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <title>&abouthealth.pagetitle;</title>
+ <link rel="icon" type="image/png" sizes="64x64"
+ href="chrome://branding/content/favicon64.png" />
+ <link rel="stylesheet"
+ href="chrome://browser/skin/aboutHealthReport.css"
+ type="text/css" />
+ <script type="text/javascript;version=1.8"
+ src="chrome://browser/content/aboutHealthReport.js" />
+ </head>
+ <body>
+ <iframe id="remote-report"/>
+ </body>
+</html>
diff --git a/mobile/android/chrome/content/aboutHome.xhtml b/mobile/android/chrome/content/aboutHome.xhtml
new file mode 100644
index 000000000..692eac2ec
--- /dev/null
+++ b/mobile/android/chrome/content/aboutHome.xhtml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+ %brandDTD;
+ <!ENTITY % abouthomeDTD SYSTEM "chrome://browser/locale/aboutHome.dtd">
+ %abouthomeDTD;
+]>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>&abouthome.title;</title>
+</head>
+<body>
+</body>
+</html>
diff --git a/mobile/android/chrome/content/aboutLogins.js b/mobile/android/chrome/content/aboutLogins.js
new file mode 100644
index 000000000..99e2af841
--- /dev/null
+++ b/mobile/android/chrome/content/aboutLogins.js
@@ -0,0 +1,518 @@
+/* Source Code Form is subject to the terms of the Mozilla Public
+* License, v. 2.0. If a copy of the MPL was not distributed with this file,
+* You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils;
+
+Cu.import("resource://services-common/utils.js"); /*global: CommonUtils */
+Cu.import("resource://gre/modules/Messaging.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/TelemetryStopwatch.jsm");
+
+XPCOMUtils.defineLazyGetter(window, "gChromeWin", () =>
+ window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow)
+ .QueryInterface(Ci.nsIDOMChromeWindow));
+
+XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
+ "resource://gre/modules/Prompt.jsm");
+
+var debug = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.d.bind(null, "AboutLogins");
+
+var gStringBundle = Services.strings.createBundle("chrome://browser/locale/aboutLogins.properties");
+
+function copyStringShowSnackbar(string, notifyString) {
+ try {
+ let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
+ clipboard.copyString(string);
+ Snackbars.show(notifyString, Snackbars.LENGTH_LONG);
+ } catch (e) {
+ debug("Error copying from about:logins");
+ Snackbars.show(gStringBundle.GetStringFromName("loginsDetails.copyFailed"), Snackbars.LENGTH_LONG);
+ }
+}
+
+// Delay filtering while typing in MS
+const FILTER_DELAY = 500;
+
+var Logins = {
+ _logins: [],
+ _filterTimer: null,
+ _selectedLogin: null,
+
+ // Load the logins list, displaying interstitial UI (see
+ // #logins-list-loading-body) while loading. There are careful
+ // jank-avoiding measures taken in this function; be careful when
+ // modifying it!
+ //
+ // Returns a Promise that resolves to the list of logins, ordered by
+ // hostname.
+ _promiseLogins: function() {
+ let contentBody = document.getElementById("content-body");
+ let emptyBody = document.getElementById("empty-body");
+ let filterIcon = document.getElementById("filter-button");
+
+ let showSpinner = () => {
+ this._toggleListBody(true);
+ emptyBody.classList.add("hidden");
+ };
+
+ let getAllLogins = () => {
+ let logins = [];
+ try {
+ logins = Services.logins.getAllLogins();
+ } catch(e) {
+ // It's likely that the Master Password was not entered; give
+ // a hint to the next person.
+ throw new Error("Possible Master Password permissions error: " + e.toString());
+ }
+
+ logins.sort((a, b) => a.hostname.localeCompare(b.hostname));
+
+ return logins;
+ };
+
+ let hideSpinner = (logins) => {
+ this._toggleListBody(false);
+
+ if (!logins.length) {
+ contentBody.classList.add("hidden");
+ filterIcon.classList.add("hidden");
+ emptyBody.classList.remove("hidden");
+ } else {
+ contentBody.classList.remove("hidden");
+ emptyBody.classList.add("hidden");
+ }
+
+ return logins;
+ };
+
+ // Return a promise that is resolved after a paint.
+ let waitForPaint = () => {
+ // We're changing 'display'. We need to wait for the new value to take
+ // effect; otherwise, we'll block and never paint a change. Since
+ // requestAnimationFrame callback is generally triggered *before* any
+ // style flush and layout, we wait for two animation frames. This
+ // approach was cribbed from
+ // https://dxr.mozilla.org/mozilla-central/rev/5abe3c4deab94270440422c850bbeaf512b1f38d/browser/base/content/browser-fullScreen.js?offset=0#469.
+ return new Promise(function(resolve, reject) {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ resolve();
+ });
+ });
+ });
+ };
+
+ // getAllLogins janks the main-thread. We need to paint before that jank;
+ // by throwing the janky load onto the next tick, we paint the spinner; the
+ // spinner is CSS animated off-main-thread.
+ return Promise.resolve()
+ .then(showSpinner)
+ .then(waitForPaint)
+ .then(getAllLogins)
+ .then(hideSpinner);
+ },
+
+ // Reload the logins list, displaying interstitial UI while loading.
+ // Update the stored and displayed list upon completion.
+ _reloadList: function() {
+ this._promiseLogins()
+ .then((logins) => {
+ this._logins = logins;
+ this._loadList(logins);
+ })
+ .catch((e) => {
+ // There's no way to recover from errors, sadly. Log and make
+ // it obvious that something is up.
+ this._logins = [];
+ debug("Failed to _reloadList!");
+ Cu.reportError(e);
+ });
+ },
+
+ _toggleListBody: function(isLoading) {
+ let contentBody = document.getElementById("content-body");
+ let loadingBody = document.getElementById("logins-list-loading-body");
+
+ if (isLoading) {
+ contentBody.classList.add("hidden");
+ loadingBody.classList.remove("hidden");
+ } else {
+ loadingBody.classList.add("hidden");
+ contentBody.classList.remove("hidden");
+ }
+ },
+
+ init: function () {
+ window.addEventListener("popstate", this , false);
+
+ Services.obs.addObserver(this, "passwordmgr-storage-changed", false);
+ document.getElementById("update-btn").addEventListener("click", this._onSaveEditLogin.bind(this), false);
+ document.getElementById("password-btn").addEventListener("click", this._onPasswordBtn.bind(this), false);
+
+ let filterInput = document.getElementById("filter-input");
+ let filterContainer = document.getElementById("filter-input-container");
+
+ filterInput.addEventListener("input", (event) => {
+ // Stop any in-progress filter timer
+ if (this._filterTimer) {
+ clearTimeout(this._filterTimer);
+ this._filterTimer = null;
+ }
+
+ // Start a new timer
+ this._filterTimer = setTimeout(() => {
+ this._filter(event);
+ }, FILTER_DELAY);
+ }, false);
+
+ filterInput.addEventListener("blur", (event) => {
+ filterContainer.setAttribute("hidden", true);
+ });
+
+ document.getElementById("filter-button").addEventListener("click", (event) => {
+ filterContainer.removeAttribute("hidden");
+ filterInput.focus();
+ }, false);
+
+ document.getElementById("filter-clear").addEventListener("click", (event) => {
+ // Stop any in-progress filter timer
+ if (this._filterTimer) {
+ clearTimeout(this._filterTimer);
+ this._filterTimer = null;
+ }
+
+ filterInput.blur();
+ filterInput.value = "";
+ this._loadList(this._logins);
+ }, false);
+
+ this._showList();
+
+ this._updatePasswordBtn(true);
+
+ this._reloadList();
+ },
+
+ uninit: function () {
+ Services.obs.removeObserver(this, "passwordmgr-storage-changed");
+ window.removeEventListener("popstate", this, false);
+ },
+
+ _loadList: function (logins) {
+ let list = document.getElementById("logins-list");
+ let newList = list.cloneNode(false);
+
+ logins.forEach(login => {
+ let item = this._createItemForLogin(login);
+ newList.appendChild(item);
+ });
+
+ list.parentNode.replaceChild(newList, list);
+ },
+
+ _showList: function () {
+ let loginsListPage = document.getElementById("logins-list-page");
+ loginsListPage.classList.remove("hidden");
+
+ let editLoginPage = document.getElementById("edit-login-page");
+ editLoginPage.classList.add("hidden");
+
+ // If the Show/Hide password button has been flipped, reset it
+ if (this._isPasswordBtnInHideMode()) {
+ this._updatePasswordBtn(true);
+ }
+ },
+
+ _onPopState: function (event) {
+ // Called when back/forward is used to change the state of the page
+ if (event.state) {
+ this._showEditLoginDialog(event.state.id);
+ } else {
+ this._selectedLogin = null;
+ this._showList();
+ }
+ },
+ _showEditLoginDialog: function (login) {
+ let listPage = document.getElementById("logins-list-page");
+ listPage.classList.add("hidden");
+
+ let editLoginPage = document.getElementById("edit-login-page");
+ editLoginPage.classList.remove("hidden");
+
+ let usernameField = document.getElementById("username");
+ usernameField.value = login.username;
+ let passwordField = document.getElementById("password");
+ passwordField.value = login.password;
+ let domainField = document.getElementById("hostname");
+ domainField.value = login.hostname;
+
+ let img = document.getElementById("favicon");
+ this._loadFavicon(img, login.hostname);
+
+ let headerText = document.getElementById("edit-login-header-text");
+ if (login.hostname && (login.hostname != "")) {
+ headerText.textContent = login.hostname;
+ }
+ else {
+ headerText.textContent = gStringBundle.GetStringFromName("editLogin.fallbackTitle");
+ }
+
+ passwordField.addEventListener("input", (event) => {
+ let newPassword = passwordField.value;
+ let updateBtn = document.getElementById("update-btn");
+
+ if (newPassword === "") {
+ updateBtn.disabled = true;
+ updateBtn.classList.add("disabled-btn");
+ } else if ((newPassword !== "") && (updateBtn.disabled === true)) {
+ updateBtn.disabled = false;
+ updateBtn.classList.remove("disabled-btn");
+ }
+ }, false);
+ },
+
+ _onSaveEditLogin: function() {
+ let newUsername = document.getElementById("username").value;
+ let newPassword = document.getElementById("password").value;
+ let newDomain = document.getElementById("hostname").value;
+ let origUsername = this._selectedLogin.username;
+ let origPassword = this._selectedLogin.password;
+ let origDomain = this._selectedLogin.hostname;
+
+ try {
+ if ((newUsername === origUsername) &&
+ (newPassword === origPassword) &&
+ (newDomain === origDomain) ) {
+ Snackbars.show(gStringBundle.GetStringFromName("editLogin.saved1"), Snackbars.LENGTH_LONG);
+ this._showList();
+ return;
+ }
+
+ let logins = Services.logins.findLogins({}, origDomain, origDomain, null);
+
+ for (let i = 0; i < logins.length; i++) {
+ if (logins[i].username == origUsername) {
+ let clone = logins[i].clone();
+ clone.username = newUsername;
+ clone.password = newPassword;
+ clone.hostname = newDomain;
+ Services.logins.removeLogin(logins[i]);
+ Services.logins.addLogin(clone);
+ break;
+ }
+ }
+ } catch (e) {
+ Snackbars.show(gStringBundle.GetStringFromName("editLogin.couldNotSave"), Snackbars.LENGTH_LONG);
+ return;
+ }
+ Snackbars.show(gStringBundle.GetStringFromName("editLogin.saved1"), Snackbars.LENGTH_LONG);
+ this._showList();
+ },
+
+ _onPasswordBtn: function () {
+ this._updatePasswordBtn(this._isPasswordBtnInHideMode());
+ },
+
+ _updatePasswordBtn: function (aShouldShow) {
+ let passwordField = document.getElementById("password");
+ let button = document.getElementById("password-btn");
+ let show = gStringBundle.GetStringFromName("password-btn.show");
+ let hide = gStringBundle.GetStringFromName("password-btn.hide");
+ if (aShouldShow) {
+ passwordField.type = "password";
+ button.textContent = show;
+ button.classList.remove("password-btn-hide");
+ } else {
+ passwordField.type = "text";
+ button.textContent= hide;
+ button.classList.add("password-btn-hide");
+ }
+ },
+
+ _isPasswordBtnInHideMode: function () {
+ let button = document.getElementById("password-btn");
+ return button.classList.contains("password-btn-hide");
+ },
+
+ _showPassword: function(password) {
+ let passwordPrompt = new Prompt({
+ window: window,
+ message: password,
+ buttons: [
+ gStringBundle.GetStringFromName("loginsDialog.copy"),
+ gStringBundle.GetStringFromName("loginsDialog.cancel") ]
+ }).show((data) => {
+ switch (data.button) {
+ case 0:
+ // Corresponds to "Copy password" button.
+ copyStringShowSnackbar(password, gStringBundle.GetStringFromName("loginsDetails.passwordCopied"));
+ }
+ });
+ },
+
+ _onLoginClick: function (event) {
+ let loginItem = event.currentTarget;
+ let login = loginItem.login;
+ if (!login) {
+ debug("No login!");
+ return;
+ }
+
+ let prompt = new Prompt({
+ window: window,
+ });
+ let menuItems = [
+ { label: gStringBundle.GetStringFromName("loginsMenu.showPassword") },
+ { label: gStringBundle.GetStringFromName("loginsMenu.copyPassword") },
+ { label: gStringBundle.GetStringFromName("loginsMenu.copyUsername") },
+ { label: gStringBundle.GetStringFromName("loginsMenu.editLogin") },
+ { label: gStringBundle.GetStringFromName("loginsMenu.delete") }
+ ];
+
+ prompt.setSingleChoiceItems(menuItems);
+ prompt.show((data) => {
+ // Switch on indices of buttons, as they were added when creating login item.
+ switch (data.button) {
+ case 0:
+ this._showPassword(login.password);
+ break;
+ case 1:
+ copyStringShowSnackbar(login.password, gStringBundle.GetStringFromName("loginsDetails.passwordCopied"));
+ break;
+ case 2:
+ copyStringShowSnackbar(login.username, gStringBundle.GetStringFromName("loginsDetails.usernameCopied"));
+ break;
+ case 3:
+ this._selectedLogin = login;
+ this._showEditLoginDialog(login);
+ history.pushState({ id: login.guid }, document.title);
+ break;
+ case 4:
+ let confirmPrompt = new Prompt({
+ window: window,
+ message: gStringBundle.GetStringFromName("loginsDialog.confirmDelete"),
+ buttons: [
+ gStringBundle.GetStringFromName("loginsDialog.confirm"),
+ gStringBundle.GetStringFromName("loginsDialog.cancel") ]
+ });
+ confirmPrompt.show((data) => {
+ switch (data.button) {
+ case 0:
+ // Corresponds to "confirm" button.
+ Services.logins.removeLogin(login);
+ }
+ });
+ }
+ });
+ },
+
+ _loadFavicon: function (aImg, aHostname) {
+ // Load favicon from cache.
+ Messaging.sendRequestForResult({
+ type: "Favicon:CacheLoad",
+ url: aHostname,
+ }).then(function(faviconUrl) {
+ aImg.style.backgroundImage= "url('" + faviconUrl + "')";
+ aImg.style.visibility = "visible";
+ }, function(data) {
+ debug("Favicon cache failure : " + data);
+ aImg.style.visibility = "visible";
+ });
+ },
+
+ _createItemForLogin: function (login) {
+ let loginItem = document.createElement("div");
+
+ loginItem.setAttribute("loginID", login.guid);
+ loginItem.className = "login-item list-item";
+
+ loginItem.addEventListener("click", this, true);
+
+ // Create item icon.
+ let img = document.createElement("div");
+ img.className = "icon";
+
+ this._loadFavicon(img, login.hostname);
+ loginItem.appendChild(img);
+
+ // Create item details.
+ let inner = document.createElement("div");
+ inner.className = "inner";
+
+ let details = document.createElement("div");
+ details.className = "details";
+ inner.appendChild(details);
+
+ let titlePart = document.createElement("div");
+ titlePart.className = "hostname";
+ titlePart.textContent = login.hostname;
+ details.appendChild(titlePart);
+
+ let versionPart = document.createElement("div");
+ versionPart.textContent = login.httpRealm;
+ versionPart.className = "realm";
+ details.appendChild(versionPart);
+
+ let descPart = document.createElement("div");
+ descPart.textContent = login.username;
+ descPart.className = "username";
+ inner.appendChild(descPart);
+
+ loginItem.appendChild(inner);
+ loginItem.login = login;
+ return loginItem;
+ },
+
+ handleEvent: function (event) {
+ switch (event.type) {
+ case "popstate": {
+ this._onPopState(event);
+ break;
+ }
+ case "click": {
+ this._onLoginClick(event);
+ break;
+ }
+ }
+ },
+
+ observe: function (subject, topic, data) {
+ switch(topic) {
+ case "passwordmgr-storage-changed": {
+ this._reloadList();
+ break;
+ }
+ }
+ },
+
+ _filter: function(event) {
+ let value = event.target.value.toLowerCase();
+ let logins = this._logins.filter((login) => {
+ if (login.hostname.toLowerCase().indexOf(value) != -1) {
+ return true;
+ }
+ if (login.username &&
+ login.username.toLowerCase().indexOf(value) != -1) {
+ return true;
+ }
+ if (login.httpRealm &&
+ login.httpRealm.toLowerCase().indexOf(value) != -1) {
+ return true;
+ }
+ return false;
+ });
+
+ this._loadList(logins);
+ }
+};
+
+window.addEventListener("load", Logins.init.bind(Logins), false);
+window.addEventListener("unload", Logins.uninit.bind(Logins), false);
diff --git a/mobile/android/chrome/content/aboutLogins.xhtml b/mobile/android/chrome/content/aboutLogins.xhtml
new file mode 100644
index 000000000..02225d43a
--- /dev/null
+++ b/mobile/android/chrome/content/aboutLogins.xhtml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [
+<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" >
+%globalDTD;
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % aboutDTD SYSTEM "chrome://browser/locale/aboutLogins.dtd" >
+%aboutDTD;
+]>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+- License, v. 2.0. If a copy of the MPL was not distributed with this
+- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>&aboutLogins.title;</title>
+ <meta name="viewport" content="width=device-width; user-scalable=0" />
+ <link rel="icon" type="image/png" sizes="64x64" href="chrome://branding/content/favicon64.png" />
+ <link rel="stylesheet" href="chrome://browser/skin/spinner.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/skin/aboutBase.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/skin/aboutLogins.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://browser/content/aboutLogins.js"></script>
+ </head>
+ <body dir="&locale.dir;">
+
+ <div id="logins-list-page">
+ <div id="logins-header" class="header">
+ <div>&aboutLogins.title;</div>
+ <ul class="toolbar-buttons">
+ <li id="filter-button"></li>
+ </ul>
+ </div>
+ <div id="content-body">
+ <div id="logins-list" class="list"/>
+ <div id="filter-input-container" hidden="true">
+ <input id="filter-input" type="search"/>
+ <div id="filter-clear"/>
+ </div>
+ </div>
+ <div id="logins-list-loading-body" class="hidden">
+ <div id="loading-img-container">
+
+ <div id="spinner" class="mui-refresh-main">
+ <div class="mui-refresh-wrapper">
+ <div class="mui-spinner-wrapper">
+ <div class="mui-spinner-main">
+ <div class="mui-spinner-left">
+ <div class="mui-half-circle-left" />
+ </div>
+ <div class="mui-spinner-right">
+ <div class="mui-half-circle-right" />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ </div>
+ </div>
+ <div id="empty-body" class="hidden">
+ <div id="empty-obj-text-container">
+ <object type="image/svg+xml" id="empty-icon" data="chrome://browser/skin/images/icon_key_emptypage.svg"/>
+ <div class="empty-text">&aboutLogins.emptyLoginText;</div>
+ <div class="empty-hint">&aboutLogins.emptyLoginHint;</div>
+ </div>
+ </div>
+ </div>
+
+ <div id="edit-login-page" class="hidden">
+ <div id="edit-login-header" class="header">
+ <div id="edit-login-header-text"/>
+ </div>
+ <div class="edit-login-div">
+ <div id="favicon" class="edit-login-icon"/>
+ <input type="text" name="hostname" id="hostname" class="edit-login-input"/>
+ </div>
+ <div class="edit-login-div">
+ <input type="text" name="username" id="username" class="edit-login-input" autocomplete="off"/>
+ </div>
+ <div class="edit-login-div">
+ <input type="password" id="password" name="password" value="password" class="edit-login-input" />
+ <button id="password-btn"></button>
+ </div>
+ <div class="edit-login-div">
+ <button id="update-btn" class="update-button">&aboutLogins.update;</button>
+ </div>
+ </div>
+
+ </body>
+</html>
diff --git a/mobile/android/chrome/content/aboutPrivateBrowsing.js b/mobile/android/chrome/content/aboutPrivateBrowsing.js
new file mode 100644
index 000000000..782abfb5d
--- /dev/null
+++ b/mobile/android/chrome/content/aboutPrivateBrowsing.js
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(window, "gChromeWin", () =>
+ window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow)
+ .QueryInterface(Ci.nsIDOMChromeWindow));
+
+document.addEventListener("DOMContentLoaded", function() {
+ let BrowserApp = gChromeWin.BrowserApp;
+
+ if (!PrivateBrowsingUtils.isContentWindowPrivate(window)) {
+ document.body.setAttribute("class", "normal");
+ document.getElementById("newPrivateTabLink").addEventListener("click", function() {
+ BrowserApp.addTab("about:privatebrowsing", { selected: true, parentId: BrowserApp.selectedTab.id, isPrivate: true });
+ }, false);
+ }
+ }, false);
diff --git a/mobile/android/chrome/content/aboutPrivateBrowsing.xhtml b/mobile/android/chrome/content/aboutPrivateBrowsing.xhtml
new file mode 100644
index 000000000..7075bd11e
--- /dev/null
+++ b/mobile/android/chrome/content/aboutPrivateBrowsing.xhtml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+-->
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+ %brandDTD;
+ <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
+ %globalDTD;
+ <!ENTITY % privatebrowsingpageDTD SYSTEM "chrome://browser/locale/aboutPrivateBrowsing.dtd">
+ %privatebrowsingpageDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>&privatebrowsingpage.title;</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1; user-scalable=no"/>
+ <link rel="stylesheet" href="chrome://browser/skin/aboutPrivateBrowsing.css" type="text/css" media="all"/>
+ <link rel="icon" type="image/png" href="chrome://branding/content/favicon32.png" />
+ <script type="application/javascript;version=1.8" src="chrome://browser/content/aboutPrivateBrowsing.js"></script>
+ </head>
+
+ <body class="private">
+ <img class="showPrivate masq" src="chrome://browser/skin/images/privatebrowsing-mask-and-shield.svg" />
+ <img class="showNormal masq" src="chrome://browser/skin/images/privatebrowsing-mask.png" />
+
+ <h1 class="showPrivate">&privatebrowsingpage.title;<br />&privatebrowsingpage.title.private;</h1>
+ <h1 class="showNormal">&privatebrowsingpage.title.normal1;</h1>
+
+ <div class="contentSection">
+ <p class="showPrivate">&privatebrowsingpage.description.trackingProtection;<br /><br />&privatebrowsingpage.description.privateDetails;</p>
+ <p class="showNormal">&privatebrowsingpage.description.normal2;</p>
+
+ <p class="showPrivate"><a href="https://support.mozilla.org/kb/private-browsing-firefox-android">&privatebrowsingpage.link.private;</a></p>
+ <p class="showNormal"><a href="#" id="newPrivateTabLink">&privatebrowsingpage.link.normal;</a></p>
+ </div>
+
+ </body>
+</html>
diff --git a/mobile/android/chrome/content/aboutRights.xhtml b/mobile/android/chrome/content/aboutRights.xhtml
new file mode 100644
index 000000000..8172788f4
--- /dev/null
+++ b/mobile/android/chrome/content/aboutRights.xhtml
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+ %brandDTD;
+ <!ENTITY % aboutRightsDTD SYSTEM "chrome://global/locale/aboutRights.dtd">
+ %aboutRightsDTD;
+]>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+<head>
+ <title>&rights.pagetitle;</title>
+ <link rel="stylesheet" href="chrome://global/skin/about.css" type="text/css"/>
+</head>
+
+<body id="your-rights" dir="&rights.locale-direction;" class="aboutPageWideContainer">
+
+<h1>&rights.intro-header;</h1>
+
+<p>&rights.intro;</p>
+
+<ul>
+ <li>&rights.intro-point1a;<a href="https://www.mozilla.org/MPL/">&rights.intro-point1b;</a>&rights.intro-point1c;</li>
+ <!-- Point 2 discusses Mozilla trademarks, and isn't needed when the build is unbranded.
+ Point 3 discusses privacy policy, unbranded builds get a placeholder (for the vendor to replace)
+ Point 4 discusses web service terms, unbranded builds gets a placeholder (for the vendor to replace) -->
+ <li>&rights.intro-point2-a;<a href="https://www.mozilla.org/foundation/trademarks/policy.html">&rights.intro-point2-b;</a>&rights.intro-point2-c;</li>
+ <li>&rights.intro-point2.5;</li>
+ <li>&rights2.intro-point3a;<a href="https://www.mozilla.org/privacy/firefox/">&rights2.intro-point3b;</a>&rights.intro-point3c;</li>
+ <li>&rights2.intro-point4a;<a href="about:rights#webservices" onclick="showServices();">&rights.intro-point4b;</a>&rights.intro-point4c;</li>
+</ul>
+
+<div id="webservices-container">
+ <a name="webservices"/>
+ <h3>&rights2.webservices-header;</h3>
+
+ <p>&rights2.webservices-a;<a href="about:rights#disabling-webservices" onclick="showDisablingServices();">&rights2.webservices-b;</a>&rights3.webservices-c;</p>
+
+ <div id="disabling-webservices-container" style="margin-left:40px;">
+ <a name="disabling-webservices"/>
+ <!-- Safe Browsing cannot be disabled in Firefox Mobile; these instructions show how to do it on desktop
+ <p><strong>&rights.safebrowsing-a;</strong>&rights.safebrowsing-b;</p>
+ <ul>
+ <li>&rights.safebrowsing-term1;</li>
+ <li>&rights.safebrowsing-term2;</li>
+ <li>&rights2.safebrowsing-term3;</li>
+ <li>&rights.safebrowsing-term4;</li>
+ </ul>
+ -->
+
+ <p><strong>&rights.locationawarebrowsing-a;</strong>&rights.locationawarebrowsing-b;</p>
+ <ul>
+ <li>&rights.locationawarebrowsing-term1a;<code>&rights.locationawarebrowsing-term1b;</code></li>
+ <li>&rights.locationawarebrowsing-term2;</li>
+ <li>&rights.locationawarebrowsing-term3;</li>
+ <li>&rights.locationawarebrowsing-term4;</li>
+ </ul>
+ </div>
+
+ <ol>
+ <!-- Terms only apply to official builds, unbranded builds get a placeholder. -->
+ <li>&rights2.webservices-term1;</li>
+ <li>&rights.webservices-term2;</li>
+ <li>&rights2.webservices-term3;</li>
+ <li><strong>&rights.webservices-term4;</strong></li>
+ <li><strong>&rights.webservices-term5;</strong></li>
+ <li>&rights.webservices-term6;</li>
+ <li>&rights.webservices-term7;</li>
+ </ol>
+</div>
+
+<script type="application/javascript"><![CDATA[
+ var servicesDiv = document.getElementById("webservices-container");
+ servicesDiv.style.display = "none";
+
+ function showServices() {
+ servicesDiv.style.display = "";
+ }
+
+ var disablingServicesDiv = document.getElementById("disabling-webservices-container");
+ disablingServicesDiv.style.display = "none";
+
+ function showDisablingServices() {
+ disablingServicesDiv.style.display = "";
+ }
+]]></script>
+
+</body>
+</html>
diff --git a/mobile/android/chrome/content/bindings/checkbox.xml b/mobile/android/chrome/content/bindings/checkbox.xml
new file mode 100644
index 000000000..ec5c7828b
--- /dev/null
+++ b/mobile/android/chrome/content/bindings/checkbox.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE bindings [
+ <!ENTITY % checkboxDTD SYSTEM "chrome://browser/locale/checkbox.dtd">
+ %checkboxDTD;
+]>
+
+<bindings
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xbl="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <binding id="checkbox-radio" display="xul:box" extends="chrome://global/content/bindings/checkbox.xml#checkbox-baseline">
+ <content>
+ <xul:radiogroup anonid="group" xbl:inherits="disabled">
+ <xul:radio anonid="on" class="checkbox-radio-on" label="&checkbox.yes.label;" xbl:inherits="label=onlabel"/>
+ <xul:radio anonid="off" class="checkbox-radio-off" label="&checkbox.no.label;" xbl:inherits="label=offlabel"/>
+ </xul:radiogroup>
+ </content>
+ <implementation>
+ <constructor><![CDATA[
+ this.setChecked(this.checked);
+ ]]></constructor>
+
+ <field name="_group">
+ document.getAnonymousElementByAttribute(this, "anonid", "group");
+ </field>
+
+ <field name="_on">
+ document.getAnonymousElementByAttribute(this, "anonid", "on");
+ </field>
+
+ <field name="_off">
+ document.getAnonymousElementByAttribute(this, "anonid", "off");
+ </field>
+
+ <property name="onlabel"
+ onget="return this._on.label"
+ onset="this._on.label=val"/>
+
+ <property name="offlabel"
+ onget="return this._off.label"
+ onset="this._off.label=val"/>
+
+ <method name="setChecked">
+ <parameter name="aValue"/>
+ <body>
+ <![CDATA[
+ var change = (aValue != this.checked);
+ if (aValue) {
+ this.setAttribute("checked", "true");
+ this._group.selectedItem = this._on;
+ }
+ else {
+ this.removeAttribute("checked");
+ this._group.selectedItem = this._off;
+ }
+
+ if (change) {
+ var event = document.createEvent("Events");
+ event.initEvent("CheckboxStateChange", true, true);
+ this.dispatchEvent(event);
+ }
+ return aValue;
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+</bindings>
diff --git a/mobile/android/chrome/content/bindings/settings.xml b/mobile/android/chrome/content/bindings/settings.xml
new file mode 100644
index 000000000..0019e9d3b
--- /dev/null
+++ b/mobile/android/chrome/content/bindings/settings.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<bindings
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xbl="http://www.mozilla.org/xbl"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <binding id="setting-fulltoggle-bool" extends="chrome://mozapps/content/extensions/setting.xml#setting-bool">
+ <handlers>
+ <handler event="command" button="0" phase="capturing">
+ <![CDATA[
+ event.stopPropagation();
+ ]]>
+ </handler>
+ <handler event="click" button="0" phase="capturing">
+ <![CDATA[
+ event.stopPropagation();
+ this.input.checked = !this.input.checked;
+ this.inputChanged();
+ this.fireEvent("oncommand");
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="setting-fulltoggle-boolint" extends="chrome://mozapps/content/extensions/setting.xml#setting-boolint">
+ <handlers>
+ <handler event="command" button="0" phase="capturing">
+ <![CDATA[
+ event.stopPropagation();
+ ]]>
+ </handler>
+ <handler event="click" button="0" phase="capturing">
+ <![CDATA[
+ event.stopPropagation();
+ this.input.checked = !this.input.checked;
+ this.inputChanged();
+ this.fireEvent("oncommand");
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="setting-fulltoggle-localized-bool" extends="chrome://mozapps/content/extensions/setting.xml#setting-localized-bool">
+ <handlers>
+ <handler event="command" button="0" phase="capturing">
+ <![CDATA[
+ event.stopPropagation();
+ ]]>
+ </handler>
+ <handler event="click" button="0" phase="capturing">
+ <![CDATA[
+ event.stopPropagation();
+ this.input.checked = !this.input.checked;
+ this.inputChanged();
+ this.fireEvent("oncommand");
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+</bindings>
diff --git a/mobile/android/chrome/content/blockedSite.xhtml b/mobile/android/chrome/content/blockedSite.xhtml
new file mode 100644
index 000000000..5f04edbef
--- /dev/null
+++ b/mobile/android/chrome/content/blockedSite.xhtml
@@ -0,0 +1,195 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
+ %globalDTD;
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+ %brandDTD;
+ <!ENTITY % blockedSiteDTD SYSTEM "chrome://browser/locale/phishing.dtd">
+ %blockedSiteDTD;
+]>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml" class="blacklist">
+ <head>
+ <meta name="viewport" content="width=device-width; user-scalable=false" />
+ <link rel="stylesheet" href="chrome://global/skin/netError.css" type="text/css" media="all" />
+ <link rel="icon" type="image/png" id="favicon" sizes="64x64" href="chrome://browser/skin/images/blocked-warning.png"/>
+
+ <script type="application/javascript"><![CDATA[
+ // Error url MUST be formatted like this:
+ // about:blocked?e=error_code&u=url(&o=1)?
+ // (o=1 when user overrides are allowed)
+
+ // Note that this file uses document.documentURI to get
+ // the URL (with the format from above). This is because
+ // document.location.href gets the current URI off the docshell,
+ // which is the URL displayed in the location bar, i.e.
+ // the URI that the user attempted to load.
+
+ function getErrorCode()
+ {
+ var url = document.documentURI;
+ var error = url.search(/e\=/);
+ var duffUrl = url.search(/\&u\=/);
+ return decodeURIComponent(url.slice(error + 2, duffUrl));
+ }
+
+ function getURL()
+ {
+ var url = document.documentURI;
+ var match = url.match(/&u=([^&]+)&/);
+
+ // match == null if not found; if so, return an empty string
+ // instead of what would turn out to be portions of the URI
+ if (!match)
+ return "";
+
+ url = decodeURIComponent(match[1]);
+
+ // If this is a view-source page, then get then real URI of the page
+ if (/^view-source\:/.test(url))
+ url = url.slice(12);
+ return url;
+ }
+
+ /**
+ * Check whether this warning page should be overridable or whether
+ * the "ignore warning" button should be hidden.
+ */
+ function getOverride()
+ {
+ var url = document.documentURI;
+ var match = url.match(/&o=1&/);
+ return !!match;
+ }
+
+ /**
+ * Attempt to get the hostname via document.location. Fail back
+ * to getURL so that we always return something meaningful.
+ */
+ function getHostString()
+ {
+ try {
+ return document.location.hostname;
+ } catch (e) {
+ return getURL();
+ }
+ }
+
+ function initPage()
+ {
+ var error = "";
+ switch (getErrorCode()) {
+ case "malwareBlocked" :
+ error = "malware";
+ break;
+ case "deceptiveBlocked" :
+ error = "phishing";
+ break;
+ case "unwantedBlocked" :
+ error = "unwanted";
+ break;
+ default:
+ return;
+ }
+
+ var el;
+
+ if (error !== "malware") {
+ el = document.getElementById("errorTitleText_malware");
+ el.parentNode.removeChild(el);
+ el = document.getElementById("errorShortDescText_malware");
+ el.parentNode.removeChild(el);
+ el = document.getElementById("errorLongDescText_malware");
+ el.parentNode.removeChild(el);
+ }
+
+ if (error !== "phishing") {
+ el = document.getElementById("errorTitleText_phishing");
+ el.parentNode.removeChild(el);
+ el = document.getElementById("errorShortDescText_phishing");
+ el.parentNode.removeChild(el);
+ el = document.getElementById("errorLongDescText_phishing");
+ el.parentNode.removeChild(el);
+ }
+
+ if (error !== "unwanted") {
+ el = document.getElementById("errorTitleText_unwanted");
+ el.parentNode.removeChild(el);
+ el = document.getElementById("errorShortDescText_unwanted");
+ el.parentNode.removeChild(el);
+ el = document.getElementById("errorLongDescText_unwanted");
+ el.parentNode.removeChild(el);
+ }
+
+ if (!getOverride()) {
+ var btn = document.getElementById("ignoreWarningButton");
+ if (btn) {
+ btn.parentNode.removeChild(btn);
+ }
+ }
+
+ // Set sitename
+ document.getElementById(error + "_sitename").textContent = getHostString();
+ document.title = document.getElementById("errorTitleText_" + error)
+ .innerHTML;
+
+ // Inform the test harness that we're done loading the page
+ var event = new CustomEvent("AboutBlockedLoaded");
+ document.dispatchEvent(event);
+ }
+ ]]></script>
+ </head>
+
+ <body id="errorPage" class="blockedsite" dir="&locale.dir;">
+
+ <div id="errorPageContainer">
+
+ <!-- Error Title -->
+ <div id="errorTitle">
+ <h1 id="errorTitleText_phishing" class="errorTitleText">&safeb.blocked.phishingPage.title3;</h1>
+ <h1 id="errorTitleText_malware" class="errorTitleText">&safeb.blocked.malwarePage.title;</h1>
+ <h1 id="errorTitleText_unwanted" class="errorTitleText">&safeb.blocked.unwantedPage.title;</h1>
+ </div>
+
+ <div id="errorLongContent">
+
+ <!-- Short Description -->
+ <div id="errorShortDesc">
+ <p id="errorShortDescText_phishing">&safeb.blocked.phishingPage.shortDesc3;</p>
+ <p id="errorShortDescText_malware">&safeb.blocked.malwarePage.shortDesc;</p>
+ <p id="errorShortDescText_unwanted">&safeb.blocked.unwantedPage.shortDesc;</p>
+ </div>
+
+ <!-- Long Description -->
+ <div id="errorLongDesc">
+ <p id="errorLongDescText_phishing">&safeb.blocked.phishingPage.longDesc3;</p>
+ <p id="errorLongDescText_malware">&safeb.blocked.malwarePage.longDesc;</p>
+ <p id="errorLongDescText_unwanted">&safeb.blocked.unwantedPage.longDesc;</p>
+ </div>
+
+ <!-- Action buttons -->
+ <div id="buttons">
+ <!-- Commands handled in browser.js -->
+ <button id="getMeOutButton">&safeb.palm.accept.label;</button>
+ <button id="reportButton">&safeb.palm.reportPage.label;</button>
+ </div>
+ </div>
+ <div id="ignoreWarning">
+ <button id="ignoreWarningButton">&safeb.palm.decline.label;</button>
+ </div>
+ </div>
+ <!--
+ - Note: It is important to run the script this way, instead of using
+ - an onload handler. This is because error pages are loaded as
+ - LOAD_BACKGROUND, which means that onload handlers will not be executed.
+ -->
+ <script type="application/javascript">initPage();</script>
+ </body>
+</html>
diff --git a/mobile/android/chrome/content/browser.css b/mobile/android/chrome/content/browser.css
new file mode 100644
index 000000000..fdc35d961
--- /dev/null
+++ b/mobile/android/chrome/content/browser.css
@@ -0,0 +1,7 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+browser[remote="true"] {
+ -moz-binding: url("chrome://global/content/bindings/remote-browser.xml#remote-browser");
+} \ No newline at end of file
diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js
new file mode 100644
index 000000000..b00e1af15
--- /dev/null
+++ b/mobile/android/chrome/content/browser.js
@@ -0,0 +1,6999 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+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/AsyncPrefs.jsm");
+Cu.import("resource://gre/modules/DelayedInit.jsm");
+
+if (AppConstants.ACCESSIBILITY) {
+ XPCOMUtils.defineLazyModuleGetter(this, "AccessFu",
+ "resource://gre/modules/accessibility/AccessFu.jsm");
+}
+
+XPCOMUtils.defineLazyModuleGetter(this, "SpatialNavigation",
+ "resource://gre/modules/SpatialNavigation.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadNotifications",
+ "resource://gre/modules/DownloadNotifications.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "JNI",
+ "resource://gre/modules/JNI.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
+ "resource://gre/modules/UITelemetry.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
+ "resource://gre/modules/PluralForm.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+ "resource://gre/modules/Downloads.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging",
+ "resource://gre/modules/Messaging.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "UserAgentOverrides",
+ "resource://gre/modules/UserAgentOverrides.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent",
+ "resource://gre/modules/LoginManagerContent.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerParent",
+ "resource://gre/modules/LoginManagerParent.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "SafeBrowsing",
+ "resource://gre/modules/SafeBrowsing.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
+ "resource://gre/modules/BrowserUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Sanitizer",
+ "resource://gre/modules/Sanitizer.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
+ "resource://gre/modules/Prompt.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "HelperApps",
+ "resource://gre/modules/HelperApps.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "SSLExceptions",
+ "resource://gre/modules/SSLExceptions.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
+ "resource://gre/modules/FormHistory.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator");
+
+XPCOMUtils.defineLazyServiceGetter(this, "Profiler",
+ "@mozilla.org/tools/profiler;1",
+ "nsIProfiler");
+
+XPCOMUtils.defineLazyModuleGetter(this, "SimpleServiceDiscovery",
+ "resource://gre/modules/SimpleServiceDiscovery.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu",
+ "resource://gre/modules/CharsetMenu.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetErrorHelper",
+ "resource://gre/modules/NetErrorHelper.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PermissionsUtils",
+ "resource://gre/modules/PermissionsUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+ "resource://gre/modules/Preferences.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "SharedPreferences",
+ "resource://gre/modules/SharedPreferences.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Notifications",
+ "resource://gre/modules/Notifications.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "RuntimePermissions", "resource://gre/modules/RuntimePermissions.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "WebsiteMetadata", "resource://gre/modules/WebsiteMetadata.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "FontEnumerator",
+ "@mozilla.org/gfx/fontenumerator;1",
+ "nsIFontEnumerator");
+
+var lazilyLoadedBrowserScripts = [
+ ["SelectHelper", "chrome://browser/content/SelectHelper.js"],
+ ["InputWidgetHelper", "chrome://browser/content/InputWidgetHelper.js"],
+ ["MasterPassword", "chrome://browser/content/MasterPassword.js"],
+ ["PluginHelper", "chrome://browser/content/PluginHelper.js"],
+ ["OfflineApps", "chrome://browser/content/OfflineApps.js"],
+ ["Linkifier", "chrome://browser/content/Linkify.js"],
+ ["CastingApps", "chrome://browser/content/CastingApps.js"],
+ ["RemoteDebugger", "chrome://browser/content/RemoteDebugger.js"],
+];
+if (!AppConstants.RELEASE_OR_BETA) {
+ lazilyLoadedBrowserScripts.push(
+ ["WebcompatReporter", "chrome://browser/content/WebcompatReporter.js"]);
+}
+
+lazilyLoadedBrowserScripts.forEach(function (aScript) {
+ let [name, script] = aScript;
+ XPCOMUtils.defineLazyGetter(window, name, function() {
+ let sandbox = {};
+ Services.scriptloader.loadSubScript(script, sandbox);
+ return sandbox[name];
+ });
+});
+
+var lazilyLoadedObserverScripts = [
+ ["MemoryObserver", ["memory-pressure", "Memory:Dump"], "chrome://browser/content/MemoryObserver.js"],
+ ["ConsoleAPI", ["console-api-log-event"], "chrome://browser/content/ConsoleAPI.js"],
+ ["FindHelper", ["FindInPage:Opened", "FindInPage:Closed", "Tab:Selected"], "chrome://browser/content/FindHelper.js"],
+ ["PermissionsHelper", ["Permissions:Check", "Permissions:Get", "Permissions:Clear"], "chrome://browser/content/PermissionsHelper.js"],
+ ["FeedHandler", ["Feeds:Subscribe"], "chrome://browser/content/FeedHandler.js"],
+ ["Feedback", ["Feedback:Show"], "chrome://browser/content/Feedback.js"],
+ ["EmbedRT", ["GeckoView:ImportScript"], "chrome://browser/content/EmbedRT.js"],
+ ["Reader", ["Reader:AddToCache", "Reader:RemoveFromCache"], "chrome://browser/content/Reader.js"],
+ ["PrintHelper", ["Print:PDF"], "chrome://browser/content/PrintHelper.js"],
+];
+
+lazilyLoadedObserverScripts.push(
+["ActionBarHandler", ["TextSelection:Get", "TextSelection:Action", "TextSelection:End"],
+ "chrome://browser/content/ActionBarHandler.js"]
+);
+
+if (AppConstants.MOZ_WEBRTC) {
+ lazilyLoadedObserverScripts.push(
+ ["WebrtcUI", ["getUserMedia:request",
+ "PeerConnection:request",
+ "recording-device-events",
+ "VideoCapture:Paused",
+ "VideoCapture:Resumed"], "chrome://browser/content/WebrtcUI.js"])
+}
+
+lazilyLoadedObserverScripts.forEach(function (aScript) {
+ let [name, notifications, script] = aScript;
+ XPCOMUtils.defineLazyGetter(window, name, function() {
+ let sandbox = {};
+ Services.scriptloader.loadSubScript(script, sandbox);
+ return sandbox[name];
+ });
+ let observer = (s, t, d) => {
+ Services.obs.removeObserver(observer, t);
+ Services.obs.addObserver(window[name], t, false);
+ window[name].observe(s, t, d); // Explicitly notify new observer
+ };
+ notifications.forEach((notification) => {
+ Services.obs.addObserver(observer, notification, false);
+ });
+});
+
+// Lazily-loaded browser scripts that use message listeners.
+[
+ ["Reader", [
+ ["Reader:AddToCache", false],
+ ["Reader:RemoveFromCache", false],
+ ["Reader:ArticleGet", false],
+ ["Reader:DropdownClosed", true], // 'true' allows us to survive mid-air cycle-collection.
+ ["Reader:DropdownOpened", false],
+ ["Reader:FaviconRequest", false],
+ ["Reader:ToolbarHidden", false],
+ ["Reader:SystemUIVisibility", false],
+ ["Reader:UpdateReaderButton", false],
+ ], "chrome://browser/content/Reader.js"],
+].forEach(aScript => {
+ let [name, messages, script] = aScript;
+ XPCOMUtils.defineLazyGetter(window, name, function() {
+ let sandbox = {};
+ Services.scriptloader.loadSubScript(script, sandbox);
+ return sandbox[name];
+ });
+
+ let mm = window.getGroupMessageManager("browsers");
+ let listener = (message) => {
+ mm.removeMessageListener(message.name, listener);
+ let listenAfterClose = false;
+ for (let [name, laClose] of messages) {
+ if (message.name === name) {
+ listenAfterClose = laClose;
+ break;
+ }
+ }
+
+ mm.addMessageListener(message.name, window[name], listenAfterClose);
+ window[name].receiveMessage(message);
+ };
+
+ messages.forEach((message) => {
+ let [name, listenAfterClose] = message;
+ mm.addMessageListener(name, listener, listenAfterClose);
+ });
+});
+
+// Lazily-loaded JS modules that use observer notifications
+[
+ ["Home", ["HomeBanner:Get", "HomePanels:Get", "HomePanels:Authenticate", "HomePanels:RefreshView",
+ "HomePanels:Installed", "HomePanels:Uninstalled"], "resource://gre/modules/Home.jsm"],
+].forEach(module => {
+ let [name, notifications, resource] = module;
+ XPCOMUtils.defineLazyModuleGetter(this, name, resource);
+ let observer = (s, t, d) => {
+ Services.obs.removeObserver(observer, t);
+ Services.obs.addObserver(this[name], t, false);
+ this[name].observe(s, t, d); // Explicitly notify new observer
+ };
+ notifications.forEach(notification => {
+ Services.obs.addObserver(observer, notification, false);
+ });
+});
+
+XPCOMUtils.defineLazyServiceGetter(this, "Haptic",
+ "@mozilla.org/widget/hapticfeedback;1", "nsIHapticFeedback");
+
+XPCOMUtils.defineLazyServiceGetter(this, "ParentalControls",
+ "@mozilla.org/parental-controls-service;1", "nsIParentalControlsService");
+
+XPCOMUtils.defineLazyServiceGetter(this, "DOMUtils",
+ "@mozilla.org/inspector/dom-utils;1", "inIDOMUtils");
+
+XPCOMUtils.defineLazyServiceGetter(window, "URIFixup",
+ "@mozilla.org/docshell/urifixup;1", "nsIURIFixup");
+
+if (AppConstants.MOZ_WEBRTC) {
+ XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService",
+ "@mozilla.org/mediaManagerService;1", "nsIMediaManagerService");
+}
+
+XPCOMUtils.defineLazyModuleGetter(this, "Log",
+ "resource://gre/modules/AndroidLog.jsm", "AndroidLog");
+
+// Define the "dump" function as a binding of the Log.d function so it specifies
+// the "debug" priority and a log tag.
+function dump(msg) {
+ Log.d("Browser", msg);
+}
+
+const kStateActive = 0x00000001; // :active pseudoclass for elements
+
+const kXLinkNamespace = "http://www.w3.org/1999/xlink";
+
+function fuzzyEquals(a, b) {
+ return (Math.abs(a - b) < 1e-6);
+}
+
+XPCOMUtils.defineLazyGetter(this, "ContentAreaUtils", function() {
+ let ContentAreaUtils = {};
+ Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js", ContentAreaUtils);
+ return ContentAreaUtils;
+});
+
+XPCOMUtils.defineLazyModuleGetter(this, "Rect", "resource://gre/modules/Geometry.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Point", "resource://gre/modules/Geometry.jsm");
+
+function resolveGeckoURI(aURI) {
+ if (!aURI)
+ throw "Can't resolve an empty uri";
+
+ if (aURI.startsWith("chrome://")) {
+ 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.startsWith("resource://")) {
+ 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 = {
+ init: function () {
+ XPCOMUtils.defineLazyGetter(Strings, "brand", () => Services.strings.createBundle("chrome://branding/locale/brand.properties"));
+ XPCOMUtils.defineLazyGetter(Strings, "browser", () => Services.strings.createBundle("chrome://browser/locale/browser.properties"));
+ XPCOMUtils.defineLazyGetter(Strings, "reader", () => Services.strings.createBundle("chrome://global/locale/aboutReader.properties"));
+ },
+
+ flush: function () {
+ Services.strings.flushBundles();
+ this.init();
+ },
+};
+
+Strings.init();
+
+const kFormHelperModeDisabled = 0;
+const kFormHelperModeEnabled = 1;
+const kFormHelperModeDynamic = 2; // disabled on tablets
+const kMaxHistoryListSize = 50;
+
+function InitLater(fn, object, name) {
+ return DelayedInit.schedule(fn, object, name, 15000 /* 15s max wait */);
+}
+
+var BrowserApp = {
+ _tabs: [],
+ _selectedTab: null,
+
+ get isTablet() {
+ let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
+ delete this.isTablet;
+ return this.isTablet = sysInfo.get("tablet");
+ },
+
+ get isOnLowMemoryPlatform() {
+ let memory = Cc["@mozilla.org/xpcom/memory-service;1"].getService(Ci.nsIMemory);
+ delete this.isOnLowMemoryPlatform;
+ return this.isOnLowMemoryPlatform = memory.isLowMemoryPlatform();
+ },
+
+ deck: null,
+
+ startup: function startup() {
+ window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = new nsBrowserAccess();
+ dump("zerdatime " + Date.now() + " - browser chrome startup finished.");
+ Services.obs.notifyObservers(this.browser, "BrowserChrome:Ready", null);
+
+ this.deck = document.getElementById("browsers");
+
+ BrowserEventHandler.init();
+
+ ViewportHandler.init();
+
+ Services.androidBridge.browserApp = this;
+
+ Services.obs.addObserver(this, "Locale:OS", false);
+ Services.obs.addObserver(this, "Locale:Changed", 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:Navigate", 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, "ScrollTo:FocusedInput", false);
+ Services.obs.addObserver(this, "Sanitize:ClearData", false);
+ Services.obs.addObserver(this, "FullScreen:Exit", false);
+ Services.obs.addObserver(this, "Passwords:Init", false);
+ Services.obs.addObserver(this, "FormHistory:Init", false);
+ Services.obs.addObserver(this, "android-get-pref", false);
+ Services.obs.addObserver(this, "android-set-pref", false);
+ Services.obs.addObserver(this, "gather-telemetry", false);
+ Services.obs.addObserver(this, "keyword-search", false);
+ Services.obs.addObserver(this, "sessionstore-state-purge-complete", false);
+ Services.obs.addObserver(this, "Fonts:Reload", false);
+ Services.obs.addObserver(this, "Vibration:Request", false);
+
+ Messaging.addListener(this.getHistory.bind(this), "Session:GetHistory");
+
+ window.addEventListener("fullscreen", function() {
+ Messaging.sendRequest({
+ type: window.fullScreen ? "ToggleChrome:Hide" : "ToggleChrome:Show"
+ });
+ }, false);
+
+ window.addEventListener("fullscreenchange", (e) => {
+ // This event gets fired on the document and its entire ancestor chain
+ // of documents. When enabling fullscreen, it is fired on the top-level
+ // document first and goes down; when disabling the order is reversed
+ // (per spec). This means the last event on enabling will be for the innermost
+ // document, which will have fullscreenElement set correctly.
+ let doc = e.target;
+ Messaging.sendRequest({
+ type: doc.fullscreenElement ? "DOMFullScreen:Start" : "DOMFullScreen:Stop",
+ rootElement: doc.fullscreenElement == doc.documentElement
+ });
+
+ if (this.fullscreenTransitionTab) {
+ // Tab selection has changed during a fullscreen transition, handle it now.
+ let tab = this.fullscreenTransitionTab;
+ this.fullscreenTransitionTab = null;
+ this._handleTabSelected(tab);
+ }
+ }, false);
+
+ NativeWindow.init();
+ FormAssistant.init();
+ IndexedDB.init();
+ XPInstallObserver.init();
+ CharacterEncoding.init();
+ ActivityObserver.init();
+ RemoteDebugger.init();
+ UserAgentOverrides.init();
+ DesktopUserAgent.init();
+ Distribution.init();
+ Tabs.init();
+ SearchEngines.init();
+ Experiments.init();
+
+ // 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);
+
+ if (this._startupStatus) {
+ this.onAppUpdated();
+ }
+
+ if (!ParentalControls.isAllowed(ParentalControls.INSTALL_EXTENSION)) {
+ // Disable extension installs
+ Services.prefs.setIntPref("extensions.enabledScopes", 1);
+ Services.prefs.setIntPref("extensions.autoDisableScopes", 1);
+ Services.prefs.setBoolPref("xpinstall.enabled", false);
+ } else if (ParentalControls.parentalControlsEnabled) {
+ Services.prefs.clearUserPref("extensions.enabledScopes");
+ Services.prefs.clearUserPref("extensions.autoDisableScopes");
+ Services.prefs.setBoolPref("xpinstall.enabled", true);
+ }
+
+ if (ParentalControls.parentalControlsEnabled) {
+ let isBlockListEnabled = ParentalControls.isAllowed(ParentalControls.BLOCK_LIST);
+ Services.prefs.setBoolPref("browser.safebrowsing.forbiddenURIs.enabled", isBlockListEnabled);
+ Services.prefs.setBoolPref("browser.safebrowsing.allowOverride", !isBlockListEnabled);
+
+ let isTelemetryEnabled = ParentalControls.isAllowed(ParentalControls.TELEMETRY);
+ Services.prefs.setBoolPref("toolkit.telemetry.enabled", isTelemetryEnabled);
+
+ let isHealthReportEnabled = ParentalControls.isAllowed(ParentalControls.HEALTH_REPORT);
+ SharedPreferences.forApp().setBoolPref("android.not_a_preference.healthreport.uploadEnabled", isHealthReportEnabled);
+ }
+
+ let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
+ if (sysInfo.get("version") < 16) {
+ let defaults = Services.prefs.getDefaultBranch(null);
+ defaults.setBoolPref("media.autoplay.enabled", false);
+ }
+
+ InitLater(() => {
+ // The order that context menu items are added is important
+ // Make sure the "Open in App" context menu item appears at the bottom of the list
+ this.initContextMenu();
+ ExternalApps.init();
+ }, NativeWindow, "contextmenus");
+
+ if (AppConstants.ACCESSIBILITY) {
+ InitLater(() => AccessFu.attach(window), window, "AccessFu");
+ }
+
+ // Don't delay loading content.js because when we restore reader mode tabs,
+ // we require the reader mode scripts in content.js right away.
+ let mm = window.getGroupMessageManager("browsers");
+ mm.loadFrameScript("chrome://browser/content/content.js", true);
+
+ // We can't delay registering WebChannel listeners: if the first page is
+ // about:accounts, which can happen when starting the Firefox Account flow
+ // from the first run experience, or via the Firefox Account Status
+ // Activity, we can and do miss messages from the fxa-content-server.
+ // However, we never allow suitably restricted profiles from listening to
+ // fxa-content-server messages.
+ if (ParentalControls.isAllowed(ParentalControls.MODIFY_ACCOUNTS)) {
+ console.log("browser.js: loading Firefox Accounts WebChannel");
+ Cu.import("resource://gre/modules/FxAccountsWebChannel.jsm");
+ EnsureFxAccountsWebChannel();
+ } else {
+ console.log("browser.js: not loading Firefox Accounts WebChannel; this profile cannot connect to Firefox Accounts.");
+ }
+
+ // Notify Java that Gecko has loaded.
+ Messaging.sendRequest({ type: "Gecko:Ready" });
+
+ this.deck.addEventListener("DOMContentLoaded", function BrowserApp_delayedStartup() {
+ BrowserApp.deck.removeEventListener("DOMContentLoaded", BrowserApp_delayedStartup, false);
+
+ InitLater(() => Cu.import("resource://gre/modules/NotificationDB.jsm"));
+ InitLater(() => Cu.import("resource://gre/modules/PresentationDeviceInfoManager.jsm"));
+
+ InitLater(() => Services.obs.notifyObservers(window, "browser-delayed-startup-finished", ""));
+ InitLater(() => Messaging.sendRequest({ type: "Gecko:DelayedStartup" }));
+
+ if (!AppConstants.RELEASE_OR_BETA) {
+ InitLater(() => WebcompatReporter.init());
+ }
+
+ // Collect telemetry data.
+ // We do this at startup because we want to move away from "gather-telemetry" (bug 1127907)
+ InitLater(() => {
+ Telemetry.addData("FENNEC_TRACKING_PROTECTION_STATE", parseInt(BrowserApp.getTrackingProtectionState()));
+ Telemetry.addData("ZOOMED_VIEW_ENABLED", Services.prefs.getBoolPref("ui.zoomedview.enabled"));
+ });
+
+ InitLater(() => LightWeightThemeWebInstaller.init());
+ InitLater(() => SpatialNavigation.init(BrowserApp.deck, null), window, "SpatialNavigation");
+ InitLater(() => CastingApps.init(), window, "CastingApps");
+ InitLater(() => Services.search.init(), Services, "search");
+ InitLater(() => DownloadNotifications.init(), window, "DownloadNotifications");
+
+ // Bug 778855 - Perf regression if we do this here. To be addressed in bug 779008.
+ InitLater(() => SafeBrowsing.init(), window, "SafeBrowsing");
+
+ InitLater(() => Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager));
+ InitLater(() => LoginManagerParent.init(), window, "LoginManagerParent");
+
+ }, false);
+
+ // Pass caret StateChanged events to ActionBarHandler.
+ window.addEventListener("mozcaretstatechanged", e => {
+ ActionBarHandler.caretStateChangedHandler(e);
+ }, /* useCapture = */ true, /* wantsUntrusted = */ false);
+ },
+
+ get _startupStatus() {
+ delete this._startupStatus;
+
+ let savedMilestone = null;
+ try {
+ savedMilestone = Services.prefs.getCharPref("browser.startup.homepage_override.mstone");
+ } catch (e) {
+ }
+ let ourMilestone = AppConstants.MOZ_APP_VERSION;
+ this._startupStatus = "";
+ if (ourMilestone != savedMilestone) {
+ Services.prefs.setCharPref("browser.startup.homepage_override.mstone", ourMilestone);
+ this._startupStatus = savedMilestone ? "upgrade" : "new";
+ }
+
+ return this._startupStatus;
+ },
+
+ /**
+ * Pass this a locale string, such as "fr" or "es_ES".
+ */
+ setLocale: function (locale) {
+ console.log("browser.js: requesting locale set: " + locale);
+ Messaging.sendRequest({ type: "Locale:Set", locale: locale });
+ },
+
+ initContextMenu: function () {
+ // We pass a thunk in place of a raw label string. This allows the
+ // context menu to automatically accommodate locale changes without
+ // having to be rebuilt.
+ let stringGetter = name => () => Strings.browser.GetStringFromName(name);
+
+ // TODO: These should eventually move into more appropriate classes
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.openInNewTab"),
+ NativeWindow.contextmenus.linkOpenableNonPrivateContext,
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_open_new_tab");
+ UITelemetry.addEvent("loadurl.1", "contextmenu", null);
+
+ let url = NativeWindow.contextmenus._getLinkURL(aTarget);
+ ContentAreaUtils.urlSecurityCheck(url, aTarget.ownerDocument.nodePrincipal);
+ let tab = 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);
+ let buttonLabel = Strings.browser.GetStringFromName("newtabpopup.switch");
+
+ Snackbars.show(label, Snackbars.LENGTH_LONG, {
+ action: {
+ label: buttonLabel,
+ callback: () => { BrowserApp.selectTab(tab); },
+ }
+ });
+ });
+
+ let showOpenInPrivateTab = true;
+ if ("@mozilla.org/parental-controls-service;1" in Cc) {
+ let pc = Cc["@mozilla.org/parental-controls-service;1"].createInstance(Ci.nsIParentalControlsService);
+ showOpenInPrivateTab = pc.isAllowed(Ci.nsIParentalControlsService.PRIVATE_BROWSING);
+ }
+
+ if (showOpenInPrivateTab) {
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.openInPrivateTab"),
+ NativeWindow.contextmenus.linkOpenableContext,
+ function (aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_open_new_tab");
+ UITelemetry.addEvent("loadurl.1", "contextmenu", null);
+
+ let url = NativeWindow.contextmenus._getLinkURL(aTarget);
+ ContentAreaUtils.urlSecurityCheck(url, aTarget.ownerDocument.nodePrincipal);
+ let tab = BrowserApp.addTab(url, {selected: false, parentId: BrowserApp.selectedTab.id, isPrivate: true});
+
+ let newtabStrings = Strings.browser.GetStringFromName("newprivatetabpopup.opened");
+ let label = PluralForm.get(1, newtabStrings).replace("#1", 1);
+ let buttonLabel = Strings.browser.GetStringFromName("newtabpopup.switch");
+ Snackbars.show(label, Snackbars.LENGTH_LONG, {
+ action: {
+ label: buttonLabel,
+ callback: () => { BrowserApp.selectTab(tab); },
+ }
+ });
+ });
+ }
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.copyLink"),
+ NativeWindow.contextmenus.linkCopyableContext,
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_link");
+
+ let url = NativeWindow.contextmenus._getLinkURL(aTarget);
+ NativeWindow.contextmenus._copyStringToDefaultClipboard(url);
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.copyEmailAddress"),
+ NativeWindow.contextmenus.emailLinkContext,
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_email");
+
+ let url = NativeWindow.contextmenus._getLinkURL(aTarget);
+ let emailAddr = NativeWindow.contextmenus._stripScheme(url);
+ NativeWindow.contextmenus._copyStringToDefaultClipboard(emailAddr);
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.copyPhoneNumber"),
+ NativeWindow.contextmenus.phoneNumberLinkContext,
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_phone");
+
+ let url = NativeWindow.contextmenus._getLinkURL(aTarget);
+ let phoneNumber = NativeWindow.contextmenus._stripScheme(url);
+ NativeWindow.contextmenus._copyStringToDefaultClipboard(phoneNumber);
+ });
+
+ NativeWindow.contextmenus.add({
+ label: stringGetter("contextmenu.shareLink"),
+ order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, // Show above HTML5 menu items
+ selector: NativeWindow.contextmenus._disableRestricted("SHARE", NativeWindow.contextmenus.linkShareableContext),
+ showAsActions: function(aElement) {
+ return {
+ title: aElement.textContent.trim() || aElement.title.trim(),
+ uri: NativeWindow.contextmenus._getLinkURL(aElement),
+ };
+ },
+ icon: "drawable://ic_menu_share",
+ callback: function(aTarget) {
+ // share.1 telemetry is handled in Java via PromptList
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_link");
+ }
+ });
+
+ NativeWindow.contextmenus.add({
+ label: stringGetter("contextmenu.shareEmailAddress"),
+ order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1,
+ selector: NativeWindow.contextmenus._disableRestricted("SHARE", NativeWindow.contextmenus.emailLinkContext),
+ showAsActions: function(aElement) {
+ let url = NativeWindow.contextmenus._getLinkURL(aElement);
+ let emailAddr = NativeWindow.contextmenus._stripScheme(url);
+ let title = aElement.textContent || aElement.title;
+ return {
+ title: title,
+ uri: emailAddr,
+ };
+ },
+ icon: "drawable://ic_menu_share",
+ callback: function(aTarget) {
+ // share.1 telemetry is handled in Java via PromptList
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_email");
+ }
+ });
+
+ NativeWindow.contextmenus.add({
+ label: stringGetter("contextmenu.sharePhoneNumber"),
+ order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1,
+ selector: NativeWindow.contextmenus._disableRestricted("SHARE", NativeWindow.contextmenus.phoneNumberLinkContext),
+ showAsActions: function(aElement) {
+ let url = NativeWindow.contextmenus._getLinkURL(aElement);
+ let phoneNumber = NativeWindow.contextmenus._stripScheme(url);
+ let title = aElement.textContent || aElement.title;
+ return {
+ title: title,
+ uri: phoneNumber,
+ };
+ },
+ icon: "drawable://ic_menu_share",
+ callback: function(aTarget) {
+ // share.1 telemetry is handled in Java via PromptList
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_phone");
+ }
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.addToContacts"),
+ NativeWindow.contextmenus._disableRestricted("ADD_CONTACT", NativeWindow.contextmenus.emailLinkContext),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_contact_email");
+
+ let url = NativeWindow.contextmenus._getLinkURL(aTarget);
+ Messaging.sendRequest({
+ type: "Contact:Add",
+ email: url
+ });
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.addToContacts"),
+ NativeWindow.contextmenus._disableRestricted("ADD_CONTACT", NativeWindow.contextmenus.phoneNumberLinkContext),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_contact_phone");
+
+ let url = NativeWindow.contextmenus._getLinkURL(aTarget);
+ Messaging.sendRequest({
+ type: "Contact:Add",
+ phone: url
+ });
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.bookmarkLink"),
+ NativeWindow.contextmenus._disableRestricted("BOOKMARK", NativeWindow.contextmenus.linkBookmarkableContext),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_bookmark");
+ UITelemetry.addEvent("save.1", "contextmenu", null, "bookmark");
+
+ let url = NativeWindow.contextmenus._getLinkURL(aTarget);
+ let title = aTarget.textContent || aTarget.title || url;
+ Messaging.sendRequest({
+ type: "Bookmark:Insert",
+ url: url,
+ title: title
+ });
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.playMedia"),
+ NativeWindow.contextmenus.mediaContext("media-paused"),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_play");
+ aTarget.play();
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.pauseMedia"),
+ NativeWindow.contextmenus.mediaContext("media-playing"),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_pause");
+ aTarget.pause();
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.showControls2"),
+ NativeWindow.contextmenus.mediaContext("media-hidingcontrols"),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_controls_media");
+ aTarget.setAttribute("controls", true);
+ });
+
+ NativeWindow.contextmenus.add({
+ label: stringGetter("contextmenu.shareMedia"),
+ order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1,
+ selector: NativeWindow.contextmenus._disableRestricted(
+ "SHARE", NativeWindow.contextmenus.videoContext()),
+ showAsActions: function(aElement) {
+ let url = (aElement.currentSrc || aElement.src);
+ let title = aElement.textContent || aElement.title;
+ return {
+ title: title,
+ uri: url,
+ type: "video/*",
+ };
+ },
+ icon: "drawable://ic_menu_share",
+ callback: function(aTarget) {
+ // share.1 telemetry is handled in Java via PromptList
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_media");
+ }
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.fullScreen"),
+ NativeWindow.contextmenus.videoContext("not-fullscreen"),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_fullscreen");
+ aTarget.requestFullscreen();
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.mute"),
+ NativeWindow.contextmenus.mediaContext("media-unmuted"),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_mute");
+ aTarget.muted = true;
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.unmute"),
+ NativeWindow.contextmenus.mediaContext("media-muted"),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_unmute");
+ aTarget.muted = false;
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.viewImage"),
+ NativeWindow.contextmenus.imageLocationCopyableContext,
+ function(aTarget) {
+ let url = aTarget.src;
+ ContentAreaUtils.urlSecurityCheck(url, aTarget.ownerDocument.nodePrincipal,
+ Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
+
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_view_image");
+ UITelemetry.addEvent("loadurl.1", "contextmenu", null);
+ BrowserApp.selectedBrowser.loadURI(url);
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.copyImageLocation"),
+ NativeWindow.contextmenus.imageLocationCopyableContext,
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_image");
+
+ let url = aTarget.src;
+ NativeWindow.contextmenus._copyStringToDefaultClipboard(url);
+ });
+
+ NativeWindow.contextmenus.add({
+ label: stringGetter("contextmenu.shareImage"),
+ selector: NativeWindow.contextmenus._disableRestricted("SHARE", NativeWindow.contextmenus.imageShareableContext),
+ order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, // Show above HTML5 menu items
+ showAsActions: function(aTarget) {
+ let doc = aTarget.ownerDocument;
+ let imageCache = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools)
+ .getImgCacheForDocument(doc);
+ let props = imageCache.findEntryProperties(aTarget.currentURI, doc);
+ let src = aTarget.src;
+ return {
+ title: src,
+ uri: src,
+ type: "image/*",
+ };
+ },
+ icon: "drawable://ic_menu_share",
+ menu: true,
+ callback: function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_image");
+ }
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.saveImage"),
+ NativeWindow.contextmenus.imageSaveableContext,
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_save_image");
+ UITelemetry.addEvent("save.1", "contextmenu", null, "image");
+
+ RuntimePermissions.waitForPermissions(RuntimePermissions.WRITE_EXTERNAL_STORAGE).then(function(permissionGranted) {
+ if (!permissionGranted) {
+ return;
+ }
+
+ ContentAreaUtils.saveImageURL(aTarget.currentURI.spec, null, "SaveImageTitle",
+ false, true, aTarget.ownerDocument.documentURIObject,
+ aTarget.ownerDocument);
+ });
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.setImageAs"),
+ NativeWindow.contextmenus._disableRestricted("SET_IMAGE", NativeWindow.contextmenus.imageSaveableContext),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_background_image");
+
+ let src = aTarget.src;
+ Messaging.sendRequest({
+ type: "Image:SetAs",
+ url: src
+ });
+ });
+
+ NativeWindow.contextmenus.add(
+ function(aTarget) {
+ if (aTarget instanceof HTMLVideoElement) {
+ // If a video element is zero width or height, its essentially
+ // an HTMLAudioElement.
+ if (aTarget.videoWidth == 0 || aTarget.videoHeight == 0 )
+ return Strings.browser.GetStringFromName("contextmenu.saveAudio");
+ return Strings.browser.GetStringFromName("contextmenu.saveVideo");
+ } else if (aTarget instanceof HTMLAudioElement) {
+ return Strings.browser.GetStringFromName("contextmenu.saveAudio");
+ }
+ return Strings.browser.GetStringFromName("contextmenu.saveVideo");
+ }, NativeWindow.contextmenus.mediaSaveableContext,
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_save_media");
+ UITelemetry.addEvent("save.1", "contextmenu", null, "media");
+
+ let url = aTarget.currentSrc || aTarget.src;
+ let filePickerTitleKey = (aTarget instanceof HTMLVideoElement &&
+ (aTarget.videoWidth != 0 && aTarget.videoHeight != 0))
+ ? "SaveVideoTitle" : "SaveAudioTitle";
+ // Skipped trying to pull MIME type out of cache for now
+ ContentAreaUtils.internalSave(url, null, null, null, null, false,
+ filePickerTitleKey, null, aTarget.ownerDocument.documentURIObject,
+ aTarget.ownerDocument, true, null);
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.showImage"),
+ NativeWindow.contextmenus.imageBlockingPolicyContext,
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_show_image");
+ aTarget.setAttribute("data-ctv-show", "true");
+ aTarget.setAttribute("src", aTarget.getAttribute("data-ctv-src"));
+
+ // Shows a snackbar to unblock all images if browser.image_blocking.enabled is enabled.
+ let blockedImgs = aTarget.ownerDocument.querySelectorAll("[data-ctv-src]");
+ if (blockedImgs.length == 0) {
+ return;
+ }
+ let message = Strings.browser.GetStringFromName("imageblocking.downloadedImage");
+ Snackbars.show(message, Snackbars.LENGTH_LONG, {
+ action: {
+ label: Strings.browser.GetStringFromName("imageblocking.showAllImages"),
+ callback: () => {
+ UITelemetry.addEvent("action.1", "toast", null, "web_show_all_image");
+ for (let i = 0; i < blockedImgs.length; ++i) {
+ blockedImgs[i].setAttribute("data-ctv-show", "true");
+ blockedImgs[i].setAttribute("src", blockedImgs[i].getAttribute("data-ctv-src"));
+ }
+ },
+ }
+ });
+ });
+ },
+
+ onAppUpdated: function() {
+ // initialize the form history and passwords databases on upgrades
+ Services.obs.notifyObservers(null, "FormHistory:Init", "");
+ Services.obs.notifyObservers(null, "Passwords:Init", "");
+
+ if (this._startupStatus === "upgrade") {
+ this._migrateUI();
+ }
+ },
+
+ _migrateUI: function() {
+ const UI_VERSION = 3;
+ let currentUIVersion = 0;
+ try {
+ currentUIVersion = Services.prefs.getIntPref("browser.migration.version");
+ } catch(ex) {}
+ if (currentUIVersion >= UI_VERSION) {
+ return;
+ }
+
+ if (currentUIVersion < 1) {
+ // Migrate user-set "plugins.click_to_play" pref. See bug 884694.
+ // Because the default value is true, a user-set pref means that the pref was set to false.
+ if (Services.prefs.prefHasUserValue("plugins.click_to_play")) {
+ Services.prefs.setIntPref("plugin.default.state", Ci.nsIPluginTag.STATE_ENABLED);
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ }
+
+ // Migrate the "privacy.donottrackheader.value" pref. See bug 1042135.
+ if (Services.prefs.prefHasUserValue("privacy.donottrackheader.value")) {
+ // Make sure the doNotTrack value conforms to the conversion from
+ // three-state to two-state. (This reverts a setting of "please track me"
+ // to the default "don't say anything").
+ if (Services.prefs.getBoolPref("privacy.donottrackheader.enabled") &&
+ (Services.prefs.getIntPref("privacy.donottrackheader.value") != 1)) {
+ Services.prefs.clearUserPref("privacy.donottrackheader.enabled");
+ }
+
+ // This pref has been removed, so always clear it.
+ Services.prefs.clearUserPref("privacy.donottrackheader.value");
+ }
+
+ // Set the search activity default pref on app upgrade if it has not been set already.
+ if (!Services.prefs.prefHasUserValue("searchActivity.default.migrated")) {
+ Services.prefs.setBoolPref("searchActivity.default.migrated", true);
+ SearchEngines.migrateSearchActivityDefaultPref();
+ }
+
+ Reader.migrateCache().catch(e => Cu.reportError("Error migrating Reader cache: " + e));
+
+ // We removed this pref from user visible settings, so we should reset it.
+ // Power users can go into about:config to re-enable this if they choose.
+ if (Services.prefs.prefHasUserValue("nglayout.debug.paint_flashing")) {
+ Services.prefs.clearUserPref("nglayout.debug.paint_flashing");
+ }
+ }
+
+ if (currentUIVersion < 2) {
+ let name;
+ if (Services.prefs.prefHasUserValue("browser.search.defaultenginename")) {
+ name = Services.prefs.getCharPref("browser.search.defaultenginename");
+ }
+ if (!name && Services.prefs.prefHasUserValue("browser.search.defaultenginename.US")) {
+ name = Services.prefs.getCharPref("browser.search.defaultenginename.US");
+ }
+ if (name) {
+ Services.search.init(() => {
+ let engine = Services.search.getEngineByName(name);
+ if (engine) {
+ Services.search.defaultEngine = engine;
+ Services.obs.notifyObservers(null, "default-search-engine-migrated", "");
+ }
+ });
+ }
+ }
+
+ if (currentUIVersion < 3) {
+ const kOldSafeBrowsingPref = "browser.safebrowsing.enabled";
+ // Default value is set to true, a user pref means that the pref was
+ // set to false.
+ if (Services.prefs.prefHasUserValue(kOldSafeBrowsingPref) &&
+ !Services.prefs.getBoolPref(kOldSafeBrowsingPref)) {
+ Services.prefs.setBoolPref("browser.safebrowsing.phishing.enabled",
+ false);
+ // Should just remove support for the pref entirely, even if it's
+ // only in about:config
+ Services.prefs.clearUserPref(kOldSafeBrowsingPref);
+ }
+ }
+
+ // Update the migration version.
+ Services.prefs.setIntPref("browser.migration.version", UI_VERSION);
+ },
+
+ // 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() {
+ try {
+ if (!Services.androidBridge.isContentDocumentDisplayed(window))
+ return false;
+ } catch (e) {
+ return false;
+ }
+
+ let tab = this.selectedTab;
+ if (!tab)
+ return false;
+ return tab.contentDocumentIsDisplayed;
+ },
+
+ contentDocumentChanged: function() {
+ window.top.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).isFirstPaint = true;
+ Services.androidBridge.contentDocumentChanged(window);
+ },
+
+ get tabs() {
+ return this._tabs;
+ },
+
+ set selectedTab(aTab) {
+ if (this._selectedTab == aTab)
+ return;
+
+ if (this._selectedTab) {
+ this._selectedTab.setActive(false);
+ }
+
+ this._selectedTab = aTab;
+ if (!aTab)
+ return;
+
+ aTab.setActive(true);
+ this.contentDocumentChanged();
+ this.deck.selectedPanel = aTab.browser;
+ // Focus the browser so that things like selection will be styled correctly.
+ aTab.browser.focus();
+ },
+
+ 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;
+
+ let tab = this.getTabForBrowser(aBrowser);
+ if (tab) {
+ if ("userRequested" in aParams) tab.userRequested = aParams.userRequested;
+ tab.isSearch = ("isSearch" in aParams) ? aParams.isSearch : false;
+ }
+
+ try {
+ aBrowser.loadURIWithFlags(aURI, flags, referrerURI, charset, postData);
+ } catch(e) {
+ if (tab) {
+ let message = {
+ type: "Content:LoadError",
+ tabID: tab.id
+ };
+ Messaging.sendRequest(message);
+ dump("Handled load error: " + e)
+ }
+ }
+ },
+
+ addTab: function addTab(aURI, aParams) {
+ aParams = aParams || {};
+
+ let newTab = new Tab(aURI, aParams);
+
+ if (typeof aParams.tabIndex == "number") {
+ this._tabs.splice(aParams.tabIndex, 0, newTab);
+ } else {
+ this._tabs.push(newTab);
+ }
+
+ let selected = "selected" in aParams ? aParams.selected : true;
+ if (selected)
+ this.selectedTab = newTab;
+
+ let pinned = "pinned" in aParams ? aParams.pinned : false;
+ if (pinned) {
+ let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
+ ss.setTabValue(newTab, "appOrigin", aURI);
+ }
+
+ 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 = {
+ type: "Tab:Close",
+ tabID: aTab.id
+ };
+ Messaging.sendRequest(message);
+ },
+
+ // Calling this will update the state in BrowserApp after a tab has been
+ // closed in the Java UI.
+ _handleTabClosed: function _handleTabClosed(aTab, aShowUndoSnackbar) {
+ if (aTab == this.selectedTab)
+ this.selectedTab = null;
+
+ let tabIndex = this._tabs.indexOf(aTab);
+
+ let evt = document.createEvent("UIEvents");
+ evt.initUIEvent("TabClose", true, false, window, tabIndex);
+ aTab.browser.dispatchEvent(evt);
+
+ if (aShowUndoSnackbar) {
+ // Get a title for the undo close snackbar. Fall back to the URL if there is no title.
+ let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
+ let closedTabData = ss.getClosedTabs(window)[0];
+
+ let message;
+ let title = closedTabData.entries[closedTabData.index - 1].title;
+ let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(aTab.browser);
+
+ if (isPrivate) {
+ message = Strings.browser.GetStringFromName("privateClosedMessage.message");
+ } else if (title) {
+ message = Strings.browser.formatStringFromName("undoCloseToast.message", [title], 1);
+ } else {
+ message = Strings.browser.GetStringFromName("undoCloseToast.messageDefault");
+ }
+
+ Snackbars.show(message, Snackbars.LENGTH_LONG, {
+ action: {
+ label: Strings.browser.GetStringFromName("undoCloseToast.action2"),
+ callback: function() {
+ UITelemetry.addEvent("undo.1", "toast", null, "closetab");
+ ss.undoCloseTab(window, closedTabData);
+ }
+ }
+ });
+ }
+
+ aTab.destroy();
+ this._tabs.splice(tabIndex, 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 doc = this.selectedBrowser.contentDocument;
+ if (doc.fullscreenElement) {
+ // We'll finish the tab selection once the fullscreen transition has ended,
+ // remember the new tab for this.
+ this.fullscreenTransitionTab = aTab;
+ doc.exitFullscreen();
+ }
+
+ let message = {
+ type: "Tab:Select",
+ tabID: aTab.id
+ };
+ Messaging.sendRequest(message);
+ },
+
+ /**
+ * Gets an open tab with the given URL.
+ *
+ * @param aURL URL to look for
+ * @param aOptions Options for the search. Currently supports:
+ ** @option startsWith a Boolean indicating whether to search for a tab who's url starts with the
+ * requested url. Useful if you want to ignore hash codes on the end of a url. For instance
+ * to have about:downloads match about:downloads#123.
+ * @return the tab with the given URL, or null if no such tab exists
+ */
+ getTabWithURL: function getTabWithURL(aURL, aOptions) {
+ aOptions = aOptions || {};
+ let uri = Services.io.newURI(aURL, null, null);
+ for (let i = 0; i < this._tabs.length; ++i) {
+ let tab = this._tabs[i];
+ if (aOptions.startsWith) {
+ if (tab.browser.currentURI.spec.startsWith(aURL)) {
+ return tab;
+ }
+ } else {
+ if (tab.browser.currentURI.equals(uri)) {
+ return tab;
+ }
+ }
+ }
+ return null;
+ },
+
+ /**
+ * If a tab with the given URL already exists, that tab is selected.
+ * Otherwise, a new tab is opened with the given URL.
+ *
+ * @param aURL URL to open
+ * @param aParam Options used if a tab is created
+ * @param aFlags Options for the search. Currently supports:
+ ** @option startsWith a Boolean indicating whether to search for a tab who's url starts with the
+ * requested url. Useful if you want to ignore hash codes on the end of a url. For instance
+ * to have about:downloads match about:downloads#123.
+ */
+ selectOrAddTab: function selectOrAddTab(aURL, aParams, aFlags) {
+ let tab = this.getTabWithURL(aURL, aFlags);
+ if (tab == null) {
+ tab = this.addTab(aURL, aParams);
+ } else {
+ this.selectTab(tab);
+ }
+
+ return tab;
+ },
+
+ // This method updates the state in BrowserApp after a tab has been selected
+ // in the Java UI.
+ _handleTabSelected: function _handleTabSelected(aTab) {
+ if (this.fullscreenTransitionTab) {
+ // Defer updating to "fullscreenchange" if tab selection happened during
+ // a fullscreen transition.
+ return;
+ }
+ this.selectedTab = aTab;
+
+ let evt = document.createEvent("UIEvents");
+ evt.initUIEvent("TabSelect", true, false, window, null);
+ aTab.browser.dispatchEvent(evt);
+ },
+
+ quit: function quit(aClear = { sanitize: {}, dontSaveSession: false }) {
+ // 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", null);
+
+ // Quit aborted.
+ if (cancelQuit.data) {
+ return;
+ }
+
+ Services.obs.notifyObservers(null, "quit-application-proceeding", null);
+
+ // Tell session store to forget about this window
+ if (aClear.dontSaveSession) {
+ let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
+ ss.removeWindow(window);
+ }
+
+ BrowserApp.sanitize(aClear.sanitize, function() {
+ let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup);
+ appStartup.quit(Ci.nsIAppStartup.eForceQuit);
+ }, true);
+ },
+
+ saveAsPDF: function saveAsPDF(aBrowser) {
+ RuntimePermissions.waitForPermissions(RuntimePermissions.WRITE_EXTERNAL_STORAGE).then(function(permissionGranted) {
+ if (!permissionGranted) {
+ return;
+ }
+
+ Task.spawn(function* () {
+ let fileName = ContentAreaUtils.getDefaultFileName(aBrowser.contentTitle, aBrowser.currentURI, null, null);
+ fileName = fileName.trim() + ".pdf";
+
+ let downloadsDir = yield Downloads.getPreferredDownloadsDirectory();
+ let file = OS.Path.join(downloadsDir, fileName);
+
+ // Force this to have a unique name.
+ let openedFile = yield OS.File.openUnique(file, { humanReadable: true });
+ file = openedFile.path;
+ yield openedFile.file.close();
+
+ let download = yield Downloads.createDownload({
+ source: aBrowser.contentWindow,
+ target: file,
+ saver: "pdf",
+ startTime: Date.now(),
+ });
+
+ let list = yield Downloads.getList(download.source.isPrivate ? Downloads.PRIVATE : Downloads.PUBLIC)
+ yield list.add(download);
+ yield download.start();
+ });
+ });
+ },
+
+ // These values come from pref_tracking_protection_entries in arrays.xml.
+ PREF_TRACKING_PROTECTION_ENABLED: "2",
+ PREF_TRACKING_PROTECTION_ENABLED_PB: "1",
+ PREF_TRACKING_PROTECTION_DISABLED: "0",
+
+ /**
+ * Returns the current state of the tracking protection pref.
+ * (0 = Disabled, 1 = Enabled in PB, 2 = Enabled)
+ */
+ getTrackingProtectionState: function() {
+ if (Services.prefs.getBoolPref("privacy.trackingprotection.enabled")) {
+ return this.PREF_TRACKING_PROTECTION_ENABLED;
+ }
+ if (Services.prefs.getBoolPref("privacy.trackingprotection.pbmode.enabled")) {
+ return this.PREF_TRACKING_PROTECTION_ENABLED_PB;
+ }
+ return this.PREF_TRACKING_PROTECTION_DISABLED;
+ },
+
+ sanitize: function (aItems, callback, aShutdown) {
+ let success = true;
+ var promises = [];
+
+ for (let key in aItems) {
+ if (!aItems[key])
+ continue;
+
+ key = key.replace("private.data.", "");
+
+ switch (key) {
+ case "cookies_sessions":
+ promises.push(Sanitizer.clearItem("cookies"));
+ promises.push(Sanitizer.clearItem("sessions"));
+ break;
+ default:
+ promises.push(Sanitizer.clearItem(key));
+ }
+ }
+
+ Promise.all(promises).then(function() {
+ Messaging.sendRequest({
+ type: "Sanitize:Finished",
+ success: true,
+ shutdown: aShutdown === true
+ });
+
+ if (callback) {
+ callback();
+ }
+ }).catch(function(err) {
+ Messaging.sendRequest({
+ type: "Sanitize:Finished",
+ error: err,
+ success: false,
+ shutdown: aShutdown === true
+ });
+
+ if (callback) {
+ callback();
+ }
+ })
+ },
+
+ getFocusedInput: function(aBrowser, aOnlyInputElements = false) {
+ if (!aBrowser)
+ return null;
+
+ let doc = aBrowser.contentDocument;
+ if (!doc)
+ return null;
+
+ let focused = doc.activeElement;
+ while (focused instanceof HTMLFrameElement || focused instanceof HTMLIFrameElement) {
+ doc = focused.contentDocument;
+ focused = doc.activeElement;
+ }
+
+ if (focused instanceof HTMLInputElement &&
+ (focused.mozIsTextField(false) || focused.type === "number")) {
+ return focused;
+ }
+
+ if (aOnlyInputElements)
+ return null;
+
+ if (focused && (focused instanceof HTMLTextAreaElement || focused.isContentEditable)) {
+
+ if (focused instanceof HTMLBodyElement) {
+ // we are putting focus into a contentEditable frame. scroll the frame into
+ // view instead of the contentEditable document contained within, because that
+ // results in a better user experience
+ focused = focused.ownerDocument.defaultView.frameElement;
+ }
+ return focused;
+ }
+ return null;
+ },
+
+ scrollToFocusedInput: function(aBrowser, aAllowZoom = true) {
+ let formHelperMode = Services.prefs.getIntPref("formhelper.mode");
+ if (formHelperMode == kFormHelperModeDisabled)
+ return;
+
+ let dwu = aBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ if (!dwu) {
+ return;
+ }
+
+ let apzFlushDone = function() {
+ Services.obs.removeObserver(apzFlushDone, "apz-repaints-flushed", false);
+ dwu.zoomToFocusedInput();
+ };
+
+ let paintDone = function() {
+ window.removeEventListener("MozAfterPaint", paintDone, false);
+ if (dwu.flushApzRepaints()) {
+ Services.obs.addObserver(apzFlushDone, "apz-repaints-flushed", false);
+ } else {
+ apzFlushDone();
+ }
+ };
+
+ let gotResizeWindow = false;
+ let resizeWindow = function(e) {
+ gotResizeWindow = true;
+ aBrowser.contentWindow.removeEventListener("resize", resizeWindow, false);
+ if (dwu.isMozAfterPaintPending) {
+ window.addEventListener("MozAfterPaint", paintDone, false);
+ } else {
+ paintDone();
+ }
+ }
+
+ aBrowser.contentWindow.addEventListener("resize", resizeWindow, false);
+
+ // The "resize" event sometimes fails to fire, so set a timer to catch that case
+ // and unregister the event listener. See Bug 1253469
+ setTimeout(function(e) {
+ if (!gotResizeWindow) {
+ aBrowser.contentWindow.removeEventListener("resize", resizeWindow, false);
+ dwu.zoomToFocusedInput();
+ }
+ }, 500);
+ },
+
+ getUALocalePref: function () {
+ try {
+ return Services.prefs.getComplexValue("general.useragent.locale", Ci.nsIPrefLocalizedString).data;
+ } catch (e) {
+ try {
+ return Services.prefs.getCharPref("general.useragent.locale");
+ } catch (ee) {
+ return undefined;
+ }
+ }
+ },
+
+ getOSLocalePref: function () {
+ try {
+ return Services.prefs.getCharPref("intl.locale.os");
+ } catch (e) {
+ return undefined;
+ }
+ },
+
+ setLocalizedPref: function (pref, value) {
+ let pls = Cc["@mozilla.org/pref-localizedstring;1"]
+ .createInstance(Ci.nsIPrefLocalizedString);
+ pls.data = value;
+ Services.prefs.setComplexValue(pref, Ci.nsIPrefLocalizedString, pls);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ let browser = this.selectedBrowser;
+
+ switch (aTopic) {
+
+ case "Session:Back":
+ browser.goBack();
+ break;
+
+ case "Session:Forward":
+ browser.goForward();
+ break;
+
+ case "Session:Navigate":
+ let index = JSON.parse(aData);
+ let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
+ let historySize = webNav.sessionHistory.count;
+
+ if (index < 0) {
+ index = 0;
+ Log.e("Browser", "Negative index truncated to zero");
+ } else if (index >= historySize) {
+ Log.e("Browser", "Incorrect index " + index + " truncated to " + historySize - 1);
+ index = historySize - 1;
+ }
+
+ browser.gotoIndex(index);
+ break;
+
+ case "Session:Reload": {
+ let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+
+ // Check to see if this is a message to enable/disable mixed content blocking.
+ if (aData) {
+ let data = JSON.parse(aData);
+
+ if (data.bypassCache) {
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE |
+ Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY;
+ }
+
+ if (data.contentType === "tracking") {
+ // Convert document URI into the format used by
+ // nsChannelClassifier::ShouldEnableTrackingProtection
+ // (any scheme turned into https is correct)
+ let normalizedUrl = Services.io.newURI("https://" + browser.currentURI.hostPort, null, null);
+ if (data.allowContent) {
+ // Add the current host in the 'trackingprotection' consumer of
+ // the permission manager using a normalized URI. This effectively
+ // places this host on the tracking protection white list.
+ if (PrivateBrowsingUtils.isBrowserPrivate(browser)) {
+ PrivateBrowsingUtils.addToTrackingAllowlist(normalizedUrl);
+ } else {
+ Services.perms.add(normalizedUrl, "trackingprotection", Services.perms.ALLOW_ACTION);
+ Telemetry.addData("TRACKING_PROTECTION_EVENTS", 1);
+ }
+ } else {
+ // Remove the current host from the 'trackingprotection' consumer
+ // of the permission manager. This effectively removes this host
+ // from the tracking protection white list (any list actually).
+ if (PrivateBrowsingUtils.isBrowserPrivate(browser)) {
+ PrivateBrowsingUtils.removeFromTrackingAllowlist(normalizedUrl);
+ } else {
+ Services.perms.remove(normalizedUrl, "trackingprotection");
+ Telemetry.addData("TRACKING_PROTECTION_EVENTS", 2);
+ }
+ }
+ }
+ }
+
+ // Try to use the session history to reload so that framesets are
+ // handled properly. If the window has no session history, fall back
+ // to using the web navigation's reload method.
+ let webNav = browser.webNavigation;
+ try {
+ let sh = webNav.sessionHistory;
+ if (sh)
+ webNav = sh.QueryInterface(Ci.nsIWebNavigation);
+ } catch (e) {}
+ webNav.reload(flags);
+ break;
+ }
+
+ case "Session:Stop":
+ browser.stop();
+ break;
+
+ case "Tab:Load": {
+ let data = JSON.parse(aData);
+ let url = data.url;
+ let flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP
+ | Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
+
+ // Pass LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL to prevent any loads from
+ // inheriting the currently loaded document's principal.
+ if (data.userEntered) {
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL;
+ }
+
+ let delayLoad = ("delayLoad" in data) ? data.delayLoad : false;
+ let params = {
+ selected: ("selected" in data) ? data.selected : !delayLoad,
+ parentId: ("parentId" in data) ? data.parentId : -1,
+ flags: flags,
+ tabID: data.tabID,
+ isPrivate: (data.isPrivate === true),
+ pinned: (data.pinned === true),
+ delayLoad: (delayLoad === true),
+ desktopMode: (data.desktopMode === true)
+ };
+
+ params.userRequested = 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;
+ params.isSearch = true;
+ }
+ }
+
+ if (data.newTab) {
+ this.addTab(url, params);
+ } else {
+ if (data.tabId) {
+ // Use a specific browser instead of the selected browser, if it exists
+ let specificBrowser = this.getTabForId(data.tabId).browser;
+ if (specificBrowser)
+ browser = specificBrowser;
+ }
+ this.loadURI(url, browser, params);
+ }
+ break;
+ }
+
+ case "Tab:Selected":
+ this._handleTabSelected(this.getTabForId(parseInt(aData)));
+ break;
+
+ case "Tab:Closed": {
+ let data = JSON.parse(aData);
+ this._handleTabClosed(this.getTabForId(data.tabId), data.showUndoToast);
+ break;
+ }
+
+ case "keyword-search":
+ // This event refers to a search via the URL bar, not a bookmarks
+ // keyword search. Note that this code assumes that the user can only
+ // perform a keyword search on the selected tab.
+ this.selectedTab.isSearch = true;
+
+ // Don't store queries in private browsing mode.
+ let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.selectedTab.browser);
+ let query = isPrivate ? "" : aData;
+
+ let engine = aSubject.QueryInterface(Ci.nsISearchEngine);
+ Messaging.sendRequest({
+ type: "Search:Keyword",
+ identifier: engine.identifier,
+ name: engine.name,
+ query: query
+ });
+ break;
+
+ case "Browser:Quit":
+ // Add-ons like QuitNow and CleanQuit provide aData as an empty-string ("").
+ // Pass undefined to invoke the methods default parms.
+ this.quit(aData ? JSON.parse(aData) : undefined);
+ break;
+
+ case "SaveAs:PDF":
+ this.saveAsPDF(browser);
+ break;
+
+ case "ScrollTo:FocusedInput":
+ // these messages come from a change in the viewable area and not user interaction
+ // we allow scrolling to the selected input, but not zooming the page
+ this.scrollToFocusedInput(browser, false);
+ break;
+
+ case "Sanitize:ClearData":
+ this.sanitize(JSON.parse(aData));
+ break;
+
+ case "FullScreen:Exit":
+ browser.contentDocument.exitFullscreen();
+ break;
+
+ case "Passwords:Init": {
+ let storage = Cc["@mozilla.org/login-manager/storage/mozStorage;1"].
+ getService(Ci.nsILoginManagerStorage);
+ storage.initialize();
+ Services.obs.removeObserver(this, "Passwords:Init");
+ break;
+ }
+
+ case "FormHistory:Init": {
+ // Force creation/upgrade of formhistory.sqlite
+ FormHistory.count({});
+ Services.obs.removeObserver(this, "FormHistory:Init");
+ break;
+ }
+
+ case "android-get-pref": {
+ // These pref names are not "real" pref names. They are used in the
+ // setting menu, and these are passed when initializing the setting
+ // menu. aSubject is a nsIWritableVariant to hold the pref value.
+ aSubject.QueryInterface(Ci.nsIWritableVariant);
+
+ switch (aData) {
+ // The plugin pref is actually two separate prefs, so
+ // we need to handle it differently
+ case "plugin.enable":
+ aSubject.setAsAString(PluginHelper.getPluginPreference());
+ break;
+
+ // Handle master password
+ case "privacy.masterpassword.enabled":
+ aSubject.setAsBool(MasterPassword.enabled);
+ break;
+
+ case "privacy.trackingprotection.state": {
+ aSubject.setAsAString(this.getTrackingProtectionState());
+ break;
+ }
+
+ // Crash reporter submit pref must be fetched from nsICrashReporter
+ // service.
+ case "datareporting.crashreporter.submitEnabled":
+ let crashReporterBuilt = "nsICrashReporter" in Ci &&
+ Services.appinfo instanceof Ci.nsICrashReporter;
+ if (crashReporterBuilt) {
+ aSubject.setAsBool(Services.appinfo.submitReports);
+ }
+ break;
+ }
+ break;
+ }
+
+ case "android-set-pref": {
+ // Pseudo-prefs. aSubject is an nsIWritableVariant that holds the pref
+ // value. Set to empty to signal the pref was handled.
+ aSubject.QueryInterface(Ci.nsIWritableVariant);
+ let value = aSubject.QueryInterface(Ci.nsIVariant);
+
+ switch (aData) {
+ // The plugin pref is actually two separate prefs, so we need to
+ // handle it differently.
+ case "plugin.enable":
+ PluginHelper.setPluginPreference(value);
+ aSubject.setAsEmpty();
+ break;
+
+ // MasterPassword pref is not real, we just need take action and leave
+ case "privacy.masterpassword.enabled":
+ if (MasterPassword.enabled) {
+ MasterPassword.removePassword(value);
+ } else {
+ MasterPassword.setPassword(value);
+ }
+ aSubject.setAsEmpty();
+ break;
+
+ // "privacy.trackingprotection.state" is not a "real" pref name, but
+ // it's used in the setting menu. By default
+ // "privacy.trackingprotection.pbmode.enabled" is true, and
+ // "privacy.trackingprotection.enabled" is false.
+ case "privacy.trackingprotection.state": {
+ switch (value) {
+ // Tracking protection disabled.
+ case this.PREF_TRACKING_PROTECTION_DISABLED:
+ Services.prefs.setBoolPref("privacy.trackingprotection.pbmode.enabled", false);
+ Services.prefs.setBoolPref("privacy.trackingprotection.enabled", false);
+ break;
+ // Tracking protection only in private browsing,
+ case this.PREF_TRACKING_PROTECTION_ENABLED_PB:
+ Services.prefs.setBoolPref("privacy.trackingprotection.pbmode.enabled", true);
+ Services.prefs.setBoolPref("privacy.trackingprotection.enabled", false);
+ break;
+ // Tracking protection everywhere.
+ case this.PREF_TRACKING_PROTECTION_ENABLED:
+ Services.prefs.setBoolPref("privacy.trackingprotection.pbmode.enabled", true);
+ Services.prefs.setBoolPref("privacy.trackingprotection.enabled", true);
+ break;
+ }
+ aSubject.setAsEmpty();
+ break;
+ }
+
+ // Crash reporter preference is in a service; set and return.
+ case "datareporting.crashreporter.submitEnabled":
+ let crashReporterBuilt = "nsICrashReporter" in Ci &&
+ Services.appinfo instanceof Ci.nsICrashReporter;
+ if (crashReporterBuilt) {
+ Services.appinfo.submitReports = value;
+ aSubject.setAsEmpty();
+ }
+ break;
+ }
+ break;
+ }
+
+ case "sessionstore-state-purge-complete":
+ Messaging.sendRequest({ type: "Session:StatePurged" });
+ break;
+
+ case "gather-telemetry":
+ Messaging.sendRequest({ type: "Telemetry:Gather" });
+ break;
+
+ case "Locale:OS":
+ // We know the system locale. We use this for generating Accept-Language headers.
+ console.log("Locale:OS: " + aData);
+ let currentOSLocale = this.getOSLocalePref();
+ if (currentOSLocale == aData) {
+ break;
+ }
+
+ console.log("New OS locale.");
+
+ // Ensure that this choice is immediately persisted, because
+ // Gecko won't be told again if it forgets.
+ Services.prefs.setCharPref("intl.locale.os", aData);
+ Services.prefs.savePrefFile(null);
+
+ let appLocale = this.getUALocalePref();
+
+ this.computeAcceptLanguages(aData, appLocale);
+ break;
+
+ case "Locale:Changed":
+ if (aData) {
+ // The value provided to Locale:Changed should be a BCP47 language tag
+ // understood by Gecko -- for example, "es-ES" or "de".
+ console.log("Locale:Changed: " + aData);
+
+ // We always write a localized pref, even though sometimes the value is a char pref.
+ // (E.g., on desktop single-locale builds.)
+ this.setLocalizedPref("general.useragent.locale", aData);
+ } else {
+ // Resetting.
+ console.log("Switching to system locale.");
+ Services.prefs.clearUserPref("general.useragent.locale");
+ }
+
+ Services.prefs.setBoolPref("intl.locale.matchOS", !aData);
+
+ // Ensure that this choice is immediately persisted, because
+ // Gecko won't be told again if it forgets.
+ Services.prefs.savePrefFile(null);
+
+ // Blow away the string cache so that future lookups get the
+ // correct locale.
+ Strings.flush();
+
+ // Make sure we use the right Accept-Language header.
+ let osLocale;
+ try {
+ // This should never not be set at this point, but better safe than sorry.
+ osLocale = Services.prefs.getCharPref("intl.locale.os");
+ } catch (e) {
+ }
+
+ this.computeAcceptLanguages(osLocale, aData);
+ break;
+
+ case "Fonts:Reload":
+ FontEnumerator.updateFontList();
+ break;
+
+ case "Vibration:Request":
+ if (aSubject instanceof Navigator) {
+ let navigator = aSubject;
+ let buttons = [
+ {
+ label: Strings.browser.GetStringFromName("vibrationRequest.denyButton"),
+ callback: function() {
+ navigator.setVibrationPermission(false);
+ }
+ },
+ {
+ label: Strings.browser.GetStringFromName("vibrationRequest.allowButton"),
+ callback: function() {
+ navigator.setVibrationPermission(true);
+ },
+ positive: true
+ }
+ ];
+ let message = Strings.browser.GetStringFromName("vibrationRequest.message");
+ let options = {};
+ NativeWindow.doorhanger.show(message, "vibration-request", buttons,
+ BrowserApp.selectedTab.id, options, "VIBRATION");
+ }
+ break;
+
+ default:
+ dump('BrowserApp.observe: unexpected topic "' + aTopic + '"\n');
+ break;
+
+ }
+ },
+
+ /**
+ * Set intl.accept_languages accordingly.
+ *
+ * After Bug 881510 this will also accept a real Accept-Language choice as
+ * input; all Accept-Language logic lives here.
+ *
+ * osLocale should never be null, but this method is safe regardless.
+ * appLocale may explicitly be null.
+ */
+ computeAcceptLanguages(osLocale, appLocale) {
+ let defaultBranch = Services.prefs.getDefaultBranch(null);
+ let defaultAccept = defaultBranch.getComplexValue("intl.accept_languages", Ci.nsIPrefLocalizedString).data;
+ console.log("Default intl.accept_languages = " + defaultAccept);
+
+ // A guard for potential breakage. Bug 438031.
+ // This should not be necessary, because we're reading from the default branch,
+ // but better safe than sorry.
+ if (defaultAccept && defaultAccept.startsWith("chrome://")) {
+ defaultAccept = null;
+ } else {
+ // Ensure lowercase everywhere so we can compare.
+ defaultAccept = defaultAccept.toLowerCase();
+ }
+
+ if (appLocale) {
+ appLocale = appLocale.toLowerCase();
+ }
+
+ if (osLocale) {
+ osLocale = osLocale.toLowerCase();
+ }
+
+ // Eliminate values if they're present in the default.
+ let chosen;
+ if (defaultAccept) {
+ // intl.accept_languages is a comma-separated list, with no q-value params. Those
+ // are added when the header is generated.
+ chosen = defaultAccept.split(",")
+ .map(String.trim)
+ .filter((x) => (x != appLocale && x != osLocale));
+ } else {
+ chosen = [];
+ }
+
+ if (osLocale) {
+ chosen.unshift(osLocale);
+ }
+
+ if (appLocale && appLocale != osLocale) {
+ chosen.unshift(appLocale);
+ }
+
+ let result = chosen.join(",");
+ console.log("Setting intl.accept_languages to " + result);
+ this.setLocalizedPref("intl.accept_languages", result);
+ },
+
+ // nsIAndroidBrowserApp
+ get selectedTab() {
+ return this._selectedTab;
+ },
+
+ // nsIAndroidBrowserApp
+ getBrowserTab: function(tabId) {
+ return this.getTabForId(tabId);
+ },
+
+ getUITelemetryObserver: function() {
+ return UITelemetry;
+ },
+
+ // This method will return a list of history items and toIndex based on the action provided from the fromIndex to toIndex,
+ // optionally selecting selIndex (if fromIndex <= selIndex <= toIndex)
+ getHistory: function(data) {
+ let action = data.action;
+ let webNav = BrowserApp.getTabForId(data.tabId).window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
+ let historyIndex = webNav.sessionHistory.index;
+ let historySize = webNav.sessionHistory.count;
+ let canGoBack = webNav.canGoBack;
+ let canGoForward = webNav.canGoForward;
+ let listitems = [];
+ let fromIndex = 0;
+ let toIndex = historySize - 1;
+ let selIndex = historyIndex;
+
+ if (action == "BACK" && canGoBack) {
+ fromIndex = Math.max(historyIndex - kMaxHistoryListSize, 0);
+ toIndex = historyIndex;
+ selIndex = historyIndex;
+ } else if (action == "FORWARD" && canGoForward) {
+ fromIndex = historyIndex;
+ toIndex = Math.min(historySize - 1, historyIndex + kMaxHistoryListSize);
+ selIndex = historyIndex;
+ } else if (action == "ALL" && (canGoBack || canGoForward)){
+ fromIndex = historyIndex - kMaxHistoryListSize / 2;
+ toIndex = historyIndex + kMaxHistoryListSize / 2;
+ if (fromIndex < 0) {
+ toIndex -= fromIndex;
+ }
+
+ if (toIndex > historySize - 1) {
+ fromIndex -= toIndex - (historySize - 1);
+ toIndex = historySize - 1;
+ }
+
+ fromIndex = Math.max(fromIndex, 0);
+ selIndex = historyIndex;
+ } else {
+ // return empty list immediately.
+ return {
+ "historyItems": listitems,
+ "toIndex": toIndex
+ };
+ }
+
+ let browser = this.selectedBrowser;
+ let hist = browser.sessionHistory;
+ for (let i = toIndex; i >= fromIndex; i--) {
+ let entry = hist.getEntryAtIndex(i, false);
+ let item = {
+ title: entry.title || entry.URI.spec,
+ url: entry.URI.spec,
+ selected: (i == selIndex)
+ };
+ listitems.push(item);
+ }
+
+ return {
+ "historyItems": listitems,
+ "toIndex": toIndex
+ };
+ },
+};
+
+var NativeWindow = {
+ init: function() {
+ Services.obs.addObserver(this, "Menu:Clicked", false);
+ Services.obs.addObserver(this, "Doorhanger:Reply", false);
+ this.contextmenus.init();
+ },
+
+ loadDex: function(zipFile, implClass) {
+ Messaging.sendRequest({
+ type: "Dex:Load",
+ zipfile: zipFile,
+ impl: implClass || "Main"
+ });
+ },
+
+ unloadDex: function(zipFile) {
+ Messaging.sendRequest({
+ type: "Dex:Unload",
+ zipfile: zipFile
+ });
+ },
+
+ menu: {
+ _callbacks: [],
+ _menuId: 1,
+ toolsMenuID: -1,
+ add: function() {
+ let options;
+ if (arguments.length == 1) {
+ options = arguments[0];
+ } else if (arguments.length == 3) {
+ Log.w("Browser", "This menu addon API has been deprecated. Instead, use the options object API.");
+ options = {
+ name: arguments[0],
+ callback: arguments[2]
+ };
+ } else {
+ throw "Incorrect number of parameters";
+ }
+
+ options.type = "Menu:Add";
+ options.id = this._menuId;
+
+ Messaging.sendRequest(options);
+ this._callbacks[this._menuId] = options.callback;
+ this._menuId++;
+ return this._menuId - 1;
+ },
+
+ remove: function(aId) {
+ Messaging.sendRequest({ type: "Menu:Remove", id: aId });
+ },
+
+ update: function(aId, aOptions) {
+ if (!aOptions)
+ return;
+
+ Messaging.sendRequest({
+ type: "Menu:Update",
+ id: aId,
+ options: aOptions
+ });
+ }
+ },
+
+ 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.
+ * persistWhileVisible:
+ * A boolean. If true, a visible notification will always
+ * persist across location changes.
+ * timeout: A time in milliseconds. The notification will not
+ * automatically dismiss before this time.
+ *
+ * checkbox: A string to appear next to a checkbox under the notification
+ * message. The button callback functions will be called with
+ * the checked state as an argument.
+ *
+ * actionText: An object that specifies a clickable string, a type of action,
+ * and a bundle blob for the consumer to create a click action.
+ * { text: <text>,
+ * type: <type>,
+ * bundle: <blob-object> }
+ *
+ * @param aCategory
+ * Doorhanger type to display (e.g., LOGIN)
+ */
+ show: function(aMessage, aValue, aButtons, aTabID, aOptions, aCategory) {
+ if (aButtons == null) {
+ aButtons = [];
+ }
+
+ if (aButtons.length > 2) {
+ console.log("Doorhanger can have a maximum of two buttons!");
+ aButtons.length = 2;
+ }
+
+ 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 = {
+ type: "Doorhanger:Add",
+ message: aMessage,
+ value: aValue,
+ buttons: aButtons,
+ // use the current tab if none is provided
+ tabID: aTabID || BrowserApp.selectedTab.id,
+ options: aOptions || {},
+ category: aCategory
+ };
+ Messaging.sendRequest(json);
+ },
+
+ hide: function(aValue, aTabID) {
+ Messaging.sendRequest({
+ 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 data = JSON.parse(aData);
+ let reply_id = data["callback"];
+
+ if (this.doorhanger._callbacks[reply_id]) {
+ // Pass the value of the optional checkbox to the callback
+ let checked = data["checked"];
+ this.doorhanger._callbacks[reply_id].cb(checked, data.inputs);
+
+ let prompt = this.doorhanger._callbacks[reply_id].prompt;
+ 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
+ DEFAULT_HTML5_ORDER: -1, // Sort order for HTML5 context menu items
+
+ init: function() {
+ // Accessing "NativeWindow.contextmenus" initializes context menus if needed.
+ BrowserApp.deck.addEventListener(
+ "contextmenu", (e) => NativeWindow.contextmenus.show(e), false);
+ },
+
+ add: function() {
+ let args;
+ if (arguments.length == 1) {
+ args = arguments[0];
+ } else if (arguments.length == 3) {
+ args = {
+ label : arguments[0],
+ selector: arguments[1],
+ callback: arguments[2]
+ };
+ } else {
+ throw "Incorrect number of parameters";
+ }
+
+ if (!args.label)
+ throw "Menu items must have a name";
+
+ let cmItem = new ContextMenuItem(args);
+ this.items[cmItem.id] = cmItem;
+ return cmItem.id;
+ },
+
+ remove: function(aId) {
+ delete this.items[aId];
+ },
+
+ // Although we do not use this ourselves anymore, add-ons may still
+ // need it as it has been documented, so we shouldn't remove it.
+ SelectorContext: function(aSelector) {
+ return {
+ matches: function(aElt) {
+ if (aElt.matches)
+ return aElt.matches(aSelector);
+ return false;
+ }
+ };
+ },
+
+ linkOpenableNonPrivateContext: {
+ matches: function linkOpenableNonPrivateContextMatches(aElement) {
+ let doc = aElement.ownerDocument;
+ if (!doc || PrivateBrowsingUtils.isContentWindowPrivate(doc.defaultView)) {
+ return false;
+ }
+
+ return NativeWindow.contextmenus.linkOpenableContext.matches(aElement);
+ }
+ },
+
+ linkOpenableContext: {
+ matches: function linkOpenableContextMatches(aElement) {
+ let uri = NativeWindow.contextmenus._getLink(aElement);
+ if (uri) {
+ let scheme = uri.scheme;
+ let dontOpen = /^(javascript|mailto|news|snews|tel)$/;
+ return (scheme && !dontOpen.test(scheme));
+ }
+ return false;
+ }
+ },
+
+ linkCopyableContext: {
+ matches: function linkCopyableContextMatches(aElement) {
+ let uri = NativeWindow.contextmenus._getLink(aElement);
+ if (uri) {
+ let scheme = uri.scheme;
+ let dontCopy = /^(mailto|tel)$/;
+ return (scheme && !dontCopy.test(scheme));
+ }
+ return false;
+ }
+ },
+
+ linkShareableContext: {
+ matches: function linkShareableContextMatches(aElement) {
+ let uri = NativeWindow.contextmenus._getLink(aElement);
+ if (uri) {
+ let scheme = uri.scheme;
+ let dontShare = /^(about|chrome|file|javascript|mailto|resource|tel)$/;
+ 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|tel)$/;
+ return (scheme && !dontBookmark.test(scheme));
+ }
+ return false;
+ }
+ },
+
+ emailLinkContext: {
+ matches: function emailLinkContextMatches(aElement) {
+ let uri = NativeWindow.contextmenus._getLink(aElement);
+ if (uri)
+ return uri.schemeIs("mailto");
+ return false;
+ }
+ },
+
+ phoneNumberLinkContext: {
+ matches: function phoneNumberLinkContextMatches(aElement) {
+ let uri = NativeWindow.contextmenus._getLink(aElement);
+ if (uri)
+ return uri.schemeIs("tel");
+ return false;
+ }
+ },
+
+ imageLocationCopyableContext: {
+ matches: function imageLinkCopyableContextMatches(aElement) {
+ if (aElement instanceof Ci.nsIDOMHTMLImageElement) {
+ // The image is blocked by Tap-to-load Images
+ if (aElement.hasAttribute("data-ctv-src") && !aElement.hasAttribute("data-ctv-show")) {
+ return false;
+ }
+ }
+ return (aElement instanceof Ci.nsIImageLoadingContent && aElement.currentURI);
+ }
+ },
+
+ imageSaveableContext: {
+ matches: function imageSaveableContextMatches(aElement) {
+ if (aElement instanceof Ci.nsIDOMHTMLImageElement) {
+ // The image is blocked by Tap-to-load Images
+ if (aElement.hasAttribute("data-ctv-src") && !aElement.hasAttribute("data-ctv-show")) {
+ return false;
+ }
+ }
+ if (aElement instanceof Ci.nsIImageLoadingContent && aElement.currentURI) {
+ // The image must be loaded to allow saving
+ let request = aElement.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
+ return (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE));
+ }
+ return false;
+ }
+ },
+
+ imageShareableContext: {
+ matches: function imageShareableContextMatches(aElement) {
+ let imgSrc = '';
+ if (aElement instanceof Ci.nsIDOMHTMLImageElement) {
+ imgSrc = aElement.src;
+ } else if (aElement instanceof Ci.nsIImageLoadingContent &&
+ aElement.currentURI &&
+ aElement.currentURI.spec) {
+ imgSrc = aElement.currentURI.spec;
+ }
+
+ // In order to share an image, we need to pass the image src over IPC via an Intent (in
+ // `ApplicationPackageManager.queryIntentActivities`). However, the transaction has a 1MB limit
+ // (shared by all transactions in progress) - otherwise we crash! (bug 1243305)
+ // https://developer.android.com/reference/android/os/TransactionTooLargeException.html
+ //
+ // The transaction limit is 1MB and we arbitrarily choose to cap this transaction at 1/4 of that = 250,000 bytes.
+ // In Java, a UTF-8 character is 1-4 bytes so, 250,000 bytes / 4 bytes/char = 62,500 char
+ let MAX_IMG_SRC_LEN = 62500;
+ let isTooLong = imgSrc.length >= MAX_IMG_SRC_LEN;
+ return !isTooLong && this.NativeWindow.contextmenus.imageSaveableContext.matches(aElement);
+ }.bind(this)
+ },
+
+ mediaSaveableContext: {
+ matches: function mediaSaveableContextMatches(aElement) {
+ return (aElement instanceof HTMLVideoElement ||
+ aElement instanceof HTMLAudioElement);
+ }
+ },
+
+ imageBlockingPolicyContext: {
+ matches: function imageBlockingPolicyContextMatches(aElement) {
+ if (aElement instanceof Ci.nsIDOMHTMLImageElement && aElement.getAttribute("data-ctv-src")) {
+ // Only show the menuitem if we are blocking the image
+ if (aElement.getAttribute("data-ctv-show") == "true") {
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+ },
+
+ mediaContext: function(aMode) {
+ return {
+ matches: function(aElt) {
+ if (aElt instanceof Ci.nsIDOMHTMLMediaElement) {
+ let hasError = aElt.error != null || aElt.networkState == aElt.NETWORK_NO_SOURCE;
+ if (hasError)
+ return false;
+
+ let paused = aElt.paused || aElt.ended;
+ if (paused && aMode == "media-paused")
+ return true;
+ if (!paused && aMode == "media-playing")
+ return true;
+ let controls = aElt.controls;
+ if (!controls && aMode == "media-hidingcontrols")
+ return true;
+
+ let muted = aElt.muted;
+ if (muted && aMode == "media-muted")
+ return true;
+ else if (!muted && aMode == "media-unmuted")
+ return true;
+ }
+ return false;
+ }
+ };
+ },
+
+ videoContext: function(aMode) {
+ return {
+ matches: function(aElt) {
+ if (aElt instanceof HTMLVideoElement) {
+ if (!aMode) {
+ return true;
+ }
+ var isFullscreen = aElt.ownerDocument.fullscreenElement == aElt;
+ if (aMode == "not-fullscreen") {
+ return !isFullscreen;
+ }
+ if (aMode == "fullscreen") {
+ return isFullscreen;
+ }
+ }
+ return false;
+ }
+ };
+ },
+
+ /* Holds a WeakRef to the original target element this context menu was shown for.
+ * Most API's will have to walk up the tree from this node to find the correct element
+ * to act on
+ */
+ get _target() {
+ if (this._targetRef)
+ return this._targetRef.get();
+ return null;
+ },
+
+ set _target(aTarget) {
+ if (aTarget)
+ this._targetRef = Cu.getWeakReference(aTarget);
+ else this._targetRef = null;
+ },
+
+ get defaultContext() {
+ delete this.defaultContext;
+ return this.defaultContext = Strings.browser.GetStringFromName("browser.menu.context.default");
+ },
+
+ /* Gets menuitems for an arbitrary node
+ * Parameters:
+ * element - The element to look at. If this element has a contextmenu attribute, the
+ * corresponding contextmenu will be used.
+ */
+ _getHTMLContextMenuItemsForElement: function(element) {
+ let htmlMenu = element.contextMenu;
+ if (!htmlMenu) {
+ return [];
+ }
+
+ htmlMenu.QueryInterface(Components.interfaces.nsIHTMLMenu);
+ htmlMenu.sendShowEvent();
+
+ return this._getHTMLContextMenuItemsForMenu(htmlMenu, element);
+ },
+
+ /* Add a menuitem for an HTML <menu> node
+ * Parameters:
+ * menu - The <menu> element to iterate through for menuitems
+ * target - The target element these context menu items are attached to
+ */
+ _getHTMLContextMenuItemsForMenu: function(menu, target) {
+ let items = [];
+ for (let i = 0; i < menu.childNodes.length; i++) {
+ let elt = menu.childNodes[i];
+ if (!elt.label)
+ continue;
+
+ items.push(new HTMLContextMenuItem(elt, target));
+ }
+
+ return items;
+ },
+
+ // Searches the current list of menuitems to show for any that match this id
+ _findMenuItem: function(aId) {
+ if (!this.menus) {
+ return null;
+ }
+
+ for (let context in this.menus) {
+ let menu = this.menus[context];
+ for (let i = 0; i < menu.length; i++) {
+ if (menu[i].id === aId) {
+ return menu[i];
+ }
+ }
+ }
+ return null;
+ },
+
+ // Returns true if there are any context menu items to show
+ _shouldShow: function() {
+ for (let context in this.menus) {
+ let menu = this.menus[context];
+ if (menu.length > 0) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /* Returns a label to be shown in a tabbed ui if there are multiple "contexts". For instance, if this
+ * is an image inside an <a> tag, we may have a "link" context and an "image" one.
+ */
+ _getContextType: function(element) {
+ // For anchor nodes, we try to use the scheme to pick a string
+ if (element instanceof Ci.nsIDOMHTMLAnchorElement) {
+ let uri = this.makeURI(this._getLinkURL(element));
+ try {
+ return Strings.browser.GetStringFromName("browser.menu.context." + uri.scheme);
+ } catch(ex) { }
+ }
+
+ // Otherwise we try the nodeName
+ try {
+ return Strings.browser.GetStringFromName("browser.menu.context." + element.nodeName.toLowerCase());
+ } catch(ex) { }
+
+ // Fallback to the default
+ return this.defaultContext;
+ },
+
+ // Adds context menu items added through the add-on api
+ _getNativeContextMenuItems: function(element, x, y) {
+ let res = [];
+ for (let itemId of Object.keys(this.items)) {
+ let item = this.items[itemId];
+
+ if (!this._findMenuItem(item.id) && item.matches(element, x, y)) {
+ res.push(item);
+ }
+ }
+
+ return res;
+ },
+
+ /* Checks if there are context menu items to show, and if it finds them
+ * sends a contextmenu event to content. We also send showing events to
+ * any html5 context menus we are about to show, and fire some local notifications
+ * for chrome consumers to do lazy menuitem construction
+ */
+ show: function(event) {
+ // Android Long-press / contextmenu event provides clientX/Y data. This is not provided
+ // by mochitest: test_browserElement_inproc_ContextmenuEvents.html.
+ if (!event.clientX || !event.clientY) {
+ return;
+ }
+
+ // If the event was already defaultPrevented by somebody (web content, or
+ // some other part of gecko), then don't do anything with it.
+ if (event.defaultPrevented) {
+ return;
+ }
+
+ // Use the highlighted element for the context menu target. When accessibility is
+ // enabled, elements may not be highlighted so use the event target instead.
+ this._target = BrowserEventHandler._highlightElement || event.target;
+ if (!this._target) {
+ return;
+ }
+
+ // Try to build a list of contextmenu items. If successful, actually show the
+ // native context menu by passing the list to Java.
+ this._buildMenu(event.clientX, event.clientY);
+ if (this._shouldShow()) {
+ BrowserEventHandler._cancelTapHighlight();
+
+ // Consume / preventDefault the event, and show the contextmenu.
+ event.preventDefault();
+ this._innerShow(this._target, event.clientX, event.clientY);
+ this._target = null;
+
+ return;
+ }
+
+ // If no context-menu for long-press event, it may be meant to trigger text-selection.
+ this.menus = null;
+ Services.obs.notifyObservers(
+ {target: this._target, x: event.clientX, y: event.clientY}, "context-menu-not-shown", "");
+ },
+
+ // Returns a title for a context menu. If no title attribute exists, will fall back to looking for a url
+ _getTitle: function(node) {
+ if (node.hasAttribute && node.hasAttribute("title")) {
+ return node.getAttribute("title");
+ }
+ return this._getUrl(node);
+ },
+
+ // Returns a url associated with a node
+ _getUrl: function(node) {
+ if ((node instanceof Ci.nsIDOMHTMLAnchorElement && node.href) ||
+ (node instanceof Ci.nsIDOMHTMLAreaElement && node.href)) {
+ return this._getLinkURL(node);
+ } else if (node instanceof Ci.nsIImageLoadingContent && node.currentURI) {
+ // The image is blocked by Tap-to-load Images
+ let originalURL = node.getAttribute("data-ctv-src");
+ if (originalURL) {
+ return originalURL;
+ }
+ return node.currentURI.spec;
+ } else if (node instanceof Ci.nsIDOMHTMLMediaElement) {
+ let srcUrl = node.currentSrc || node.src;
+ // If URL prepended with blob or mediasource, we'll remove it.
+ return srcUrl.replace(/^(?:blob|mediasource):/, '');
+ }
+
+ return "";
+ },
+
+ // Adds an array of menuitems to the current list of items to show, in the correct context
+ _addMenuItems: function(items, context) {
+ if (!this.menus[context]) {
+ this.menus[context] = [];
+ }
+ this.menus[context] = this.menus[context].concat(items);
+ },
+
+ /* Does the basic work of building a context menu to show. Will combine HTML and Native
+ * context menus items, as well as sorting menuitems into different menus based on context.
+ */
+ _buildMenu: function(x, y) {
+ // now walk up the tree and for each node look for any context menu items that apply
+ let element = this._target;
+
+ // this.menus holds a hashmap of "contexts" to menuitems associated with that context
+ // For instance, if the user taps an image inside a link, we'll have something like:
+ // {
+ // link: [ ContextMenuItem, ContextMenuItem ]
+ // image: [ ContextMenuItem, ContextMenuItem ]
+ // }
+ this.menus = {};
+
+ while (element) {
+ let context = this._getContextType(element);
+
+ // First check for any html5 context menus that might exist...
+ var items = this._getHTMLContextMenuItemsForElement(element);
+ if (items.length > 0) {
+ this._addMenuItems(items, context);
+ }
+
+ // then check for any context menu items registered in the ui.
+ items = this._getNativeContextMenuItems(element, x, y);
+ if (items.length > 0) {
+ this._addMenuItems(items, context);
+ }
+
+ // walk up the tree and find more items to show
+ element = element.parentNode;
+ }
+ },
+
+ // Walks the DOM tree to find a title from a node
+ _findTitle: function(node) {
+ let title = "";
+ while(node && !title) {
+ title = this._getTitle(node);
+ node = node.parentNode;
+ }
+ return title;
+ },
+
+ /* Reformats the list of menus to show into an object that can be sent to Prompt.jsm
+ * If there is one menu, will return a flat array of menuitems. If there are multiple
+ * menus, will return an array with appropriate tabs/items inside it. i.e. :
+ * [
+ * { label: "link", items: [...] },
+ * { label: "image", items: [...] }
+ * ]
+ */
+ _reformatList: function(target) {
+ let contexts = Object.keys(this.menus);
+
+ if (contexts.length === 1) {
+ // If there's only one context, we'll only show a single flat single select list
+ return this._reformatMenuItems(target, this.menus[contexts[0]]);
+ }
+
+ // If there are multiple contexts, we'll only show a tabbed ui with multiple lists
+ return this._reformatListAsTabs(target, this.menus);
+ },
+
+ /* Reformats the list of menus to show into an object that can be sent to Prompt.jsm's
+ * addTabs method. i.e. :
+ * { link: [...], image: [...] } becomes
+ * [ { label: "link", items: [...] } ]
+ *
+ * Also reformats items and resolves any parmaeters that aren't known until display time
+ * (for instance Helper app menu items adjust their title to reflect what Helper App can be used for this link).
+ */
+ _reformatListAsTabs: function(target, menus) {
+ let itemArray = [];
+
+ // Sort the keys so that "link" is always first
+ let contexts = Object.keys(this.menus);
+ contexts.sort((context1, context2) => {
+ if (context1 === this.defaultContext) {
+ return -1;
+ } else if (context2 === this.defaultContext) {
+ return 1;
+ }
+ return 0;
+ });
+
+ contexts.forEach(context => {
+ itemArray.push({
+ label: context,
+ items: this._reformatMenuItems(target, menus[context])
+ });
+ });
+
+ return itemArray;
+ },
+
+ /* Reformats an array of ContextMenuItems into an array that can be handled by Prompt.jsm. Also reformats items
+ * and resolves any parmaeters that aren't known until display time
+ * (for instance Helper app menu items adjust their title to reflect what Helper App can be used for this link).
+ */
+ _reformatMenuItems: function(target, menuitems) {
+ let itemArray = [];
+
+ for (let i = 0; i < menuitems.length; i++) {
+ let t = target;
+ while(t) {
+ if (menuitems[i].matches(t)) {
+ let val = menuitems[i].getValue(t);
+
+ // hidden menu items will return null from getValue
+ if (val) {
+ itemArray.push(val);
+ break;
+ }
+ }
+
+ t = t.parentNode;
+ }
+ }
+
+ return itemArray;
+ },
+
+ // Called where we're finally ready to actually show the contextmenu. Sorts the items and shows a prompt.
+ _innerShow: function(target, x, y) {
+ Haptic.performSimpleAction(Haptic.LongPress);
+
+ // spin through the tree looking for a title for this context menu
+ let title = this._findTitle(target);
+
+ for (let context in this.menus) {
+ let menu = this.menus[context];
+ menu.sort((a,b) => {
+ if (a.order === b.order) {
+ return 0;
+ }
+ return (a.order > b.order) ? 1 : -1;
+ });
+ }
+
+ let useTabs = Object.keys(this.menus).length > 1;
+ let prompt = new Prompt({
+ window: target.ownerDocument.defaultView,
+ title: useTabs ? undefined : title
+ });
+
+ let items = this._reformatList(target);
+ if (useTabs) {
+ prompt.addTabs({
+ id: "tabs",
+ items: items
+ });
+ } else {
+ prompt.setSingleChoiceItems(items);
+ }
+
+ prompt.show(this._promptDone.bind(this, target, x, y, items));
+ },
+
+ // Called when the contextmenu prompt is closed
+ _promptDone: function(target, x, y, items, data) {
+ if (data.button == -1) {
+ // Prompt was cancelled, or an ActionView was used.
+ return;
+ }
+
+ let selectedItemId;
+ if (data.tabs) {
+ let menu = items[data.tabs.tab];
+ selectedItemId = menu.items[data.tabs.item].id;
+ } else {
+ selectedItemId = items[data.list[0]].id
+ }
+
+ let selectedItem = this._findMenuItem(selectedItemId);
+ this.menus = null;
+
+ if (!selectedItem || !selectedItem.matches || !selectedItem.callback) {
+ return;
+ }
+
+ // for menuitems added using the native UI, pass the dom element that matched that item to the callback
+ while (target) {
+ if (selectedItem.matches(target, x, y)) {
+ selectedItem.callback(target, x, y);
+ break;
+ }
+ target = target.parentNode;
+ }
+ },
+
+ // 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 = this._getLinkURL(aElement);
+ return Services.io.newURI(url, null, null);
+ } catch (e) {}
+ }
+ return null;
+ },
+
+ _disableRestricted: function _disableRestricted(restriction, selector) {
+ return {
+ matches: function _disableRestrictedMatches(aElement, aX, aY) {
+ if (!ParentalControls.isAllowed(ParentalControls[restriction])) {
+ return false;
+ }
+
+ return selector.matches(aElement, aX, aY);
+ }
+ };
+ },
+
+ _getLinkURL: function ch_getLinkURL(aLink) {
+ let href = aLink.href;
+ if (href)
+ return href;
+
+ href = aLink.getAttribute("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);
+ },
+
+ _copyStringToDefaultClipboard: function(aString) {
+ let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
+ clipboard.copyString(aString);
+ Snackbars.show(Strings.browser.GetStringFromName("selectionHelper.textCopied"), Snackbars.LENGTH_LONG);
+ },
+
+ _stripScheme: function(aString) {
+ let index = aString.indexOf(":");
+ return aString.slice(index + 1);
+ }
+ }
+};
+
+XPCOMUtils.defineLazyModuleGetter(this, "PageActions",
+ "resource://gre/modules/PageActions.jsm");
+
+// These alias to the old, deprecated NativeWindow interfaces
+[
+ ["pageactions", "resource://gre/modules/PageActions.jsm", "PageActions"],
+ ["toast", "resource://gre/modules/Snackbars.jsm", "Snackbars"]
+].forEach(item => {
+ let [name, script, exprt] = item;
+
+ XPCOMUtils.defineLazyGetter(NativeWindow, name, () => {
+ var err = Strings.browser.formatStringFromName("nativeWindow.deprecated", ["NativeWindow." + name, script], 2);
+ Cu.reportError(err);
+
+ let sandbox = {};
+ Cu.import(script, sandbox);
+ return sandbox[exprt];
+ });
+});
+
+var LightWeightThemeWebInstaller = {
+ init: function sh_init() {
+ let temp = {};
+ Cu.import("resource://gre/modules/LightweightThemeConsumer.jsm", temp);
+ let theme = new temp.LightweightThemeConsumer(document);
+ BrowserApp.deck.addEventListener("InstallBrowserTheme", this, false, true);
+ BrowserApp.deck.addEventListener("PreviewBrowserTheme", this, false, true);
+ BrowserApp.deck.addEventListener("ResetBrowserThemePreview", this, false, true);
+
+ if (ParentalControls.parentalControlsEnabled &&
+ !this._manager.currentTheme &&
+ ParentalControls.isAllowed(ParentalControls.DEFAULT_THEME)) {
+ // We are using the DEFAULT_THEME restriction to differentiate between restricted profiles & guest mode - Bug 1199596
+ this._installParentalControlsTheme();
+ }
+ },
+
+ handleEvent: function (event) {
+ switch (event.type) {
+ case "InstallBrowserTheme":
+ case "PreviewBrowserTheme":
+ case "ResetBrowserThemePreview":
+ // ignore requests from background tabs
+ if (event.target.ownerDocument.defaultView.top != content)
+ return;
+ }
+
+ switch (event.type) {
+ case "InstallBrowserTheme":
+ this._installRequest(event);
+ break;
+ case "PreviewBrowserTheme":
+ this._preview(event);
+ break;
+ case "ResetBrowserThemePreview":
+ this._resetPreview(event);
+ break;
+ case "pagehide":
+ case "TabSelect":
+ this._resetPreview();
+ break;
+ }
+ },
+
+ get _manager () {
+ let temp = {};
+ Cu.import("resource://gre/modules/LightweightThemeManager.jsm", temp);
+ delete this._manager;
+ return this._manager = temp.LightweightThemeManager;
+ },
+
+ _installParentalControlsTheme: function() {
+ let mgr = this._manager;
+ let parentalControlsTheme = {
+ "headerURL": "resource://android/assets/parental_controls_theme.png",
+ "name": "Parental Controls Theme",
+ "id": "parental-controls-theme@mozilla.org"
+ };
+
+ mgr.addBuiltInTheme(parentalControlsTheme);
+ mgr.themeChanged(parentalControlsTheme);
+ },
+
+ _installRequest: function (event) {
+ let node = event.target;
+ let data = this._getThemeFromNode(node);
+ if (!data)
+ return;
+
+ if (this._isAllowed(node)) {
+ this._install(data);
+ return;
+ }
+
+ let allowButtonText = Strings.browser.GetStringFromName("lwthemeInstallRequest.allowButton");
+ let message = Strings.browser.formatStringFromName("lwthemeInstallRequest.message", [node.ownerDocument.location.hostname], 1);
+ let buttons = [{
+ label: allowButtonText,
+ callback: function () {
+ LightWeightThemeWebInstaller._install(data);
+ },
+ positive: true
+ }];
+
+ NativeWindow.doorhanger.show(message, "Personas", buttons, BrowserApp.selectedTab.id);
+ },
+
+ _install: function (newLWTheme) {
+ this._manager.currentTheme = newLWTheme;
+ },
+
+ _previewWindow: null,
+ _preview: function (event) {
+ if (!this._isAllowed(event.target))
+ return;
+ let data = this._getThemeFromNode(event.target);
+ if (!data)
+ return;
+ this._resetPreview();
+
+ this._previewWindow = event.target.ownerDocument.defaultView;
+ this._previewWindow.addEventListener("pagehide", this, true);
+ BrowserApp.deck.addEventListener("TabSelect", this, false);
+ this._manager.previewTheme(data);
+ },
+
+ _resetPreview: function (event) {
+ if (!this._previewWindow ||
+ event && !this._isAllowed(event.target))
+ return;
+
+ this._previewWindow.removeEventListener("pagehide", this, true);
+ this._previewWindow = null;
+ BrowserApp.deck.removeEventListener("TabSelect", this, false);
+
+ this._manager.resetPreview();
+ },
+
+ _isAllowed: function (node) {
+ // Make sure the whitelist has been imported to permissions
+ PermissionsUtils.importFromPrefs("xpinstall.", "install");
+
+ let pm = Services.perms;
+
+ let uri = node.ownerDocument.documentURIObject;
+ if (!uri.schemeIs("https")) {
+ return false;
+ }
+
+ return pm.testPermission(uri, "install") == pm.ALLOW_ACTION;
+ },
+
+ _getThemeFromNode: function (node) {
+ return this._manager.parseTheme(node.getAttribute("data-browsertheme"), node.baseURI);
+ }
+};
+
+var DesktopUserAgent = {
+ DESKTOP_UA: null,
+ TCO_DOMAIN: "t.co",
+ TCO_REPLACE: / Gecko.*/,
+
+ init: function ua_init() {
+ Services.obs.addObserver(this, "DesktopMode:Change", false);
+ UserAgentOverrides.addComplexOverride(this.onRequest.bind(this));
+
+ // See https://developer.mozilla.org/en/Gecko_user_agent_string_reference
+ this.DESKTOP_UA = Cc["@mozilla.org/network/protocol;1?name=http"]
+ .getService(Ci.nsIHttpProtocolHandler).userAgent
+ .replace(/Android \d.+?; [a-zA-Z]+/, "X11; Linux x86_64")
+ .replace(/Gecko\/[0-9\.]+/, "Gecko/20100101");
+ },
+
+ onRequest: function(channel, defaultUA) {
+ if (AppConstants.NIGHTLY_BUILD && this.TCO_DOMAIN == channel.URI.host) {
+ // Force the referrer
+ channel.referrer = channel.URI;
+
+ // Send a bot-like UA to t.co to get a real redirect. We strip off the
+ // "Gecko/x.y Firefox/x.y" part
+ return defaultUA.replace(this.TCO_REPLACE, "");
+ }
+
+ let channelWindow = this._getWindowForRequest(channel);
+ let tab = BrowserApp.getTabForWindow(channelWindow);
+ if (tab) {
+ return this.getUserAgentForTab(tab);
+ }
+
+ return null;
+ },
+
+ getUserAgentForWindow: function ua_getUserAgentForWindow(aWindow) {
+ let tab = BrowserApp.getTabForWindow(aWindow.top);
+ if (tab) {
+ return this.getUserAgentForTab(tab);
+ }
+
+ return null;
+ },
+
+ getUserAgentForTab: function ua_getUserAgentForTab(aTab) {
+ // Send desktop UA if "Request Desktop Site" is enabled.
+ if (aTab.desktopMode) {
+ return this.DESKTOP_UA;
+ }
+
+ return null;
+ },
+
+ _getRequestLoadContext: function ua_getRequestLoadContext(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 ua_getWindowForRequest(aRequest) {
+ let loadContext = this._getRequestLoadContext(aRequest);
+ if (loadContext) {
+ try {
+ return loadContext.associatedWindow;
+ } catch (e) {
+ // loadContext.associatedWindow can throw when there's no window
+ }
+ }
+ return null;
+ },
+
+ observe: function ua_observe(aSubject, aTopic, aData) {
+ if (aTopic === "DesktopMode:Change") {
+ let args = JSON.parse(aData);
+ let tab = BrowserApp.getTabForId(args.tabId);
+ if (tab) {
+ tab.reloadWithMode(args.desktopMode);
+ }
+ }
+ }
+};
+
+
+function nsBrowserAccess() {
+}
+
+nsBrowserAccess.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserDOMWindow]),
+
+ _getBrowser: function _getBrowser(aURI, aOpener, aWhere, aFlags) {
+ let isExternal = !!(aFlags & 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) {
+ if (isExternal) {
+ aWhere = Services.prefs.getIntPref("browser.link.open_external");
+ } else {
+ 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 ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
+ let pinned = false;
+
+ if (aURI && aWhere == Ci.nsIBrowserDOMWindow.OPEN_SWITCHTAB) {
+ pinned = true;
+ let spec = aURI.spec;
+ let tabs = BrowserApp.tabs;
+ for (let i = 0; i < tabs.length; i++) {
+ let appOrigin = ss.getTabValue(tabs[i], "appOrigin");
+ if (appOrigin == spec) {
+ let tab = tabs[i];
+ BrowserApp.selectTab(tab);
+ return tab.browser;
+ }
+ }
+ }
+
+ // If OPEN_SWITCHTAB was not handled above, we need to open a new tab,
+ // along with other OPEN_ values that create a new tab.
+ let newTab = (aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW ||
+ aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWTAB ||
+ aWhere == Ci.nsIBrowserDOMWindow.OPEN_SWITCHTAB);
+ let isPrivate = false;
+
+ if (newTab) {
+ let parentId = -1;
+ if (!isExternal && aOpener) {
+ let parent = BrowserApp.getTabForWindow(aOpener.top);
+ if (parent) {
+ parentId = parent.id;
+ isPrivate = PrivateBrowsingUtils.isBrowserPrivate(parent.browser);
+ }
+ }
+
+ let openerWindow = (aFlags & Ci.nsIBrowserDOMWindow.OPEN_NO_OPENER) ? null : aOpener;
+ // 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,
+ opener: openerWindow,
+ selected: true,
+ isPrivate: isPrivate,
+ pinned: pinned });
+
+ 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, aFlags) {
+ let browser = this._getBrowser(aURI, aOpener, aWhere, aFlags);
+ return browser ? browser.contentWindow : null;
+ },
+
+ openURIInFrame: function browser_openURIInFrame(aURI, aParams, aWhere, aFlags) {
+ let browser = this._getBrowser(aURI, null, aWhere, aFlags);
+ return browser ? browser.QueryInterface(Ci.nsIFrameLoaderOwner) : null;
+ },
+
+ isTabContentWindow: function(aWindow) {
+ return BrowserApp.getBrowserForWindow(aWindow) != null;
+ },
+
+ canClose() {
+ return BrowserUtils.canCloseWindow(window);
+ },
+};
+
+
+function Tab(aURL, aParams) {
+ this.filter = null;
+ this.browser = null;
+ this.id = 0;
+ this.lastTouchedAt = Date.now();
+ this._zoom = 1.0;
+ this._drawZoom = 1.0;
+ this._restoreZoom = false;
+ this.userScrollPos = { x: 0, y: 0 };
+ this.contentDocumentIsDisplayed = true;
+ this.pluginDoorhangerTimeout = null;
+ this.shouldShowPluginDoorhanger = true;
+ this.clickToPlayPluginsActivated = false;
+ this.desktopMode = false;
+ this.originalURI = null;
+ this.hasTouchListener = false;
+ this.playingAudio = false;
+
+ this.create(aURL, aParams);
+}
+
+/*
+ * Sanity limit for URIs passed to UI code.
+ *
+ * 2000 is the typical industry limit, largely due to older IE versions.
+ *
+ * We use 25000, so we'll allow almost any value through.
+ *
+ * Still, this truncation doesn't affect history, so this is only a practical
+ * concern in two ways: the truncated value is used when editing URIs, and as
+ * the key for favicon fetches.
+ */
+const MAX_URI_LENGTH = 25000;
+
+/*
+ * Similar restriction for titles. This is only a display concern.
+ */
+const MAX_TITLE_LENGTH = 255;
+
+/**
+ * Ensure that a string is of a sane length.
+ */
+function truncate(text, max) {
+ if (!text || !max) {
+ return text;
+ }
+
+ if (text.length <= max) {
+ return text;
+ }
+
+ return text.slice(0, max) + "…";
+}
+
+Tab.prototype = {
+ create: function(aURL, aParams) {
+ if (this.browser)
+ return;
+
+ aParams = aParams || {};
+
+ this.browser = document.createElement("browser");
+ this.browser.setAttribute("type", "content-targetable");
+ this.browser.setAttribute("messagemanagergroup", "browsers");
+
+ if (Preferences.get("browser.tabs.remote.force-enable", false)) {
+ this.browser.setAttribute("remote", "true");
+ }
+
+ this.browser.permanentKey = {};
+
+ // Check if we have a "parent" window which we need to set as our opener
+ if ("opener" in aParams) {
+ this.browser.presetOpenerWindow(aParams.opener);
+ }
+
+ // Make sure the previously selected panel remains selected. The selected panel of a deck is
+ // not stable when panels are added.
+ let selectedPanel = BrowserApp.deck.selectedPanel;
+ BrowserApp.deck.insertBefore(this.browser, aParams.sibling || null);
+ BrowserApp.deck.selectedPanel = selectedPanel;
+
+ let attrs = {};
+ if (BrowserApp.manifestUrl) {
+ let appsService = Cc["@mozilla.org/AppsService;1"].getService(Ci.nsIAppsService);
+ let manifest = appsService.getAppByManifestURL(BrowserApp.manifestUrl);
+ if (manifest) {
+ let app = manifest.QueryInterface(Ci.mozIApplication);
+ this.browser.docShell.frameType = Ci.nsIDocShell.FRAME_TYPE_APP;
+ attrs['appId'] = app.localId;
+ }
+ }
+
+ // Must be called after appendChild so the docShell has been created.
+ this.setActive(false);
+
+ let isPrivate = ("isPrivate" in aParams) && aParams.isPrivate;
+ if (isPrivate) {
+ attrs['privateBrowsingId'] = 1;
+ }
+
+ this.browser.docShell.setOriginAttributes(attrs);
+
+ // Set the new docShell load flags based on network state.
+ if (Tabs.useCache) {
+ this.browser.docShell.defaultLoadFlags |= Ci.nsIRequest.LOAD_FROM_CACHE;
+ }
+
+ this.browser.stop();
+
+ // Only set tab uri if uri is valid
+ let uri = null;
+ let title = aParams.title || aURL;
+ try {
+ uri = Services.io.newURI(aURL, null, null).spec;
+ } catch (e) {}
+
+ // When the tab is stubbed from Java, there's a window between the stub
+ // creation and the tab creation in Gecko where the stub could be removed
+ // or the selected tab can change (which is easiest to hit during startup).
+ // To prevent these races, we need to differentiate between tab stubs from
+ // Java and new tabs from Gecko.
+ let stub = false;
+
+ if (!aParams.zombifying) {
+ if ("tabID" in aParams) {
+ this.id = aParams.tabID;
+ stub = true;
+ } else {
+ let jenv = JNI.GetForThread();
+ let jTabs = JNI.LoadClass(jenv, "org.mozilla.gecko.Tabs", {
+ static_methods: [
+ { name: "getNextTabId", sig: "()I" }
+ ],
+ });
+ this.id = jTabs.getNextTabId();
+ JNI.UnloadClasses(jenv);
+ }
+
+ this.desktopMode = ("desktopMode" in aParams) ? aParams.desktopMode : false;
+
+ let message = {
+ type: "Tab:Added",
+ tabID: this.id,
+ uri: truncate(uri, MAX_URI_LENGTH),
+ parentId: ("parentId" in aParams) ? aParams.parentId : -1,
+ tabIndex: ("tabIndex" in aParams) ? aParams.tabIndex : -1,
+ external: ("external" in aParams) ? aParams.external : false,
+ selected: ("selected" in aParams || aParams.cancelEditMode === true) ? aParams.selected : true,
+ cancelEditMode: aParams.cancelEditMode === true,
+ title: truncate(title, MAX_TITLE_LENGTH),
+ delayLoad: aParams.delayLoad || false,
+ desktopMode: this.desktopMode,
+ isPrivate: isPrivate,
+ stub: stub
+ };
+ Messaging.sendRequest(message);
+ }
+
+ let flags = Ci.nsIWebProgress.NOTIFY_STATE_ALL |
+ Ci.nsIWebProgress.NOTIFY_LOCATION |
+ Ci.nsIWebProgress.NOTIFY_SECURITY;
+ this.filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"].createInstance(Ci.nsIWebProgress);
+ this.filter.addProgressListener(this, flags)
+ this.browser.addProgressListener(this.filter, flags);
+ this.browser.sessionHistory.addSHistoryListener(this);
+
+ this.browser.addEventListener("DOMContentLoaded", this, true);
+ this.browser.addEventListener("DOMFormHasPassword", this, true);
+ this.browser.addEventListener("DOMInputPasswordAdded", this, true);
+ this.browser.addEventListener("DOMLinkAdded", this, true);
+ this.browser.addEventListener("DOMLinkChanged", this, true);
+ this.browser.addEventListener("DOMMetaAdded", this, false);
+ this.browser.addEventListener("DOMTitleChanged", this, true);
+ this.browser.addEventListener("DOMAudioPlaybackStarted", this, true);
+ this.browser.addEventListener("DOMAudioPlaybackStopped", this, true);
+ this.browser.addEventListener("DOMWindowClose", this, true);
+ this.browser.addEventListener("DOMWillOpenModalDialog", this, true);
+ this.browser.addEventListener("DOMAutoComplete", this, true);
+ this.browser.addEventListener("blur", this, true);
+ this.browser.addEventListener("pageshow", this, true);
+ this.browser.addEventListener("MozApplicationManifest", this, true);
+ this.browser.addEventListener("TabPreZombify", this, true);
+
+ // Note that the XBL binding is untrusted
+ this.browser.addEventListener("PluginBindingAttached", this, true, true);
+ this.browser.addEventListener("VideoBindingAttached", this, true, true);
+ this.browser.addEventListener("VideoBindingCast", this, true, true);
+
+ Services.obs.addObserver(this, "before-first-paint", false);
+ Services.obs.addObserver(this, "media-playback", false);
+ Services.obs.addObserver(this, "media-playback-resumed", false);
+
+ // Always intialise new tabs with basic session store data to avoid
+ // problems with functions that always expect it to be present
+ this.browser.__SS_data = {
+ entries: [{
+ url: aURL,
+ title: truncate(title, MAX_TITLE_LENGTH)
+ }],
+ index: 1,
+ desktopMode: this.desktopMode,
+ isPrivate: isPrivate
+ };
+
+ if (aParams.delayLoad) {
+ // If this is a zombie tab, mark the browser for delay loading, which will
+ // restore the tab when selected using the session data added above
+ this.browser.__SS_restore = true;
+ } else {
+ 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;
+
+ // The search term the user entered to load the current URL
+ this.userRequested = "userRequested" in aParams ? aParams.userRequested : "";
+ this.isSearch = "isSearch" in aParams ? aParams.isSearch : false;
+
+ try {
+ this.browser.loadURIWithFlags(aURL, flags, referrerURI, charset, postData);
+ } catch(e) {
+ let message = {
+ type: "Content:LoadError",
+ tabID: this.id
+ };
+ Messaging.sendRequest(message);
+ dump("Handled load error: " + e);
+ }
+ }
+ },
+
+ /**
+ * Reloads the tab with the desktop mode setting.
+ */
+ reloadWithMode: function (aDesktopMode) {
+ // notify desktopmode for PIDOMWindow
+ let win = this.browser.contentWindow;
+ let dwi = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ dwi.setDesktopModeViewport(aDesktopMode);
+
+ // Set desktop mode for tab and send change to Java
+ if (this.desktopMode != aDesktopMode) {
+ this.desktopMode = aDesktopMode;
+ Messaging.sendRequest({
+ type: "DesktopMode:Changed",
+ desktopMode: aDesktopMode,
+ tabID: this.id
+ });
+ }
+
+ // Only reload the page for http/https schemes
+ let currentURI = this.browser.currentURI;
+ if (!currentURI.schemeIs("http") && !currentURI.schemeIs("https"))
+ return;
+
+ let url = currentURI.spec;
+ // We need LOAD_FLAGS_BYPASS_CACHE here since we're changing the User-Agent
+ // string, and servers typically don't use the Vary: User-Agent header, so
+ // not doing this means that we'd get some of the previously cached content.
+ let flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE |
+ Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY;
+ if (this.originalURI && !this.originalURI.equals(currentURI)) {
+ // We were redirected; reload the original URL
+ url = this.originalURI.spec;
+ }
+
+ this.browser.docShell.loadURI(url, flags, null, null, null);
+ },
+
+ destroy: function() {
+ if (!this.browser)
+ return;
+
+ this.browser.removeProgressListener(this.filter);
+ this.filter.removeProgressListener(this);
+ this.filter = null;
+ this.browser.sessionHistory.removeSHistoryListener(this);
+
+ this.browser.removeEventListener("DOMContentLoaded", this, true);
+ this.browser.removeEventListener("DOMFormHasPassword", this, true);
+ this.browser.removeEventListener("DOMInputPasswordAdded", this, true);
+ this.browser.removeEventListener("DOMLinkAdded", this, true);
+ this.browser.removeEventListener("DOMLinkChanged", this, true);
+ this.browser.removeEventListener("DOMMetaAdded", this, false);
+ this.browser.removeEventListener("DOMTitleChanged", this, true);
+ this.browser.removeEventListener("DOMAudioPlaybackStarted", this, true);
+ this.browser.removeEventListener("DOMAudioPlaybackStopped", this, true);
+ this.browser.removeEventListener("DOMWindowClose", this, true);
+ this.browser.removeEventListener("DOMWillOpenModalDialog", this, true);
+ this.browser.removeEventListener("DOMAutoComplete", this, true);
+ this.browser.removeEventListener("blur", this, true);
+ this.browser.removeEventListener("pageshow", this, true);
+ this.browser.removeEventListener("MozApplicationManifest", this, true);
+ this.browser.removeEventListener("TabPreZombify", this, true);
+
+ this.browser.removeEventListener("PluginBindingAttached", this, true, true);
+ this.browser.removeEventListener("VideoBindingAttached", this, true, true);
+ this.browser.removeEventListener("VideoBindingCast", this, true, true);
+
+ Services.obs.removeObserver(this, "before-first-paint");
+ Services.obs.removeObserver(this, "media-playback", false);
+ Services.obs.removeObserver(this, "media-playback-resumed", false);
+
+ // 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 || !this.browser.docShell)
+ return;
+
+ this.lastTouchedAt = Date.now();
+
+ if (aActive) {
+ this.browser.setAttribute("type", "content-primary");
+ this.browser.focus();
+ this.browser.docShellIsActive = true;
+ Reader.updatePageAction(this);
+ ExternalApps.updatePageAction(this.browser.currentURI, this.browser.contentDocument);
+ } else {
+ this.browser.setAttribute("type", "content-targetable");
+ this.browser.docShellIsActive = false;
+ this.browser.blur();
+ }
+ },
+
+ getActive: function getActive() {
+ return this.browser.docShellIsActive;
+ },
+
+ // These constants are used to prioritize high quality metadata over low quality data, so that
+ // we can collect data as we find meta tags, and replace low quality metadata with higher quality
+ // matches. For instance a msApplicationTile icon is a better tile image than an og:image tag.
+ METADATA_GOOD_MATCH: 10,
+ METADATA_NORMAL_MATCH: 1,
+
+ addMetadata: function(type, value, quality = 1) {
+ if (!this.metatags) {
+ this.metatags = {
+ url: this.browser.currentURI.specIgnoringRef
+ };
+ }
+
+ if (type == "touchIconList") {
+ if (!this.metatags['touchIconList']) {
+ this.metatags['touchIconList'] = {};
+ }
+ this.metatags.touchIconList[quality] = value;
+ } else if (!this.metatags[type] || this.metatags[type + "_quality"] < quality) {
+ this.metatags[type] = value;
+ this.metatags[type + "_quality"] = quality;
+ }
+ },
+
+ sanitizeRelString: function(linkRel) {
+ // Sanitize the rel string
+ let list = [];
+ if (linkRel) {
+ list = linkRel.toLowerCase().split(/\s+/);
+ let hash = {};
+ list.forEach(function(value) { hash[value] = true; });
+ list = [];
+ for (let rel in hash)
+ list.push("[" + rel + "]");
+ }
+ return list;
+ },
+
+ makeFaviconMessage: function(eventTarget) {
+ // We want to get the largest icon size possible for our UI.
+ let maxSize = 0;
+
+ // We use the sizes attribute if available
+ // see http://www.whatwg.org/specs/web-apps/current-work/multipage/links.html#rel-icon
+ if (eventTarget.hasAttribute("sizes")) {
+ let sizes = eventTarget.getAttribute("sizes").toLowerCase();
+
+ if (sizes == "any") {
+ // Since Java expects an integer, use -1 to represent icons with sizes="any"
+ maxSize = -1;
+ } else {
+ let tokens = sizes.split(" ");
+ tokens.forEach(function(token) {
+ // TODO: check for invalid tokens
+ let [w, h] = token.split("x");
+ maxSize = Math.max(maxSize, Math.max(w, h));
+ });
+ }
+ }
+ return {
+ type: "Link:Favicon",
+ tabID: this.id,
+ href: resolveGeckoURI(eventTarget.href),
+ size: maxSize,
+ mime: eventTarget.getAttribute("type") || ""
+ };
+ },
+
+ makeFeedMessage: function(eventTarget, targetType) {
+ try {
+ // urlSecurityCeck will throw if things are not OK
+ ContentAreaUtils.urlSecurityCheck(eventTarget.href,
+ eventTarget.ownerDocument.nodePrincipal,
+ Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
+
+ if (!this.browser.feeds)
+ this.browser.feeds = [];
+
+ this.browser.feeds.push({
+ href: eventTarget.href,
+ title: eventTarget.title,
+ type: targetType
+ });
+
+ return {
+ type: "Link:Feed",
+ tabID: this.id
+ };
+ } catch (e) {
+ return null;
+ }
+ },
+
+ sendOpenSearchMessage: function(eventTarget) {
+ let type = eventTarget.type && eventTarget.type.toLowerCase();
+ // Replace all starting or trailing spaces or spaces before "*;" globally w/ "".
+ type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
+
+ // Check that type matches opensearch.
+ let isOpenSearch = (type == "application/opensearchdescription+xml");
+ if (isOpenSearch && eventTarget.title && /^(?:https?|ftp):/i.test(eventTarget.href)) {
+ Services.search.init(() => {
+ let visibleEngines = Services.search.getVisibleEngines();
+ // NOTE: Engines are currently identified by name, but this can be changed
+ // when Engines are identified by URL (see bug 335102).
+ if (visibleEngines.some(function(e) {
+ return e.name == eventTarget.title;
+ })) {
+ // This engine is already present, do nothing.
+ return null;
+ }
+
+ if (this.browser.engines) {
+ // This engine has already been handled, do nothing.
+ if (this.browser.engines.some(function(e) {
+ return e.url == eventTarget.href;
+ })) {
+ return null;
+ }
+ } else {
+ this.browser.engines = [];
+ }
+
+ // Get favicon.
+ let iconURL = eventTarget.ownerDocument.documentURIObject.prePath + "/favicon.ico";
+
+ let newEngine = {
+ title: eventTarget.title,
+ url: eventTarget.href,
+ iconURL: iconURL
+ };
+
+ this.browser.engines.push(newEngine);
+
+ // Don't send a message to display engines if we've already handled an engine.
+ if (this.browser.engines.length > 1)
+ return null;
+
+ // Broadcast message that this tab contains search engines that should be visible.
+ Messaging.sendRequest({
+ type: "Link:OpenSearch",
+ tabID: this.id,
+ visible: true
+ });
+ });
+ }
+ },
+
+ handleEvent: function(aEvent) {
+ switch (aEvent.type) {
+ case "DOMContentLoaded": {
+ let target = aEvent.originalTarget;
+
+ // ignore on frames and other documents
+ if (target != this.browser.contentDocument)
+ 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 { contentDocument, contentWindow } = this.browser;
+ let computedStyle = contentWindow.getComputedStyle(contentDocument.body);
+ backgroundColor = computedStyle.backgroundColor;
+ } catch (e) {
+ // Ignore. Catching and ignoring exceptions here ensures that Talos succeeds.
+ }
+
+ let docURI = target.documentURI;
+ let errorType = "";
+ if (docURI.startsWith("about:certerror")) {
+ errorType = "certerror";
+ }
+ else if (docURI.startsWith("about:blocked")) {
+ errorType = "blocked";
+ }
+ else if (docURI.startsWith("about:neterror")) {
+ let error = docURI.search(/e\=/);
+ let duffUrl = docURI.search(/\&u\=/);
+ let errorExtra = decodeURIComponent(docURI.slice(error + 2, duffUrl));
+ // Here is a list of errorExtra types (et_*)
+ // http://mxr.mozilla.org/mozilla-central/source/mobile/android/chrome/content/netError.xhtml#287
+ UITelemetry.addEvent("neterror.1", "content", null, errorExtra);
+ errorType = "neterror";
+ }
+
+ // 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 (docURI.startsWith("about:neterror")) {
+ NetErrorHelper.attachToBrowser(this.browser);
+ }
+
+ Messaging.sendRequest({
+ type: "DOMContentLoaded",
+ tabID: this.id,
+ bgColor: backgroundColor,
+ errorType: errorType,
+ metadata: this.metatags,
+ });
+
+ // Reset isSearch so that the userRequested term will be erased on next page load
+ this.metatags = null;
+
+ if (docURI.startsWith("about:certerror") || docURI.startsWith("about:blocked")) {
+ this.browser.addEventListener("click", ErrorPageEventHandler, true);
+ let listener = function() {
+ this.browser.removeEventListener("click", ErrorPageEventHandler, true);
+ this.browser.removeEventListener("pagehide", listener, true);
+ }.bind(this);
+
+ this.browser.addEventListener("pagehide", listener, true);
+ }
+
+ if (AppConstants.NIGHTLY_BUILD || AppConstants.MOZ_ANDROID_ACTIVITY_STREAM) {
+ WebsiteMetadata.parseAsynchronously(this.browser.contentDocument);
+ }
+
+ break;
+ }
+
+ case "DOMFormHasPassword": {
+ LoginManagerContent.onDOMFormHasPassword(aEvent,
+ this.browser.contentWindow);
+
+ // Send logins for this hostname to Java.
+ let hostname = aEvent.target.baseURIObject.prePath;
+ let foundLogins = Services.logins.findLogins({}, hostname, "", "");
+ if (foundLogins.length > 0) {
+ let displayHost = IdentityHandler.getEffectiveHost();
+ let title = { text: displayHost, resource: hostname };
+ let selectObj = { title: title, logins: foundLogins };
+ Messaging.sendRequest({ type: "Doorhanger:Logins", data: selectObj });
+ }
+ break;
+ }
+
+ case "DOMInputPasswordAdded": {
+ LoginManagerContent.onDOMInputPasswordAdded(aEvent,
+ this.browser.contentWindow);
+ }
+
+ case "DOMMetaAdded":
+ let target = aEvent.originalTarget;
+ let browser = BrowserApp.getBrowserForDocument(target.ownerDocument);
+
+ switch (target.name) {
+ case "msapplication-TileImage":
+ this.addMetadata("tileImage", browser.currentURI.resolve(target.content), this.METADATA_GOOD_MATCH);
+ break;
+ case "msapplication-TileColor":
+ this.addMetadata("tileColor", target.content, this.METADATA_GOOD_MATCH);
+ break;
+ }
+
+ break;
+
+ case "DOMLinkAdded":
+ case "DOMLinkChanged": {
+ let jsonMessage = null;
+ let target = aEvent.originalTarget;
+ if (!target.href || target.disabled)
+ return;
+
+ // Ignore on frames and other documents
+ if (target.ownerDocument != this.browser.contentDocument)
+ return;
+
+ // Sanitize rel link
+ let list = this.sanitizeRelString(target.rel);
+ if (list.indexOf("[icon]") != -1) {
+ jsonMessage = this.makeFaviconMessage(target);
+ } else if (list.indexOf("[apple-touch-icon]") != -1 ||
+ list.indexOf("[apple-touch-icon-precomposed]") != -1) {
+ jsonMessage = this.makeFaviconMessage(target);
+ jsonMessage['type'] = 'Link:Touchicon';
+ this.addMetadata("touchIconList", jsonMessage.href, jsonMessage.size);
+ } else if (list.indexOf("[alternate]") != -1 && aEvent.type == "DOMLinkAdded") {
+ let type = target.type.toLowerCase().replace(/^\s+|\s*(?:;.*)?$/g, "");
+ let isFeed = (type == "application/rss+xml" || type == "application/atom+xml");
+
+ if (!isFeed)
+ return;
+
+ jsonMessage = this.makeFeedMessage(target, type);
+ } else if (list.indexOf("[search]") != -1 && aEvent.type == "DOMLinkAdded") {
+ this.sendOpenSearchMessage(target);
+ }
+ if (!jsonMessage)
+ return;
+
+ Messaging.sendRequest(jsonMessage);
+ break;
+ }
+
+ case "DOMTitleChanged": {
+ if (!aEvent.isTrusted)
+ return;
+
+ // ignore on frames and other documents
+ if (aEvent.originalTarget != this.browser.contentDocument)
+ return;
+
+ Messaging.sendRequest({
+ type: "DOMTitleChanged",
+ tabID: this.id,
+ title: truncate(aEvent.target.title, MAX_TITLE_LENGTH)
+ });
+ break;
+ }
+
+ case "TabPreZombify": {
+ if (!this.playingAudio) {
+ return;
+ }
+ // Fall through to the DOMAudioPlayback events, so the
+ // audio playback indicator gets reset upon zombification.
+ }
+ case "DOMAudioPlaybackStarted":
+ case "DOMAudioPlaybackStopped": {
+ if (!Services.prefs.getBoolPref("browser.tabs.showAudioPlayingIcon") ||
+ !aEvent.isTrusted) {
+ return;
+ }
+
+ let browser = aEvent.originalTarget;
+ if (browser != this.browser) {
+ return;
+ }
+
+ this.playingAudio = aEvent.type === "DOMAudioPlaybackStarted";
+
+ Messaging.sendRequest({
+ type: "Tab:AudioPlayingChange",
+ tabID: this.id,
+ isAudioPlaying: this.playingAudio
+ });
+ return;
+ }
+
+ case "DOMWindowClose": {
+ if (!aEvent.isTrusted)
+ return;
+
+ // Find the relevant tab, and close it from Java
+ if (this.browser.contentWindow == aEvent.target) {
+ aEvent.preventDefault();
+
+ Messaging.sendRequest({
+ type: "Tab:Close",
+ 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 "DOMAutoComplete":
+ case "blur": {
+ LoginManagerContent.onUsernameInput(aEvent);
+ break;
+ }
+
+ case "PluginBindingAttached": {
+ PluginHelper.handlePluginBindingAttached(this, aEvent);
+ break;
+ }
+
+ case "VideoBindingAttached": {
+ CastingApps.handleVideoBindingAttached(this, aEvent);
+ break;
+ }
+
+ case "VideoBindingCast": {
+ CastingApps.handleVideoBindingCast(this, aEvent);
+ break;
+ }
+
+ case "MozApplicationManifest": {
+ OfflineApps.offlineAppRequested(aEvent.originalTarget.defaultView);
+ break;
+ }
+
+ case "pageshow": {
+ LoginManagerContent.onPageShow(aEvent, this.browser.contentWindow);
+
+ // The rest of this only handles pageshow for the top-level document.
+ if (aEvent.originalTarget.defaultView != this.browser.contentWindow)
+ return;
+
+ let target = aEvent.originalTarget;
+ let docURI = target.documentURI;
+ if (!docURI.startsWith("about:neterror") && !this.isSearch) {
+ // If this wasn't an error page and the user isn't search, don't retain the typed entry
+ this.userRequested = "";
+ }
+
+ Messaging.sendRequest({
+ type: "Content:PageShow",
+ tabID: this.id,
+ userRequested: this.userRequested,
+ fromCache: Tabs.useCache
+ });
+
+ this.isSearch = false;
+
+ if (!aEvent.persisted && Services.prefs.getBoolPref("browser.ui.linkify.phone")) {
+ if (!this._linkifier)
+ this._linkifier = new Linkifier();
+ this._linkifier.linkifyNumbers(this.browser.contentWindow.document);
+ }
+
+ // Update page actions for helper apps.
+ let uri = this.browser.currentURI;
+ if (BrowserApp.selectedTab == this) {
+ if (ExternalApps.shouldCheckUri(uri)) {
+ ExternalApps.updatePageAction(uri, this.browser.contentDocument);
+ } else {
+ ExternalApps.clearPageAction();
+ }
+ }
+ }
+ }
+ },
+
+ onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
+ let contentWin = aWebProgress.DOMWindow;
+ if (contentWin != contentWin.top)
+ return;
+
+ // Filter optimization: Only really send NETWORK state changes to Java listener
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
+ if (AppConstants.NIGHTLY_BUILD && (aStateFlags & Ci.nsIWebProgressListener.STATE_START)) {
+ Profiler.AddMarker("Load start: " + aRequest.QueryInterface(Ci.nsIChannel).originalURI.spec);
+ } else if (AppConstants.NIGHTLY_BUILD && (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) && !aWebProgress.isLoadingDocument) {
+ Profiler.AddMarker("Load stop: " + aRequest.QueryInterface(Ci.nsIChannel).originalURI.spec);
+ }
+
+ if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) && aWebProgress.isLoadingDocument) {
+ // We may receive a document stop event while a document is still loading
+ // (such as when doing URI fixup). Don't notify Java UI in these cases.
+ return;
+ }
+
+ // Clear page-specific opensearch engines and feeds for a new request.
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_START && aRequest && aWebProgress.isTopLevel) {
+ this.browser.engines = null;
+ this.browser.feeds = null;
+ }
+
+ // true if the page loaded successfully (i.e., no 404s or other errors)
+ let success = false;
+ let uri = "";
+ try {
+ // Remember original URI for UA changes on redirected pages
+ this.originalURI = aRequest.QueryInterface(Components.interfaces.nsIChannel).originalURI;
+
+ if (this.originalURI != null)
+ uri = this.originalURI.spec;
+ } catch (e) { }
+ try {
+ success = aRequest.QueryInterface(Components.interfaces.nsIHttpChannel).requestSucceeded;
+ } catch (e) {
+ // If the request does not handle the nsIHttpChannel interface, use nsIRequest's success
+ // status. Used for local files. See bug 948849.
+ success = aRequest.status == 0;
+ }
+
+ // 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) > 0;
+
+ let message = {
+ type: "Content:StateChange",
+ tabID: this.id,
+ uri: truncate(uri, MAX_URI_LENGTH),
+ state: aStateFlags,
+ restoring: restoring,
+ success: success
+ };
+ Messaging.sendRequest(message);
+ }
+ },
+
+ onLocationChange: function(aWebProgress, aRequest, aLocationURI, aFlags) {
+ let contentWin = aWebProgress.DOMWindow;
+
+ // Browser webapps may load content inside iframes that can not reach across the app/frame boundary
+ // i.e. even though the page is loaded in an iframe window.top != webapp
+ // Make cure this window is a top level tab before moving on.
+ if (BrowserApp.getBrowserForWindow(contentWin) == null)
+ return;
+
+ this._hostChanged = true;
+
+ let fixedURI = aLocationURI;
+ try {
+ fixedURI = URIFixup.createExposableURI(aLocationURI);
+ } catch (ex) { }
+
+ // In restricted profiles, we refuse to let you open various urls.
+ if (!ParentalControls.isAllowed(ParentalControls.BROWSE, fixedURI)) {
+ aRequest.cancel(Cr.NS_BINDING_ABORTED);
+
+ this.browser.docShell.displayLoadError(Cr.NS_ERROR_UNKNOWN_PROTOCOL, fixedURI, null);
+ }
+
+ let contentType = contentWin.document.contentType;
+
+ // If fixedURI matches browser.lastURI, we assume this isn't a real location
+ // change but rather a spurious addition like a wyciwyg URI prefix. See Bug 747883.
+ // Note that we have to ensure fixedURI is not the same as aLocationURI so we
+ // don't false-positive page reloads as spurious additions.
+ let sameDocument = (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) != 0 ||
+ ((this.browser.lastURI != null) && fixedURI.equals(this.browser.lastURI) && !fixedURI.equals(aLocationURI));
+ this.browser.lastURI = fixedURI;
+
+ // Let the reader logic know about same document changes because we won't get a DOMContentLoaded
+ // or pageshow event, but we'll still want to update the reader view button to account for this change.
+ // This mirrors the desktop logic in TabsProgressListener.
+ if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
+ this.browser.messageManager.sendAsyncMessage("Reader:PushState", {isArticle: this.browser.isArticle});
+ }
+
+ // Reset state of click-to-play plugin notifications.
+ clearTimeout(this.pluginDoorhangerTimeout);
+ this.pluginDoorhangerTimeout = null;
+ this.shouldShowPluginDoorhanger = true;
+ this.clickToPlayPluginsActivated = false;
+
+ let documentURI = contentWin.document.documentURIObject.spec;
+
+ // If reader mode, get the base domain for the original url.
+ let strippedURI = this._stripAboutReaderURL(documentURI);
+
+ // Borrowed from desktop Firefox: http://hg.mozilla.org/mozilla-central/annotate/72835344333f/browser/base/content/urlbarBindings.xml#l236
+ let matchedURL = strippedURI.match(/^((?:[a-z]+:\/\/)?(?:[^\/]+@)?)(.+?)(?::\d+)?(?:\/|$)/);
+ let baseDomain = "";
+ if (matchedURL) {
+ var domain = "";
+ [, , domain] = matchedURL;
+
+ try {
+ baseDomain = Services.eTLD.getBaseDomainFromHost(domain);
+ if (!domain.endsWith(baseDomain)) {
+ // getBaseDomainFromHost converts its resultant to ACE.
+ let IDNService = Cc["@mozilla.org/network/idn-service;1"].getService(Ci.nsIIDNService);
+ baseDomain = IDNService.convertACEtoUTF8(baseDomain);
+ }
+ } catch (e) {}
+ }
+
+ // If we are navigating to a new location with a different host,
+ // clear any URL origin that might have been pinned to this tab.
+ let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
+ let appOrigin = ss.getTabValue(this, "appOrigin");
+ if (appOrigin) {
+ let originHost = "";
+ try {
+ originHost = Services.io.newURI(appOrigin, null, null).host;
+ } catch (e if (e.result == Cr.NS_ERROR_FAILURE)) {
+ // NS_ERROR_FAILURE can be thrown by nsIURI.host if the URI scheme does not possess a host - in this case
+ // we just act as if we have an empty host.
+ }
+ if (originHost != aLocationURI.host) {
+ // Note: going 'back' will not make this tab pinned again
+ ss.deleteTabValue(this, "appOrigin");
+ }
+ }
+
+ // Update the page actions URI for helper apps.
+ if (BrowserApp.selectedTab == this) {
+ ExternalApps.updatePageActionUri(fixedURI);
+ }
+
+ let webNav = contentWin.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
+
+ let message = {
+ type: "Content:LocationChange",
+ tabID: this.id,
+ uri: truncate(fixedURI.spec, MAX_URI_LENGTH),
+ userRequested: this.userRequested || "",
+ baseDomain: baseDomain,
+ contentType: (contentType ? contentType : ""),
+ sameDocument: sameDocument,
+
+ historyIndex: webNav.sessionHistory.index,
+ historySize: webNav.sessionHistory.count,
+ canGoBack: webNav.canGoBack,
+ canGoForward: webNav.canGoForward,
+ };
+
+ Messaging.sendRequest(message);
+
+ if (!sameDocument) {
+ // 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;
+ this.hasTouchListener = false;
+ Services.obs.notifyObservers(this.browser, "Session:NotifyLocationChange", null);
+ }
+ },
+
+ _stripAboutReaderURL: function (url) {
+ return ReaderMode.getOriginalUrl(url) || url;
+ },
+
+ // Properties used to cache security state used to update the UI
+ _state: null,
+ _hostChanged: false, // onLocationChange will flip this bit
+
+ onSecurityChange: function(aWebProgress, aRequest, aState) {
+ // Don't need to do anything if the data we use to update the UI hasn't changed
+ if (this._state == aState && !this._hostChanged)
+ return;
+
+ this._state = aState;
+ this._hostChanged = false;
+
+ let identity = IdentityHandler.checkIdentity(aState, this.browser);
+
+ let message = {
+ type: "Content:SecurityChange",
+ tabID: this.id,
+ identity: identity
+ };
+
+ Messaging.sendRequest(message);
+ },
+
+ onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) {
+ // Note: aWebProgess and aRequest will be NULL since we are filtering webprogress
+ // notifications using nsBrowserStatusFilter.
+ },
+
+ onStatusChange: function(aBrowser, aWebProgress, aRequest, aStatus, aMessage) {
+ // Note: aWebProgess and aRequest will be NULL since we are filtering webprogress
+ // notifications using nsBrowserStatusFilter.
+ },
+
+ _getGeckoZoom: function() {
+ let res = {};
+ let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ cwu.getResolution(res);
+ let zoom = res.value * window.devicePixelRatio;
+ return zoom;
+ },
+
+ saveSessionZoom: function(aZoom) {
+ let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ cwu.setResolutionAndScaleTo(aZoom / window.devicePixelRatio);
+ },
+
+ restoredSessionZoom: function() {
+ let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+
+ if (this._restoreZoom && cwu.isResolutionSet) {
+ return this._getGeckoZoom();
+ }
+ return null;
+ },
+
+ _updateZoomFromHistoryEvent: function(aHistoryEventName) {
+ // Restore zoom only when moving in session history, not for new page loads.
+ this._restoreZoom = aHistoryEventName !== "New";
+ },
+
+ OnHistoryNewEntry: function(aUri) {
+ this._updateZoomFromHistoryEvent("New");
+ },
+
+ OnHistoryGoBack: function(aUri) {
+ this._updateZoomFromHistoryEvent("Back");
+ return true;
+ },
+
+ OnHistoryGoForward: function(aUri) {
+ this._updateZoomFromHistoryEvent("Forward");
+ 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._updateZoomFromHistoryEvent("Goto");
+ return true;
+ },
+
+ OnHistoryPurge: function(aNumEntries) {
+ this._updateZoomFromHistoryEvent("Purge");
+ return true;
+ },
+
+ OnHistoryReplaceEntry: function(aIndex) {
+ // we don't do anything with this, so don't propogate it
+ // for now anyway.
+ },
+
+ ShouldNotifyMediaPlaybackChange: function(activeState) {
+ // If the media is active, we would check it's duration, because we don't
+ // want to show the media control interface for the short sound which
+ // duration is smaller than the threshold. The basic unit is second.
+ // Note : the streaming format's duration is infinite.
+ if (activeState === "inactive") {
+ return true;
+ }
+
+ const mediaDurationThreshold = 1.0;
+
+ let audioElements = this.browser.contentDocument.getElementsByTagName("audio");
+ for each (let audio in audioElements) {
+ if (!audio.paused && audio.duration < mediaDurationThreshold) {
+ return false;
+ }
+ }
+
+ let videoElements = this.browser.contentDocument.getElementsByTagName("video");
+ for each (let video in videoElements) {
+ if (!video.paused && video.duration < mediaDurationThreshold) {
+ return false;
+ }
+ }
+
+ return true;
+ },
+
+ 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) {
+ if (BrowserApp.selectedTab == this) {
+ BrowserApp.contentDocumentChanged();
+ }
+ this.contentDocumentIsDisplayed = true;
+
+ if (contentDocument instanceof Ci.nsIImageDocument) {
+ contentDocument.shrinkToFit();
+ }
+ }
+ break;
+
+ case "media-playback":
+ case "media-playback-resumed":
+ if (!aSubject) {
+ return;
+ }
+
+ let winId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
+ if (this.browser.outerWindowID != winId) {
+ return;
+ }
+
+ if (!this.ShouldNotifyMediaPlaybackChange(aData)) {
+ return;
+ }
+
+ let status;
+ if (aTopic == "media-playback") {
+ status = (aData === "inactive") ? "end" : "start";
+ } else if (aTopic == "media-playback-resumed") {
+ status = "resume";
+ }
+
+ Messaging.sendRequest({
+ type: "Tab:MediaPlaybackChange",
+ tabID: this.id,
+ status: status
+ });
+ 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() {
+ this._clickInZoomedView = false;
+ Services.obs.addObserver(this, "Gesture:SingleTap", false);
+ Services.obs.addObserver(this, "Gesture:ClickInZoomedView", false);
+
+ BrowserApp.deck.addEventListener("touchend", this, true);
+
+ BrowserApp.deck.addEventListener("DOMUpdatePageReport", PopupBlockerObserver.onUpdatePageReport, false);
+ BrowserApp.deck.addEventListener("MozMouseHittest", this, true);
+ BrowserApp.deck.addEventListener("OpenMediaWithExternalApp", this, true);
+
+ InitLater(() => BrowserApp.deck.addEventListener("click", InputWidgetHelper, true));
+ InitLater(() => BrowserApp.deck.addEventListener("click", SelectHelper, true));
+
+ // ReaderViews support backPress listeners.
+ Messaging.addListener(() => {
+ return Reader.onBackPress(BrowserApp.selectedTab.id);
+ }, "Browser:OnBackPressed");
+ },
+
+ handleEvent: function(aEvent) {
+ switch (aEvent.type) {
+ case 'touchend':
+ if (this._inCluster) {
+ aEvent.preventDefault();
+ }
+ break;
+ case 'MozMouseHittest':
+ this._handleRetargetedTouchStart(aEvent);
+ break;
+ case 'OpenMediaWithExternalApp': {
+ let mediaSrc = aEvent.target.currentSrc || aEvent.target.src;
+ let uuid = uuidgen.generateUUID().toString();
+ Services.androidBridge.handleGeckoMessage({
+ type: "Video:Play",
+ uri: mediaSrc,
+ uuid: uuid
+ });
+ break;
+ }
+ }
+ },
+
+ _handleRetargetedTouchStart: function(aEvent) {
+ // we should only get this called just after a new touchstart with a single
+ // touch point.
+ if (!BrowserApp.isBrowserContentDocumentDisplayed() || aEvent.defaultPrevented) {
+ return;
+ }
+
+ let target = aEvent.target;
+ if (!target) {
+ return;
+ }
+
+ this._inCluster = aEvent.hitCluster;
+ if (this._inCluster) {
+ return; // No highlight for a cluster of links
+ }
+
+ let uri = this._getLinkURI(target);
+ if (uri) {
+ try {
+ Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null);
+ } catch (e) {}
+ }
+ this._doTapHighlight(target);
+ },
+
+ _getLinkURI: function(aElement) {
+ if (aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE &&
+ ((aElement instanceof Ci.nsIDOMHTMLAnchorElement && aElement.href) ||
+ (aElement instanceof Ci.nsIDOMHTMLAreaElement && aElement.href))) {
+ try {
+ return Services.io.newURI(aElement.href, null, null);
+ } catch (e) {}
+ }
+ return null;
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ // 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()) {
+ this.handleUserEvent(aTopic, aData);
+ }
+ },
+
+ handleUserEvent: function(aTopic, aData) {
+ switch (aTopic) {
+
+ case "Gesture:ClickInZoomedView":
+ this._clickInZoomedView = true;
+ break;
+
+ case "Gesture:SingleTap": {
+ let focusedElement = BrowserApp.getFocusedInput(BrowserApp.selectedBrowser);
+ let data = JSON.parse(aData);
+ let {x, y} = data;
+
+ if (this._inCluster && this._clickInZoomedView != true) {
+ // If there is a focused element, the display of the zoomed view won't remove the focus.
+ // In this case, the form assistant linked to the focused element will never be closed.
+ // To avoid this situation, the focus is moved and the form assistant is closed.
+ if (focusedElement) {
+ try {
+ Services.focus.moveFocus(BrowserApp.selectedBrowser.contentWindow, null, Services.focus.MOVEFOCUS_ROOT, 0);
+ } catch(e) {
+ Cu.reportError(e);
+ }
+ Messaging.sendRequest({ type: "FormAssist:Hide" });
+ }
+ this._clusterClicked(x, y);
+ } else {
+ if (this._clickInZoomedView != true) {
+ this._closeZoomedView();
+ }
+ }
+ this._clickInZoomedView = false;
+ this._cancelTapHighlight();
+ break;
+ }
+
+ default:
+ dump('BrowserEventHandler.handleUserEvent: unexpected topic "' + aTopic + '"');
+ break;
+ }
+ },
+
+ _closeZoomedView: function() {
+ Messaging.sendRequest({
+ type: "Gesture:CloseZoomedView"
+ });
+ },
+
+ _clusterClicked: function(aX, aY) {
+ Messaging.sendRequest({
+ type: "Gesture:clusteredLinksClicked",
+ clickPosition: {
+ x: aX,
+ y: aY
+ }
+ });
+ },
+
+ _highlightElement: null,
+
+ _doTapHighlight: function _doTapHighlight(aElement) {
+ this._highlightElement = aElement;
+ },
+
+ _cancelTapHighlight: function _cancelTapHighlight() {
+ if (!this._highlightElement)
+ return;
+
+ this._highlightElement = null;
+ }
+};
+
+const ElementTouchHelper = {
+ 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 (errorDoc.documentURI.startsWith("about:certerror?e=nssBadCert")) {
+ 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, errorDoc.defaultView);
+ else
+ sslExceptions.addTemporaryException(uri, errorDoc.defaultView);
+ } catch (e) {
+ dump("Failed to set cert exception: " + e + "\n");
+ }
+ errorDoc.location.reload();
+ } else if (target == errorDoc.getElementById("getMeOutOfHereButton")) {
+ errorDoc.location = "about:home";
+ }
+ } else if (errorDoc.documentURI.startsWith("about:blocked")) {
+ // The event came from a button on a malware/phishing block page
+ // First check whether it's malware, phishing or unwanted, so that we
+ // can use the right strings/links
+ let bucketName = "";
+ let sendTelemetry = false;
+ if (errorDoc.documentURI.includes("e=malwareBlocked")) {
+ sendTelemetry = true;
+ bucketName = "WARNING_MALWARE_PAGE_";
+ } else if (errorDoc.documentURI.includes("e=deceptiveBlocked")) {
+ sendTelemetry = true;
+ bucketName = "WARNING_PHISHING_PAGE_";
+ } else if (errorDoc.documentURI.includes("e=unwantedBlocked")) {
+ sendTelemetry = true;
+ bucketName = "WARNING_UNWANTED_PAGE_";
+ }
+ let nsISecTel = Ci.nsISecurityUITelemetry;
+ let isIframe = (errorDoc.defaultView.parent === errorDoc.defaultView);
+ bucketName += isIframe ? "TOP_" : "FRAME_";
+
+ let formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].getService(Ci.nsIURLFormatter);
+
+ if (target == errorDoc.getElementById("getMeOutButton")) {
+ if (sendTelemetry) {
+ Telemetry.addData("SECURITY_UI", nsISecTel[bucketName + "GET_ME_OUT_OF_HERE"]);
+ }
+ errorDoc.location = "about:home";
+ } else if (target == errorDoc.getElementById("reportButton")) {
+ // We log even if malware/phishing info URL couldn't be found:
+ // the measurement is for how many users clicked the WHY BLOCKED button
+ if (sendTelemetry) {
+ Telemetry.addData("SECURITY_UI", nsISecTel[bucketName + "WHY_BLOCKED"]);
+ }
+
+ // This is the "Why is this site blocked" button. We redirect
+ // to the generic page describing phishing/malware protection.
+ let url = Services.urlFormatter.formatURLPref("app.support.baseURL");
+ BrowserApp.selectedBrowser.loadURI(url + "phishing-malware");
+ } else if (target == errorDoc.getElementById("ignoreWarningButton") &&
+ Services.prefs.getBoolPref("browser.safebrowsing.allowOverride")) {
+ if (sendTelemetry) {
+ Telemetry.addData("SECURITY_UI", nsISecTel[bucketName + "IGNORE_WARNING"]);
+ }
+
+ // Allow users to override and continue through to the site,
+ let webNav = BrowserApp.selectedBrowser.docShell.QueryInterface(Ci.nsIWebNavigation);
+ let location = BrowserApp.selectedBrowser.contentWindow.location;
+ webNav.loadURI(location, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CLASSIFIER, null, null, null);
+
+ // ....but add a notify bar as a reminder, so that they don't lose
+ // track after, e.g., tab switching.
+ NativeWindow.doorhanger.show(Strings.browser.GetStringFromName("safeBrowsingDoorhanger"), "safebrowsing-warning", [], BrowserApp.selectedTab.id);
+ }
+ }
+ break;
+ }
+ }
+ }
+};
+
+var FormAssistant = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver]),
+
+ // Used to keep track of the element that corresponds to the current
+ // autocomplete suggestions
+ _currentInputElement: null,
+
+ // The value of the currently focused input
+ _currentInputValue: null,
+
+ // Whether we're in the middle of an autocomplete
+ _doingAutocomplete: false,
+
+ // 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, "FormAssist:Remove", false);
+ Services.obs.addObserver(this, "invalidformsubmit", false);
+ Services.obs.addObserver(this, "PanZoom:StateChange", false);
+
+ // We need to use a capturing listener for focus events
+ BrowserApp.deck.addEventListener("focus", this, true);
+ BrowserApp.deck.addEventListener("blur", this, true);
+ BrowserApp.deck.addEventListener("click", this, true);
+ BrowserApp.deck.addEventListener("input", this, false);
+ BrowserApp.deck.addEventListener("pageshow", this, false);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "PanZoom:StateChange":
+ // If the user is just touching the screen and we haven't entered a pan or zoom state yet do nothing
+ if (aData == "TOUCHING" || aData == "WAITING_LISTENERS")
+ break;
+ if (aData == "NOTHING") {
+ // only look for input elements, not contentEditable or multiline text areas
+ let focused = BrowserApp.getFocusedInput(BrowserApp.selectedBrowser, true);
+ if (!focused)
+ break;
+
+ if (this._showValidationMessage(focused))
+ break;
+ let checkResultsClick = hasResults => {
+ if (!hasResults) {
+ this._hideFormAssistPopup();
+ }
+ };
+ this._showAutoCompleteSuggestions(focused, checkResultsClick);
+ } else {
+ // temporarily hide the form assist popup while we're panning or zooming the page
+ this._hideFormAssistPopup();
+ }
+ break;
+ case "FormAssist:AutoComplete":
+ if (!this._currentInputElement)
+ break;
+
+ let editableElement = this._currentInputElement.QueryInterface(Ci.nsIDOMNSEditableElement);
+
+ this._doingAutocomplete = true;
+
+ // If we have an active composition string, commit it before sending
+ // the autocomplete event with the text that will replace it.
+ try {
+ let imeEditor = editableElement.editor.QueryInterface(Ci.nsIEditorIMESupport);
+ if (imeEditor.composing)
+ imeEditor.forceCompositionEnd();
+ } catch (e) {}
+
+ editableElement.setUserInput(aData);
+ this._currentInputValue = aData;
+
+ let event = this._currentInputElement.ownerDocument.createEvent("Events");
+ event.initEvent("DOMAutoComplete", true, true);
+ this._currentInputElement.dispatchEvent(event);
+
+ this._doingAutocomplete = false;
+
+ break;
+
+ case "FormAssist:Hidden":
+ this._currentInputElement = null;
+ break;
+
+ case "FormAssist:Remove":
+ if (!this._currentInputElement) {
+ break;
+ }
+
+ FormHistory.update({
+ op: "remove",
+ fieldname: this._currentInputElement.name,
+ value: aData
+ });
+ break;
+ }
+ },
+
+ notifyInvalidSubmit: function notifyInvalidSubmit(aFormElement, aInvalidElements) {
+ if (!aInvalidElements.length)
+ return;
+
+ // Ignore this notificaiton if the current tab doesn't contain the invalid element
+ let currentElement = aInvalidElements.queryElementAt(0, Ci.nsISupports);
+ if (BrowserApp.selectedBrowser.contentDocument !=
+ currentElement.ownerDocument.defaultView.top.document)
+ return;
+
+ this._invalidSubmit = true;
+
+ // Our focus listener will show the element's validation message
+ currentElement.focus();
+ },
+
+ handleEvent: function(aEvent) {
+ switch (aEvent.type) {
+ case "focus": {
+ let currentElement = aEvent.target;
+
+ // Only show a validation message on focus.
+ this._showValidationMessage(currentElement);
+ break;
+ }
+
+ case "blur": {
+ this._currentInputValue = null;
+ break;
+ }
+
+ case "click": {
+ 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;
+
+ let checkResultsClick = hasResults => {
+ if (!hasResults) {
+ this._hideFormAssistPopup();
+ }
+ };
+
+ this._showAutoCompleteSuggestions(currentElement, checkResultsClick);
+ break;
+ }
+
+ case "input": {
+ let currentElement = aEvent.target;
+
+ // If this element isn't focused, we're already in middle of an
+ // autocomplete, or its value hasn't changed, don't show the
+ // autocomplete popup.
+ if (currentElement !== BrowserApp.getFocusedInput(BrowserApp.selectedBrowser) ||
+ this._doingAutocomplete ||
+ currentElement.value === this._currentInputValue) {
+ break;
+ }
+
+ this._currentInputValue = currentElement.value;
+
+ // Since we can only show one popup at a time, prioritze autocomplete
+ // suggestions over a form validation message
+ let checkResultsInput = hasResults => {
+ if (hasResults)
+ return;
+
+ if (this._showValidationMessage(currentElement))
+ return;
+
+ // If we're not showing autocomplete suggestions, hide the form assist popup
+ this._hideFormAssistPopup();
+ };
+
+ this._showAutoCompleteSuggestions(currentElement, checkResultsInput);
+ break;
+ }
+
+ // Reset invalid submit state on each pageshow
+ case "pageshow": {
+ if (!this._invalidSubmit)
+ return;
+
+ let selectedBrowser = BrowserApp.selectedBrowser;
+ if (selectedBrowser) {
+ let selectedDocument = selectedBrowser.contentDocument;
+ let target = aEvent.originalTarget;
+ if (target == selectedDocument || target.ownerDocument == selectedDocument)
+ this._invalidSubmit = false;
+ }
+ break;
+ }
+ }
+ },
+
+ // We only want to show autocomplete suggestions for certain elements
+ _isAutoComplete: function _isAutoComplete(aElement) {
+ if (!(aElement instanceof HTMLInputElement) || aElement.readOnly || aElement.disabled ||
+ (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.
+ // aCallback(array_of_suggestions) is called when results are available.
+ _getAutoCompleteSuggestions: function _getAutoCompleteSuggestions(aSearchString, aElement, aCallback) {
+ // Cache the form autocomplete service for future use
+ if (!this._formAutoCompleteService) {
+ this._formAutoCompleteService = Cc["@mozilla.org/satchel/form-autocomplete;1"]
+ .getService(Ci.nsIFormAutoComplete);
+ }
+
+ let resultsAvailable = function (results) {
+ 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 });
+ }
+ aCallback(suggestions);
+ };
+
+ this._formAutoCompleteService.autoCompleteSearchAsync(aElement.name || aElement.id,
+ aSearchString, aElement, null,
+ null, resultsAvailable);
+ },
+
+ /**
+ * (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().includes(lowerFieldValue)) )
+ continue;
+ suggestions.push({ label: label, value: item.value });
+ }
+
+ return suggestions;
+ },
+
+ // Retrieves autocomplete suggestions for an element from the form autocomplete service
+ // and sends the suggestions to the Java UI, along with element position data. As
+ // autocomplete queries are asynchronous, calls aCallback when done with a true
+ // argument if results were found and false if no results were found.
+ _showAutoCompleteSuggestions: function _showAutoCompleteSuggestions(aElement, aCallback) {
+ if (!this._isAutoComplete(aElement)) {
+ aCallback(false);
+ return;
+ }
+ if (this._isDisabledElement(aElement)) {
+ aCallback(false);
+ return;
+ }
+
+ let isEmpty = (aElement.value.length === 0);
+
+ let resultsAvailable = autoCompleteSuggestions => {
+ // On desktop, we show datalist suggestions below autocomplete suggestions,
+ // without duplicates removed.
+ let listSuggestions = this._getListSuggestions(aElement);
+ let suggestions = autoCompleteSuggestions.concat(listSuggestions);
+
+ // Return false if there are no suggestions to show
+ if (!suggestions.length) {
+ aCallback(false);
+ return;
+ }
+
+ Messaging.sendRequest({
+ type: "FormAssist:AutoComplete",
+ suggestions: suggestions,
+ rect: ElementTouchHelper.getBoundingContentRect(aElement),
+ isEmpty: isEmpty,
+ });
+
+ // Keep track of input element so we can fill it in if the user
+ // selects an autocomplete suggestion
+ this._currentInputElement = aElement;
+ aCallback(true);
+ };
+
+ this._getAutoCompleteSuggestions(aElement.value, aElement, resultsAvailable);
+ },
+
+ // 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;
+
+ Messaging.sendRequest({
+ type: "FormAssist:ValidationMessage",
+ validationMessage: aElement.validationMessage,
+ rect: ElementTouchHelper.getBoundingContentRect(aElement)
+ });
+
+ return true;
+ },
+
+ _hideFormAssistPopup: function _hideFormAssistPopup() {
+ Messaging.sendRequest({ type: "FormAssist:Hide" });
+ },
+
+ _isDisabledElement : function(aElement) {
+ let currentElement = aElement;
+ while (currentElement) {
+ if(currentElement.disabled)
+ return true;
+
+ currentElement = currentElement.parentElement;
+ }
+ return false;
+ }
+};
+
+var XPInstallObserver = {
+ init: function() {
+ Services.obs.addObserver(this, "addon-install-origin-blocked", false);
+ Services.obs.addObserver(this, "addon-install-disabled", false);
+ Services.obs.addObserver(this, "addon-install-blocked", false);
+ Services.obs.addObserver(this, "addon-install-started", false);
+ Services.obs.addObserver(this, "xpi-signature-changed", false);
+ Services.obs.addObserver(this, "browser-delayed-startup-finished", false);
+
+ AddonManager.addInstallListener(this);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ let installInfo, tab, host;
+ if (aSubject && aSubject instanceof Ci.amIWebInstallInfo) {
+ installInfo = aSubject;
+ tab = BrowserApp.getTabForBrowser(installInfo.browser);
+ if (installInfo.originatingURI) {
+ host = installInfo.originatingURI.host;
+ }
+ }
+
+ let strings = Strings.browser;
+ let brandShortName = Strings.brand.GetStringFromName("brandShortName");
+
+ switch (aTopic) {
+ case "addon-install-started":
+ Snackbars.show(strings.GetStringFromName("alertAddonsDownloading"), Snackbars.LENGTH_LONG);
+ break;
+ case "addon-install-disabled": {
+ if (!tab)
+ return;
+
+ let enabled = true;
+ try {
+ enabled = Services.prefs.getBoolPref("xpinstall.enabled");
+ } catch (e) {}
+
+ let buttons, message, callback;
+ if (!enabled) {
+ message = strings.GetStringFromName("xpinstallDisabledMessageLocked");
+ buttons = [strings.GetStringFromName("unsignedAddonsDisabled.dismiss")];
+ callback: (data) => {};
+ } else {
+ message = strings.formatStringFromName("xpinstallDisabledMessage2", [brandShortName, host], 2);
+ buttons = [
+ strings.GetStringFromName("xpinstallDisabledButton"),
+ strings.GetStringFromName("unsignedAddonsDisabled.dismiss")
+ ];
+ callback: (data) => {
+ if (data.button === 1) {
+ Services.prefs.setBoolPref("xpinstall.enabled", true)
+ }
+ };
+ }
+
+ new Prompt({
+ title: Strings.browser.GetStringFromName("addonError.titleError"),
+ message: message,
+ buttons: buttons
+ }).show(callback);
+ break;
+ }
+ case "addon-install-blocked": {
+ if (!tab)
+ return;
+
+ let message;
+ if (host) {
+ // We have a host which asked for the install.
+ message = strings.formatStringFromName("xpinstallPromptWarning2", [brandShortName, host], 2);
+ } else {
+ // Without a host we address the add-on as the initiator of the install.
+ let addon = null;
+ if (installInfo.installs.length > 0) {
+ addon = installInfo.installs[0].name;
+ }
+ if (addon) {
+ // We have an addon name, show the regular message.
+ message = strings.formatStringFromName("xpinstallPromptWarningLocal", [brandShortName, addon], 2);
+ } else {
+ // We don't have an addon name, show an alternative message.
+ message = strings.formatStringFromName("xpinstallPromptWarningDirect", [brandShortName], 1);
+ }
+ }
+
+ let buttons = [
+ strings.GetStringFromName("xpinstallPromptAllowButton"),
+ strings.GetStringFromName("unsignedAddonsDisabled.dismiss")
+ ];
+ new Prompt({
+ title: Strings.browser.GetStringFromName("addonError.titleBlocked"),
+ message: message,
+ buttons: buttons
+ }).show((data) => {
+ if (data.button === 0) {
+ // Kick off the install
+ installInfo.install();
+ }
+ });
+ break;
+ }
+ case "addon-install-origin-blocked": {
+ if (!tab)
+ return;
+
+ new Prompt({
+ title: Strings.browser.GetStringFromName("addonError.titleBlocked"),
+ message: strings.formatStringFromName("xpinstallPromptWarningDirect", [brandShortName], 1),
+ buttons: [strings.GetStringFromName("unsignedAddonsDisabled.dismiss")]
+ }).show((data) => {});
+ break;
+ }
+ case "xpi-signature-changed": {
+ if (JSON.parse(aData).disabled.length) {
+ this._notifyUnsignedAddonsDisabled();
+ }
+ break;
+ }
+ case "browser-delayed-startup-finished": {
+ let disabledAddons = AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_DISABLED);
+ for (let id of disabledAddons) {
+ if (AddonManager.getAddonByID(id).signedState <= AddonManager.SIGNEDSTATE_MISSING) {
+ this._notifyUnsignedAddonsDisabled();
+ break;
+ }
+ }
+ break;
+ }
+ }
+ },
+
+ _notifyUnsignedAddonsDisabled: function() {
+ new Prompt({
+ window: window,
+ title: Strings.browser.GetStringFromName("unsignedAddonsDisabled.title"),
+ message: Strings.browser.GetStringFromName("unsignedAddonsDisabled.message"),
+ buttons: [
+ Strings.browser.GetStringFromName("unsignedAddonsDisabled.viewAddons"),
+ Strings.browser.GetStringFromName("unsignedAddonsDisabled.dismiss")
+ ]
+ }).show((data) => {
+ if (data.button === 0) {
+ // TODO: Open about:addons to show only unsigned add-ons?
+ BrowserApp.selectOrAddTab("about:addons", { parentId: BrowserApp.selectedTab.id });
+ }
+ });
+ },
+
+ onInstallEnded: function(aInstall, aAddon) {
+ // Don't create a notification for distribution add-ons.
+ if (Distribution.pendingAddonInstalls.has(aInstall)) {
+ Distribution.pendingAddonInstalls.delete(aInstall);
+ return;
+ }
+
+ 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) {
+ this.showRestartPrompt();
+ } else {
+ // Display completion message for new installs or updates not done Automatically
+ if (!aInstall.existingAddon || !AddonManager.shouldAutoUpdate(aInstall.existingAddon)) {
+ let message = Strings.browser.GetStringFromName("alertAddonsInstalledNoRestart.message");
+ Snackbars.show(message, Snackbars.LENGTH_LONG, {
+ action: {
+ label: Strings.browser.GetStringFromName("alertAddonsInstalledNoRestart.action2"),
+ callback: () => {
+ UITelemetry.addEvent("show.1", "toast", null, "addons");
+ BrowserApp.selectOrAddTab("about:addons", { parentId: BrowserApp.selectedTab.id });
+ },
+ }
+ });
+ }
+ }
+ },
+
+ onInstallFailed: function(aInstall) {
+ this._showErrorMessage(aInstall);
+ },
+
+ onDownloadProgress: function(aInstall) {},
+
+ onDownloadFailed: function(aInstall) {
+ this._showErrorMessage(aInstall);
+ },
+
+ onDownloadCancelled: function(aInstall) {},
+
+ _showErrorMessage: function(aInstall) {
+ // Don't create a notification for distribution add-ons.
+ if (Distribution.pendingAddonInstalls.has(aInstall)) {
+ Cu.reportError("Error installing distribution add-on: " + aInstall.addon.id);
+ Distribution.pendingAddonInstalls.delete(aInstall);
+ return;
+ }
+
+ 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 {
+ error += "Incompatible";
+ }
+
+ 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);
+
+ if (aInstall.error == AddonManager.ERROR_SIGNEDSTATE_REQUIRED) {
+ new Prompt({
+ window: window,
+ title: Strings.browser.GetStringFromName("addonError.titleBlocked"),
+ message: msg,
+ buttons: [Strings.browser.GetStringFromName("addonError.learnMore")]
+ }).show((data) => {
+ if (data.button === 0) {
+ let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons";
+ BrowserApp.addTab(url, { parentId: BrowserApp.selectedTab.id });
+ }
+ });
+ } else {
+ Services.prompt.alert(null, Strings.browser.GetStringFromName("addonError.titleError"), msg);
+ }
+ },
+
+ showRestartPrompt: function() {
+ 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) {
+ Services.obs.notifyObservers(null, "quit-application-proceeding", null);
+ let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup);
+ appStartup.quit(Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit);
+ }
+ },
+ positive: true
+ }];
+
+ let message = Strings.browser.GetStringFromName("notificationRestart.normal");
+ NativeWindow.doorhanger.show(message, "addon-app-restart", buttons, BrowserApp.selectedTab.id, { persistence: -1 });
+ },
+
+ hideRestartPrompt: function() {
+ NativeWindow.doorhanger.hide("addon-app-restart", BrowserApp.selectedTab.id);
+ }
+};
+
+var ViewportHandler = {
+ init: function init() {
+ Services.obs.addObserver(this, "Window:Resize", false);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == "Window:Resize" && aData) {
+ let scrollChange = JSON.parse(aData);
+ let windowUtils = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ windowUtils.setNextPaintSyncId(scrollChange.id);
+ }
+ }
+};
+
+/**
+ * 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 popupCount = browser.pageReport.length;
+
+ let strings = Strings.browser;
+ let message = PluralForm.get(popupCount, strings.GetStringFromName("popup.message"))
+ .replace("#1", brandShortName)
+ .replace("#2", popupCount);
+
+ let buttons = [
+ {
+ label: strings.GetStringFromName("popup.dontShow"),
+ callback: function(aChecked) {
+ if (aChecked)
+ PopupBlockerObserver.allowPopupsForSite(false);
+ }
+ },
+ {
+ label: strings.GetStringFromName("popup.show"),
+ callback: function(aChecked) {
+ // Set permission before opening popup windows
+ if (aChecked)
+ PopupBlockerObserver.allowPopupsForSite(true);
+
+ PopupBlockerObserver.showPopupsForSite();
+ },
+ positive: true
+ }
+ ];
+
+ let options = { checkbox: Strings.browser.GetStringFromName("popup.dontAskAgain") };
+ NativeWindow.doorhanger.show(message, "popup-blocked", buttons, null, options);
+ }
+ // 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].popupWindowURIspec;
+
+ // 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;
+ let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(parent.browser);
+ BrowserApp.addTab(popupURIspec, { parentId: parent.id, isPrivate: isPrivate });
+ }
+ }
+ }
+};
+
+
+var IndexedDB = {
+ _permissionsPrompt: "indexedDB-permissions-prompt",
+ _permissionsResponse: "indexedDB-permissions-response",
+
+ init: function IndexedDB_init() {
+ Services.obs.addObserver(this, this._permissionsPrompt, false);
+ },
+
+ observe: function IndexedDB_observe(subject, topic, data) {
+ if (topic != this._permissionsPrompt) {
+ throw new Error("Unexpected topic!");
+ }
+
+ let requestor = subject.QueryInterface(Ci.nsIInterfaceRequestor);
+
+ let browser = requestor.getInterface(Ci.nsIDOMNode);
+ let tab = BrowserApp.getTabForBrowser(browser);
+ if (!tab)
+ return;
+
+ let host = browser.currentURI.asciiHost;
+
+ let strings = Strings.browser;
+
+ let message, responseTopic;
+ if (topic == this._permissionsPrompt) {
+ message = strings.formatStringFromName("offlineApps.ask", [host], 1);
+ responseTopic = this._permissionsResponse;
+ }
+
+ const firstTimeoutDuration = 300000; // 5 minutes
+
+ let timeoutId;
+
+ let notificationID = responseTopic + host;
+ let observer = requestor.getInterface(Ci.nsIObserver);
+
+ // This will be set to the result of PopupNotifications.show() below, or to
+ // the result of PopupNotifications.getNotification() if this is a
+ // quotaCancel notification.
+ let notification;
+
+ function timeoutNotification() {
+ // Remove the notification.
+ NativeWindow.doorhanger.hide(notificationID, tab.id);
+
+ // Clear all of our timeout stuff. We may be called directly, not just
+ // when the timeout actually elapses.
+ clearTimeout(timeoutId);
+
+ // And tell the page that the popup timed out.
+ observer.observe(null, responseTopic, Ci.nsIPermissionManager.UNKNOWN_ACTION);
+ }
+
+ let buttons = [
+ {
+ label: strings.GetStringFromName("offlineApps.dontAllow2"),
+ callback: function(aChecked) {
+ clearTimeout(timeoutId);
+ let action = aChecked ? Ci.nsIPermissionManager.DENY_ACTION : Ci.nsIPermissionManager.UNKNOWN_ACTION;
+ observer.observe(null, responseTopic, action);
+ }
+ },
+ {
+ label: strings.GetStringFromName("offlineApps.allow"),
+ callback: function() {
+ clearTimeout(timeoutId);
+ observer.observe(null, responseTopic, Ci.nsIPermissionManager.ALLOW_ACTION);
+ },
+ positive: true
+ }];
+
+ let options = { checkbox: Strings.browser.GetStringFromName("offlineApps.dontAskAgain") };
+ NativeWindow.doorhanger.show(message, notificationID, buttons, tab.id, options);
+
+ // Set the timeoutId after the popup has been created, and use the long
+ // timeout value. If the user doesn't notice the popup after this amount of
+ // time then it is most likely not visible and we want to alert the page.
+ timeoutId = setTimeout(timeoutNotification, firstTimeoutDuration);
+ }
+};
+
+var CharacterEncoding = {
+ _charsets: [],
+
+ init: function init() {
+ Services.obs.addObserver(this, "CharEncoding:Get", false);
+ Services.obs.addObserver(this, "CharEncoding:Set", false);
+ InitLater(() => this.sendState());
+ },
+
+ 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 */ }
+
+ Messaging.sendRequest({
+ type: "CharEncoding:State",
+ visible: showCharEncoding
+ });
+ },
+
+ getEncoding: function getEncoding() {
+ function infoToCharset(info) {
+ return { code: info.value, title: info.label };
+ }
+
+ if (!this._charsets.length) {
+ let data = CharsetMenu.getData();
+
+ // In the desktop UI, the pinned charsets are shown above the rest.
+ let pinnedCharsets = data.pinnedCharsets.map(infoToCharset);
+ let otherCharsets = data.otherCharsets.map(infoToCharset)
+
+ this._charsets = pinnedCharsets.concat(otherCharsets);
+ }
+
+ // Look for the index of the selected charset. Default to -1 if the
+ // doc charset isn't found in the list of available charsets.
+ let docCharset = BrowserApp.selectedBrowser.contentDocument.characterSet;
+ let selected = -1;
+ let charsetCount = this._charsets.length;
+
+ for (let i = 0; i < charsetCount; i++) {
+ if (this._charsets[i].code === docCharset) {
+ selected = i;
+ break;
+ }
+ }
+
+ Messaging.sendRequest({
+ type: "CharEncoding:Data",
+ charsets: this._charsets,
+ selected: selected
+ });
+ },
+
+ setEncoding: function setEncoding(aEncoding) {
+ let browser = BrowserApp.selectedBrowser;
+ browser.docShell.gatherCharsetMenuTelemetry();
+ browser.docShell.charset = aEncoding;
+ browser.reload(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
+ }
+};
+
+var IdentityHandler = {
+ // No trusted identity information. No site identity icon is shown.
+ IDENTITY_MODE_UNKNOWN: "unknown",
+
+ // Domain-Validation SSL CA-signed domain verification (DV).
+ IDENTITY_MODE_IDENTIFIED: "identified",
+
+ // Extended-Validation SSL CA-signed identity information (EV). A more rigorous validation process.
+ IDENTITY_MODE_VERIFIED: "verified",
+
+ // Part of the product's UI (built in about: pages)
+ IDENTITY_MODE_CHROMEUI: "chromeUI",
+
+ // The following mixed content modes are only used if "security.mixed_content.block_active_content"
+ // is enabled. Our Java frontend coalesces them into one indicator.
+
+ // No mixed content information. No mixed content icon is shown.
+ MIXED_MODE_UNKNOWN: "unknown",
+
+ // Blocked active mixed content.
+ MIXED_MODE_CONTENT_BLOCKED: "blocked",
+
+ // Loaded active mixed content.
+ MIXED_MODE_CONTENT_LOADED: "loaded",
+
+ // The following tracking content modes are only used if tracking protection
+ // is enabled. Our Java frontend coalesces them into one indicator.
+
+ // No tracking content information. No tracking content icon is shown.
+ TRACKING_MODE_UNKNOWN: "unknown",
+
+ // Blocked active tracking content. Shield icon is shown, with a popup option to load content.
+ TRACKING_MODE_CONTENT_BLOCKED: "tracking_content_blocked",
+
+ // Loaded active tracking content. Yellow triangle icon is shown.
+ TRACKING_MODE_CONTENT_LOADED: "tracking_content_loaded",
+
+ // Cache the most recent SSLStatus and Location seen in getIdentityStrings
+ _lastStatus : null,
+ _lastLocation : null,
+
+ /**
+ * Helper to parse out the important parts of _lastStatus (of the SSL cert in
+ * particular) for use in constructing identity UI strings
+ */
+ getIdentityData : function() {
+ let result = {};
+ let status = this._lastStatus.QueryInterface(Components.interfaces.nsISSLStatus);
+ let cert = status.serverCert;
+
+ // Human readable name of Subject
+ result.subjectOrg = cert.organization;
+
+ // SubjectName fields, broken up for individual access
+ if (cert.subjectName) {
+ result.subjectNameFields = {};
+ cert.subjectName.split(",").forEach(function(v) {
+ let field = v.split("=");
+ this[field[0]] = field[1];
+ }, result.subjectNameFields);
+
+ // Call out city, state, and country specifically
+ result.city = result.subjectNameFields.L;
+ result.state = result.subjectNameFields.ST;
+ result.country = result.subjectNameFields.C;
+ }
+
+ // Human readable name of Certificate Authority
+ result.caOrg = cert.issuerOrganization || cert.issuerCommonName;
+ result.cert = cert;
+
+ return result;
+ },
+
+ /**
+ * Determines the identity mode corresponding to the icon we show in the urlbar.
+ */
+ getIdentityMode: function getIdentityMode(aState, uri) {
+ if (aState & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL) {
+ return this.IDENTITY_MODE_VERIFIED;
+ }
+
+ if (aState & Ci.nsIWebProgressListener.STATE_IS_SECURE) {
+ return this.IDENTITY_MODE_IDENTIFIED;
+ }
+
+ // We also allow "about:" by allowing the selector to be empty (i.e. '(|.....|...|...)'
+ let whitelist = /^about:($|about|accounts|addons|buildconfig|cache|config|crashes|devices|downloads|fennec|firefox|feedback|healthreport|home|license|logins|logo|memory|mozilla|networking|plugins|privatebrowsing|rights|serviceworkers|support|telemetry|webrtc)($|\?)/i;
+ if (uri.schemeIs("about") && whitelist.test(uri.spec)) {
+ return this.IDENTITY_MODE_CHROMEUI;
+ }
+
+ return this.IDENTITY_MODE_UNKNOWN;
+ },
+
+ getMixedDisplayMode: function getMixedDisplayMode(aState) {
+ if (aState & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT) {
+ return this.MIXED_MODE_CONTENT_LOADED;
+ }
+
+ if (aState & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_DISPLAY_CONTENT) {
+ return this.MIXED_MODE_CONTENT_BLOCKED;
+ }
+
+ return this.MIXED_MODE_UNKNOWN;
+ },
+
+ getMixedActiveMode: function getActiveDisplayMode(aState) {
+ // Only show an indicator for loaded mixed content if the pref to block it is enabled
+ if ((aState & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT) &&
+ !Services.prefs.getBoolPref("security.mixed_content.block_active_content")) {
+ return this.MIXED_MODE_CONTENT_LOADED;
+ }
+
+ if (aState & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT) {
+ return this.MIXED_MODE_CONTENT_BLOCKED;
+ }
+
+ return this.MIXED_MODE_UNKNOWN;
+ },
+
+ getTrackingMode: function getTrackingMode(aState, aBrowser) {
+ if (aState & Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT) {
+ this.shieldHistogramAdd(aBrowser, 2);
+ return this.TRACKING_MODE_CONTENT_BLOCKED;
+ }
+
+ // Only show an indicator for loaded tracking content if the pref to block it is enabled
+ let tpEnabled = Services.prefs.getBoolPref("privacy.trackingprotection.enabled") ||
+ (Services.prefs.getBoolPref("privacy.trackingprotection.pbmode.enabled") &&
+ PrivateBrowsingUtils.isBrowserPrivate(aBrowser));
+
+ if ((aState & Ci.nsIWebProgressListener.STATE_LOADED_TRACKING_CONTENT) && tpEnabled) {
+ this.shieldHistogramAdd(aBrowser, 1);
+ return this.TRACKING_MODE_CONTENT_LOADED;
+ }
+
+ this.shieldHistogramAdd(aBrowser, 0);
+ return this.TRACKING_MODE_UNKNOWN;
+ },
+
+ shieldHistogramAdd: function(browser, value) {
+ if (PrivateBrowsingUtils.isBrowserPrivate(browser)) {
+ return;
+ }
+ Telemetry.addData("TRACKING_PROTECTION_SHIELD", value);
+ },
+
+ /**
+ * Determine the identity of the page being displayed by examining its SSL cert
+ * (if available). Return the data needed to update the UI.
+ */
+ checkIdentity: function checkIdentity(aState, aBrowser) {
+ this._lastStatus = aBrowser.securityUI
+ .QueryInterface(Components.interfaces.nsISSLStatusProvider)
+ .SSLStatus;
+
+ // Don't pass in the actual location object, since it can cause us to
+ // hold on to the window object too long. Just pass in the fields we
+ // care about. (bug 424829)
+ let locationObj = {};
+ try {
+ let location = aBrowser.contentWindow.location;
+ locationObj.host = location.host;
+ locationObj.hostname = location.hostname;
+ locationObj.port = location.port;
+ locationObj.origin = location.origin;
+ } catch (ex) {
+ // Can sometimes throw if the URL being visited has no host/hostname,
+ // e.g. about:blank. The _state for these pages means we won't need these
+ // properties anyways, though.
+ }
+ this._lastLocation = locationObj;
+
+ let uri = aBrowser.currentURI;
+ try {
+ uri = Services.uriFixup.createExposableURI(uri);
+ } catch (e) {}
+
+ let identityMode = this.getIdentityMode(aState, uri);
+ let mixedDisplay = this.getMixedDisplayMode(aState);
+ let mixedActive = this.getMixedActiveMode(aState);
+ let trackingMode = this.getTrackingMode(aState, aBrowser);
+ let result = {
+ origin: locationObj.origin,
+ mode: {
+ identity: identityMode,
+ mixed_display: mixedDisplay,
+ mixed_active: mixedActive,
+ tracking: trackingMode
+ }
+ };
+
+ // Don't show identity data for pages with an unknown identity or if any
+ // mixed content is loaded (mixed display content is loaded by default).
+ // We also return for CHROMEUI pages since they don't have any certificate
+ // information to load either. result.secure specifically refers to connection
+ // security, which is irrelevant for about: pages, as they're loaded locally.
+ if (identityMode == this.IDENTITY_MODE_UNKNOWN ||
+ identityMode == this.IDENTITY_MODE_CHROMEUI ||
+ aState & Ci.nsIWebProgressListener.STATE_IS_BROKEN) {
+ result.secure = false;
+ return result;
+ }
+
+ result.secure = true;
+
+ result.host = this.getEffectiveHost();
+
+ let iData = this.getIdentityData();
+ result.verifier = Strings.browser.formatStringFromName("identity.identified.verifier", [iData.caOrg], 1);
+
+ // If the cert is identified, then we can populate the results with credentials
+ if (aState & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL) {
+ result.owner = iData.subjectOrg;
+
+ // Build an appropriate supplemental block out of whatever location data we have
+ let supplemental = "";
+ if (iData.city) {
+ supplemental += iData.city + "\n";
+ }
+ if (iData.state && iData.country) {
+ supplemental += Strings.browser.formatStringFromName("identity.identified.state_and_country", [iData.state, iData.country], 2);
+ result.country = iData.country;
+ } else if (iData.state) { // State only
+ supplemental += iData.state;
+ } else if (iData.country) { // Country only
+ supplemental += iData.country;
+ result.country = iData.country;
+ }
+ result.supplemental = supplemental;
+
+ return result;
+ }
+
+ // Cache the override service the first time we need to check it
+ if (!this._overrideService)
+ this._overrideService = Cc["@mozilla.org/security/certoverride;1"].getService(Ci.nsICertOverrideService);
+
+ // Check whether this site is a security exception. XPConnect does the right
+ // thing here in terms of converting _lastLocation.port from string to int, but
+ // the overrideService doesn't like undefined ports, so make sure we have
+ // something in the default case (bug 432241).
+ // .hostname can return an empty string in some exceptional cases -
+ // hasMatchingOverride does not handle that, so avoid calling it.
+ // Updating the tooltip value in those cases isn't critical.
+ // FIXME: Fixing bug 646690 would probably makes this check unnecessary
+ if (this._lastLocation.hostname &&
+ this._overrideService.hasMatchingOverride(this._lastLocation.hostname,
+ (this._lastLocation.port || 443),
+ iData.cert, {}, {}))
+ result.verifier = Strings.browser.GetStringFromName("identity.identified.verified_by_you");
+
+ return result;
+ },
+
+ /**
+ * Attempt to provide proper IDN treatment for host names
+ */
+ getEffectiveHost: function getEffectiveHost() {
+ if (!this._IDNService)
+ this._IDNService = Cc["@mozilla.org/network/idn-service;1"]
+ .getService(Ci.nsIIDNService);
+ try {
+ return this._IDNService.convertToDisplayIDN(this._uri.host, {});
+ } catch (e) {
+ // If something goes wrong (e.g. hostname is an IP address) just fail back
+ // to the full domain.
+ return this._lastLocation.hostname;
+ }
+ }
+};
+
+var SearchEngines = {
+ _contextMenuId: null,
+ PREF_SUGGEST_ENABLED: "browser.search.suggest.enabled",
+ PREF_SUGGEST_PROMPTED: "browser.search.suggest.prompted",
+
+ // Shared preference key used for search activity default engine.
+ PREF_SEARCH_ACTIVITY_ENGINE_KEY: "search.engines.defaultname",
+
+ init: function init() {
+ Services.obs.addObserver(this, "SearchEngines:Add", false);
+ Services.obs.addObserver(this, "SearchEngines:GetVisible", false);
+ Services.obs.addObserver(this, "SearchEngines:Remove", false);
+ Services.obs.addObserver(this, "SearchEngines:RestoreDefaults", false);
+ Services.obs.addObserver(this, "SearchEngines:SetDefault", false);
+ Services.obs.addObserver(this, "browser-search-engine-modified", false);
+ },
+
+ // Fetch list of search engines. all ? All engines : Visible engines only.
+ _handleSearchEnginesGetVisible: function _handleSearchEnginesGetVisible(rv, all) {
+ if (!Components.isSuccessCode(rv)) {
+ Cu.reportError("Could not initialize search service, bailing out.");
+ return;
+ }
+
+ let engineData = Services.search.getVisibleEngines({});
+
+ // Our Java UI assumes that the default engine is the first item in the array,
+ // so we need to make sure that's the case.
+ if (engineData[0] !== Services.search.defaultEngine) {
+ engineData = engineData.filter(engine => engine !== Services.search.defaultEngine);
+ engineData.unshift(Services.search.defaultEngine);
+ }
+
+ let searchEngines = engineData.map(function (engine) {
+ return {
+ name: engine.name,
+ identifier: engine.identifier,
+ iconURI: (engine.iconURI ? engine.iconURI.spec : null),
+ hidden: engine.hidden
+ };
+ });
+
+ let suggestTemplate = null;
+ let suggestEngine = null;
+
+ // Check to see if the default engine supports search suggestions. We only need to check
+ // the default engine because we only show suggestions for the default engine in the UI.
+ let engine = Services.search.defaultEngine;
+ if (engine.supportsResponseType("application/x-suggestions+json")) {
+ suggestEngine = engine.name;
+ suggestTemplate = engine.getSubmission("__searchTerms__", "application/x-suggestions+json").uri.spec;
+ }
+
+ // By convention, the currently configured default engine is at position zero in searchEngines.
+ Messaging.sendRequest({
+ type: "SearchEngines:Data",
+ searchEngines: searchEngines,
+ suggest: {
+ engine: suggestEngine,
+ template: suggestTemplate,
+ enabled: Services.prefs.getBoolPref(this.PREF_SUGGEST_ENABLED),
+ prompted: Services.prefs.getBoolPref(this.PREF_SUGGEST_PROMPTED)
+ }
+ });
+
+ // Send a speculative connection to the default engine.
+ Services.search.defaultEngine.speculativeConnect({window: window});
+ },
+
+ // Helper method to extract the engine name from a JSON. Simplifies the observe function.
+ _extractEngineFromJSON: function _extractEngineFromJSON(aData) {
+ let data = JSON.parse(aData);
+ return Services.search.getEngineByName(data.engine);
+ },
+
+ observe: function observe(aSubject, aTopic, aData) {
+ let engine;
+ switch(aTopic) {
+ case "SearchEngines:Add":
+ this.displaySearchEnginesList(aData);
+ break;
+ case "SearchEngines:GetVisible":
+ Services.search.init(this._handleSearchEnginesGetVisible.bind(this));
+ break;
+ case "SearchEngines:Remove":
+ // Make sure the engine isn't hidden before removing it, to make sure it's
+ // visible if the user later re-adds it (works around bug 341833)
+ engine = this._extractEngineFromJSON(aData);
+ engine.hidden = false;
+ Services.search.removeEngine(engine);
+ break;
+ case "SearchEngines:RestoreDefaults":
+ // Un-hides all default engines.
+ Services.search.restoreDefaultEngines();
+ break;
+ case "SearchEngines:SetDefault":
+ engine = this._extractEngineFromJSON(aData);
+ // Move the new default search engine to the top of the search engine list.
+ Services.search.moveEngine(engine, 0);
+ Services.search.defaultEngine = engine;
+ break;
+ case "browser-search-engine-modified":
+ if (aData == "engine-default") {
+ this._setSearchActivityDefaultPref(aSubject.QueryInterface(Ci.nsISearchEngine));
+ }
+ break;
+ default:
+ dump("Unexpected message type observed: " + aTopic);
+ break;
+ }
+ },
+
+ migrateSearchActivityDefaultPref: function migrateSearchActivityDefaultPref() {
+ Services.search.init(() => this._setSearchActivityDefaultPref(Services.search.defaultEngine));
+ },
+
+ // Updates the search activity pref when the default engine changes.
+ _setSearchActivityDefaultPref: function _setSearchActivityDefaultPref(engine) {
+ SharedPreferences.forApp().setCharPref(this.PREF_SEARCH_ACTIVITY_ENGINE_KEY, engine.name);
+ },
+
+ // Display context menu listing names of the search engines available to be added.
+ displaySearchEnginesList: function displaySearchEnginesList(aData) {
+ let data = JSON.parse(aData);
+ let tab = BrowserApp.getTabForId(data.tabId);
+
+ if (!tab)
+ return;
+
+ let browser = tab.browser;
+ let engines = browser.engines;
+
+ let p = new Prompt({
+ window: browser.contentWindow
+ }).setSingleChoiceItems(engines.map(function(e) {
+ return { label: e.title };
+ })).show((function(data) {
+ if (data.button == -1)
+ return;
+
+ this.addOpenSearchEngine(engines[data.button]);
+ engines.splice(data.button, 1);
+
+ if (engines.length < 1) {
+ // Broadcast message that there are no more add-able search engines.
+ let newEngineMessage = {
+ type: "Link:OpenSearch",
+ tabID: tab.id,
+ visible: false
+ };
+
+ Messaging.sendRequest(newEngineMessage);
+ }
+ }).bind(this));
+ },
+
+ addOpenSearchEngine: function addOpenSearchEngine(engine) {
+ Services.search.addEngine(engine.url, Ci.nsISearchEngine.DATA_XML, engine.iconURL, false, {
+ onSuccess: function() {
+ // Display a toast confirming addition of new search engine.
+ Snackbars.show(Strings.browser.formatStringFromName("alertSearchEngineAddedToast", [engine.title], 1), Snackbars.LENGTH_LONG);
+ },
+
+ onError: function(aCode) {
+ let errorMessage;
+ if (aCode == 2) {
+ // Engine is a duplicate.
+ errorMessage = "alertSearchEngineDuplicateToast";
+
+ } else {
+ // Unknown failure. Display general error message.
+ errorMessage = "alertSearchEngineErrorToast";
+ }
+
+ Snackbars.show(Strings.browser.formatStringFromName(errorMessage, [engine.title], 1), Snackbars.LENGTH_LONG);
+ }
+ });
+ },
+
+ /**
+ * Build and return an array of sorted form data / Query Parameters
+ * for an element in a submission form.
+ *
+ * @param element
+ * A valid submission element of a form.
+ */
+ _getSortedFormData: function(element) {
+ let formData = [];
+
+ for (let formElement of element.form.elements) {
+ if (!formElement.type) {
+ continue;
+ }
+
+ // Make this text field a generic search parameter.
+ if (element == formElement) {
+ formData.push({ name: formElement.name, value: "{searchTerms}" });
+ continue;
+ }
+
+ // Add other form elements as parameters.
+ switch (formElement.type.toLowerCase()) {
+ case "checkbox":
+ case "radio":
+ if (!formElement.checked) {
+ break;
+ }
+ case "text":
+ case "hidden":
+ case "textarea":
+ formData.push({ name: escape(formElement.name), value: escape(formElement.value) });
+ break;
+
+ case "select-one": {
+ for (let option of formElement.options) {
+ if (option.selected) {
+ formData.push({ name: escape(formElement.name), value: escape(formElement.value) });
+ break;
+ }
+ }
+ }
+ }
+ };
+
+ // Return valid, pre-sorted queryParams.
+ return formData.filter(a => a.name && a.value).sort((a, b) => {
+ // nsIBrowserSearchService.hasEngineWithURL() ensures sort, but this helps.
+ if (a.name > b.name) {
+ return 1;
+ }
+ if (b.name > a.name) {
+ return -1;
+ }
+
+ if (a.value > b.value) {
+ return 1;
+ }
+ if (b.value > a.value) {
+ return -1;
+ }
+
+ return 0;
+ });
+ },
+
+ /**
+ * Check if any search engines already handle an EngineURL of type
+ * URLTYPE_SEARCH_HTML, matching this request-method, formURL, and queryParams.
+ */
+ visibleEngineExists: function(element) {
+ let formData = this._getSortedFormData(element);
+
+ let form = element.form;
+ let method = form.method.toUpperCase();
+
+ let charset = element.ownerDocument.characterSet;
+ let docURI = Services.io.newURI(element.ownerDocument.URL, charset, null);
+ let formURL = Services.io.newURI(form.getAttribute("action"), charset, docURI).spec;
+
+ return Services.search.hasEngineWithURL(method, formURL, formData);
+ },
+
+ /**
+ * Adds a new search engine to the BrowserSearchService, based on its provided element. Prompts for an engine
+ * name, and appends a simple version-number in case of collision with an existing name.
+ *
+ * @return callback to handle success value. Currently used for ActionBarHandler.js and UI updates.
+ */
+ addEngine: function addEngine(aElement, resultCallback) {
+ 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 = this._getSortedFormData(aElement);
+
+ // prompt user for name of search engine
+ let promptTitle = Strings.browser.GetStringFromName("contextmenu.addSearchEngine3");
+ let title = { value: (aElement.ownerDocument.title || docURI.host) };
+ if (!Services.prompt.prompt(null, promptTitle, null, title, null, {})) {
+ if (resultCallback) {
+ resultCallback(false);
+ };
+ 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 history_with_favicons WHERE url = ?");
+ stmts[0].bindByIndex(0, docURI.spec);
+ let favicon = null;
+
+ Services.search.init(function addEngine_cb(rv) {
+ if (!Components.isSuccessCode(rv)) {
+ Cu.reportError("Could not initialize search service, bailing out.");
+ if (resultCallback) {
+ resultCallback(false);
+ };
+ return;
+ }
+
+ mDBConn.executeAsync(stmts, stmts.length, {
+ handleResult: function (results) {
+ let bytes = results.getNextRow().getResultByName("favicon");
+ if (bytes && bytes.length) {
+ favicon = "data:image/x-icon;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);
+ Snackbars.show(Strings.browser.formatStringFromName("alertSearchEngineAddedToast", [name], 1), Snackbars.LENGTH_LONG);
+
+ let engine = Services.search.getEngineByName(name);
+ engine.wrappedJSObject._queryCharset = charset;
+ formData.forEach(param => { engine.addParam(param.name, param.value, null); });
+
+ if (resultCallback) {
+ return resultCallback(true);
+ };
+ }
+ });
+ });
+ }
+};
+
+var ActivityObserver = {
+ init: function ao_init() {
+ Services.obs.addObserver(this, "application-background", false);
+ Services.obs.addObserver(this, "application-foreground", false);
+ },
+
+ observe: function ao_observe(aSubject, aTopic, aData) {
+ let isForeground = false;
+ let tab = BrowserApp.selectedTab;
+
+ UITelemetry.addEvent("show.1", "system", null, aTopic);
+
+ switch (aTopic) {
+ case "application-background" :
+ let doc = (tab ? tab.browser.contentDocument : null);
+ if (doc && doc.fullscreenElement) {
+ doc.exitFullscreen();
+ }
+ isForeground = false;
+ break;
+ case "application-foreground" :
+ isForeground = true;
+ break;
+ }
+
+ if (tab && tab.getActive() != isForeground) {
+ tab.setActive(isForeground);
+ }
+ }
+};
+
+var Telemetry = {
+ addData: function addData(aHistogramId, aValue) {
+ let histogram = Services.telemetry.getHistogramById(aHistogramId);
+ histogram.add(aValue);
+ },
+};
+
+var Experiments = {
+ // Enable malware download protection (bug 936041)
+ MALWARE_DOWNLOAD_PROTECTION: "malware-download-protection",
+
+ // Try to load pages from disk cache when network is offline (bug 935190)
+ OFFLINE_CACHE: "offline-cache",
+
+ init() {
+ Messaging.sendRequestForResult({
+ type: "Experiments:GetActive"
+ }).then(experiments => {
+ let names = JSON.parse(experiments);
+ for (let name of names) {
+ switch (name) {
+ case this.MALWARE_DOWNLOAD_PROTECTION: {
+ // Apply experiment preferences on the default branch. This allows
+ // us to avoid migrating user prefs when experiments are enabled/disabled,
+ // and it also allows users to override these prefs in about:config.
+ let defaults = Services.prefs.getDefaultBranch(null);
+ defaults.setBoolPref("browser.safebrowsing.downloads.enabled", true);
+ defaults.setBoolPref("browser.safebrowsing.downloads.remote.enabled", true);
+ continue;
+ }
+
+ case this.OFFLINE_CACHE: {
+ let defaults = Services.prefs.getDefaultBranch(null);
+ defaults.setBoolPref("browser.tabs.useCache", true);
+ continue;
+ }
+ }
+ }
+ });
+ },
+
+ setOverride(name, isEnabled) {
+ Messaging.sendRequest({
+ type: "Experiments:SetOverride",
+ name: name,
+ isEnabled: isEnabled
+ });
+ },
+
+ clearOverride(name) {
+ Messaging.sendRequest({
+ type: "Experiments:ClearOverride",
+ name: name
+ });
+ }
+};
+
+var ExternalApps = {
+ _contextMenuId: null,
+
+ // extend _getLink to pickup html5 media links.
+ _getMediaLink: function(aElement) {
+ let uri = NativeWindow.contextmenus._getLink(aElement);
+ if (uri == null && aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE && (aElement instanceof Ci.nsIDOMHTMLMediaElement)) {
+ try {
+ let mediaSrc = aElement.currentSrc || aElement.src;
+ uri = ContentAreaUtils.makeURI(mediaSrc, null, null);
+ } catch (e) {}
+ }
+ return uri;
+ },
+
+ init: function helper_init() {
+ this._contextMenuId = NativeWindow.contextmenus.add(function(aElement) {
+ let uri = null;
+ var node = aElement;
+ while (node && !uri) {
+ uri = ExternalApps._getMediaLink(node);
+ node = node.parentNode;
+ }
+ let apps = [];
+ if (uri)
+ apps = HelperApps.getAppsForUri(uri);
+
+ return apps.length == 1 ? Strings.browser.formatStringFromName("helperapps.openWithApp2", [apps[0].name], 1) :
+ Strings.browser.GetStringFromName("helperapps.openWithList2");
+ }, this.filter, this.openExternal);
+ },
+
+ filter: {
+ matches: function(aElement) {
+ let uri = ExternalApps._getMediaLink(aElement);
+ let apps = [];
+ if (uri) {
+ apps = HelperApps.getAppsForUri(uri);
+ }
+ return apps.length > 0;
+ }
+ },
+
+ openExternal: function(aElement) {
+ if (aElement.pause) {
+ aElement.pause();
+ }
+ let uri = ExternalApps._getMediaLink(aElement);
+ HelperApps.launchUri(uri);
+ },
+
+ shouldCheckUri: function(uri) {
+ if (!(uri.schemeIs("http") || uri.schemeIs("https") || uri.schemeIs("file"))) {
+ return false;
+ }
+
+ return true;
+ },
+
+ updatePageAction: function updatePageAction(uri, contentDocument) {
+ HelperApps.getAppsForUri(uri, { filterHttp: true }, (apps) => {
+ this.clearPageAction();
+ if (apps.length > 0)
+ this._setUriForPageAction(uri, apps, contentDocument);
+ });
+ },
+
+ updatePageActionUri: function updatePageActionUri(uri) {
+ this._pageActionUri = uri;
+ },
+
+ _getMediaContentElement(contentDocument) {
+ if (!contentDocument.contentType.startsWith("video/") &&
+ !contentDocument.contentType.startsWith("audio/")) {
+ return null;
+ }
+
+ let element = contentDocument.activeElement;
+
+ if (element instanceof HTMLBodyElement) {
+ element = element.firstChild;
+ }
+
+ if (element instanceof HTMLMediaElement) {
+ return element;
+ }
+
+ return null;
+ },
+
+ _setUriForPageAction: function setUriForPageAction(uri, apps, contentDocument) {
+ this.updatePageActionUri(uri);
+
+ // If the pageaction is already added, simply update the URI to be launched when 'onclick' is triggered.
+ if (this._pageActionId != undefined)
+ return;
+
+ let mediaElement = this._getMediaContentElement(contentDocument);
+
+ this._pageActionId = PageActions.add({
+ title: Strings.browser.GetStringFromName("openInApp.pageAction"),
+ icon: "drawable://icon_openinapp",
+
+ clickCallback: () => {
+ UITelemetry.addEvent("launch.1", "pageaction", null, "helper");
+
+ let wasPlaying = mediaElement && !mediaElement.paused && !mediaElement.ended;
+ if (wasPlaying) {
+ mediaElement.pause();
+ }
+
+ if (apps.length > 1) {
+ // Use the HelperApps prompt here to filter out any Http handlers
+ HelperApps.prompt(apps, {
+ title: Strings.browser.GetStringFromName("openInApp.pageAction"),
+ buttons: [
+ Strings.browser.GetStringFromName("openInApp.ok"),
+ Strings.browser.GetStringFromName("openInApp.cancel")
+ ]
+ }, (result) => {
+ if (result.button != 0) {
+ if (wasPlaying) {
+ mediaElement.play();
+ }
+
+ return;
+ }
+ apps[result.icongrid0].launch(this._pageActionUri);
+ });
+ } else {
+ apps[0].launch(this._pageActionUri);
+ }
+ }
+ });
+ },
+
+ clearPageAction: function clearPageAction() {
+ if(!this._pageActionId)
+ return;
+
+ PageActions.remove(this._pageActionId);
+ delete this._pageActionId;
+ },
+};
+
+var Distribution = {
+ // File used to store campaign data
+ _file: null,
+
+ _preferencesJSON: null,
+
+ init: function dc_init() {
+ Services.obs.addObserver(this, "Distribution:Changed", false);
+ Services.obs.addObserver(this, "Distribution:Set", false);
+ Services.obs.addObserver(this, "prefservice:after-app-defaults", false);
+ Services.obs.addObserver(this, "Campaign:Set", false);
+
+ // Look for file outside the APK:
+ // /data/data/org.mozilla.xxx/distribution.json
+ this._file = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
+ this._file.append("distribution.json");
+ this.readJSON(this._file, this.update);
+ },
+
+ observe: function dc_observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "Distribution:Changed":
+ // Re-init the search service.
+ try {
+ Services.search._asyncReInit();
+ } catch (e) {
+ console.log("Unable to reinit search service.");
+ }
+ // Fall through.
+
+ case "Distribution:Set":
+ if (aData) {
+ try {
+ this._preferencesJSON = JSON.parse(aData);
+ } catch (e) {
+ console.log("Invalid distribution JSON.");
+ }
+ }
+ // Reload the default prefs so we can observe "prefservice:after-app-defaults"
+ Services.prefs.QueryInterface(Ci.nsIObserver).observe(null, "reload-default-prefs", null);
+ this.installDistroAddons();
+ break;
+
+ case "prefservice:after-app-defaults":
+ this.getPrefs();
+ break;
+
+ case "Campaign:Set": {
+ // Update the prefs for this session
+ try {
+ this.update(JSON.parse(aData));
+ } catch (ex) {
+ Cu.reportError("Distribution: Could not parse JSON: " + ex);
+ return;
+ }
+
+ // Asynchronously copy the data to the file.
+ let array = new TextEncoder().encode(aData);
+ OS.File.writeAtomic(this._file.path, array, { tmpPath: this._file.path + ".tmp" });
+ break;
+ }
+ }
+ },
+
+ update: function dc_update(aData) {
+ // Force the distribution preferences on the default branch
+ let defaults = Services.prefs.getDefaultBranch(null);
+ defaults.setCharPref("distribution.id", aData.id);
+ defaults.setCharPref("distribution.version", aData.version);
+ },
+
+ getPrefs: function dc_getPrefs() {
+ if (this._preferencesJSON) {
+ this.applyPrefs(this._preferencesJSON);
+ this._preferencesJSON = null;
+ return;
+ }
+
+ // Get the distribution directory, and bail if it doesn't exist.
+ let file = FileUtils.getDir("XREAppDist", [], false);
+ if (!file.exists())
+ return;
+
+ file.append("preferences.json");
+ this.readJSON(file, this.applyPrefs);
+ },
+
+ applyPrefs: function dc_applyPrefs(aData) {
+ // Check for required Global preferences
+ let global = aData["Global"];
+ if (!(global && global["id"] && global["version"] && global["about"])) {
+ Cu.reportError("Distribution: missing or incomplete Global preferences");
+ return;
+ }
+
+ // Force the distribution preferences on the default branch
+ let defaults = Services.prefs.getDefaultBranch(null);
+ defaults.setCharPref("distribution.id", global["id"]);
+ defaults.setCharPref("distribution.version", global["version"]);
+
+ let locale = BrowserApp.getUALocalePref();
+ let aboutString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
+ aboutString.data = global["about." + locale] || global["about"];
+ defaults.setComplexValue("distribution.about", Ci.nsISupportsString, aboutString);
+
+ let prefs = aData["Preferences"];
+ for (let key in prefs) {
+ try {
+ let value = prefs[key];
+ switch (typeof value) {
+ case "boolean":
+ defaults.setBoolPref(key, value);
+ break;
+ case "number":
+ defaults.setIntPref(key, value);
+ break;
+ case "string":
+ case "undefined":
+ defaults.setCharPref(key, value);
+ break;
+ }
+ } catch (e) { /* ignore bad prefs and move on */ }
+ }
+
+ // Apply a lightweight theme if necessary
+ if (prefs && prefs["lightweightThemes.selectedThemeID"]) {
+ Services.obs.notifyObservers(null, "lightweight-theme-apply", "");
+ }
+
+ let localizedString = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(Ci.nsIPrefLocalizedString);
+ let localizeablePrefs = aData["LocalizablePreferences"];
+ for (let key in localizeablePrefs) {
+ try {
+ let value = localizeablePrefs[key];
+ value = value.replace(/%LOCALE%/g, locale);
+ localizedString.data = "data:text/plain," + key + "=" + value;
+ defaults.setComplexValue(key, Ci.nsIPrefLocalizedString, localizedString);
+ } catch (e) { /* ignore bad prefs and move on */ }
+ }
+
+ let localizeablePrefsOverrides = aData["LocalizablePreferences." + locale];
+ for (let key in localizeablePrefsOverrides) {
+ try {
+ let value = localizeablePrefsOverrides[key];
+ localizedString.data = "data:text/plain," + key + "=" + value;
+ defaults.setComplexValue(key, Ci.nsIPrefLocalizedString, localizedString);
+ } catch (e) { /* ignore bad prefs and move on */ }
+ }
+
+ Messaging.sendRequest({ type: "Distribution:Set:OK" });
+ },
+
+ // aFile is an nsIFile
+ // aCallback takes the parsed JSON object as a parameter
+ readJSON: function dc_readJSON(aFile, aCallback) {
+ Task.spawn(function() {
+ let bytes = yield OS.File.read(aFile.path);
+ let raw = new TextDecoder().decode(bytes) || "";
+
+ try {
+ aCallback(JSON.parse(raw));
+ } catch (e) {
+ Cu.reportError("Distribution: Could not parse JSON: " + e);
+ }
+ }).then(null, function onError(reason) {
+ if (!(reason instanceof OS.File.Error && reason.becauseNoSuchFile)) {
+ Cu.reportError("Distribution: Could not read from " + aFile.leafName + " file");
+ }
+ });
+ },
+
+ // Track pending installs so we can avoid showing notifications for them.
+ pendingAddonInstalls: new Set(),
+
+ installDistroAddons: Task.async(function* () {
+ const PREF_ADDONS_INSTALLED = "distribution.addonsInstalled";
+ try {
+ let installed = Services.prefs.getBoolPref(PREF_ADDONS_INSTALLED);
+ if (installed) {
+ return;
+ }
+ } catch (e) {
+ Services.prefs.setBoolPref(PREF_ADDONS_INSTALLED, true);
+ }
+
+ let distroPath;
+ try {
+ distroPath = FileUtils.getDir("XREAppDist", ["extensions"]).path;
+
+ let info = yield OS.File.stat(distroPath);
+ if (!info.isDir) {
+ return;
+ }
+ } catch (e) {
+ return;
+ }
+
+ let it = new OS.File.DirectoryIterator(distroPath);
+ try {
+ yield it.forEach(entry => {
+ // Only support extensions that are zipped in .xpi files.
+ if (entry.isDir || !entry.name.endsWith(".xpi")) {
+ dump("Ignoring distribution add-on that isn't an XPI: " + entry.path);
+ return;
+ }
+
+ new Promise((resolve, reject) => {
+ AddonManager.getInstallForFile(new FileUtils.File(entry.path), resolve);
+ }).then(install => {
+ let id = entry.name.substring(0, entry.name.length - 4);
+ if (install.addon.id !== id) {
+ Cu.reportError("File entry " + entry.path + " contains an add-on with an incorrect ID");
+ return;
+ }
+ this.pendingAddonInstalls.add(install);
+ install.install();
+ }).catch(e => {
+ Cu.reportError("Error installing distribution add-on: " + entry.path + ": " + e);
+ });
+ });
+ } finally {
+ it.close();
+ }
+ })
+};
+
+var Tabs = {
+ _enableTabExpiration: false,
+ _useCache: false,
+ _domains: new Set(),
+
+ init: function() {
+ // On low-memory platforms, always allow tab expiration. On high-mem
+ // platforms, allow it to be turned on once we hit a low-mem situation.
+ if (BrowserApp.isOnLowMemoryPlatform) {
+ this._enableTabExpiration = true;
+ } else {
+ Services.obs.addObserver(this, "memory-pressure", false);
+ }
+
+ // Watch for opportunities to pre-connect to high probability targets.
+ Services.obs.addObserver(this, "Session:Prefetch", false);
+
+ // Track the network connection so we can efficiently use the cache
+ // for possible offline rendering.
+ Services.obs.addObserver(this, "network:link-status-changed", false);
+ let network = Cc["@mozilla.org/network/network-link-service;1"].getService(Ci.nsINetworkLinkService);
+ this.useCache = !network.isLinkUp;
+
+ BrowserApp.deck.addEventListener("pageshow", this, false);
+ BrowserApp.deck.addEventListener("TabOpen", this, false);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "memory-pressure":
+ if (aData != "heap-minimize") {
+ // We received a low-memory related notification. This will enable
+ // expirations.
+ this._enableTabExpiration = true;
+ Services.obs.removeObserver(this, "memory-pressure");
+ } else {
+ // Use "heap-minimize" as a trigger to expire the most stale tab.
+ this.expireLruTab();
+ }
+ break;
+ case "Session:Prefetch":
+ if (aData) {
+ try {
+ let uri = Services.io.newURI(aData, null, null);
+ if (uri && !this._domains.has(uri.host)) {
+ Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null);
+ this._domains.add(uri.host);
+ }
+ } catch (e) {}
+ }
+ break;
+ case "network:link-status-changed":
+ if (["down", "unknown", "up"].indexOf(aData) == -1) {
+ return;
+ }
+ this.useCache = (aData === "down");
+ break;
+ }
+ },
+
+ handleEvent: function(aEvent) {
+ switch (aEvent.type) {
+ case "pageshow":
+ // Clear the domain cache whenever a page is loaded into any browser.
+ this._domains.clear();
+
+ break;
+ case "TabOpen":
+ // Use opening a new tab as a trigger to expire the most stale tab.
+ this.expireLruTab();
+ break;
+ }
+ },
+
+ // Manage the most-recently-used list of tabs. Each tab has a timestamp
+ // associated with it that indicates when it was last touched.
+ expireLruTab: function() {
+ if (!this._enableTabExpiration) {
+ return false;
+ }
+ let expireTimeMs = Services.prefs.getIntPref("browser.tabs.expireTime") * 1000;
+ if (expireTimeMs < 0) {
+ // This behaviour is disabled.
+ return false;
+ }
+ let tabs = BrowserApp.tabs;
+ let selected = BrowserApp.selectedTab;
+ let lruTab = null;
+ // Find the least recently used non-zombie tab.
+ for (let i = 0; i < tabs.length; i++) {
+ if (tabs[i] == selected ||
+ tabs[i].browser.__SS_restore ||
+ tabs[i].playingAudio) {
+ // This tab is selected, is already a zombie, or is currently playing
+ // audio, skip it.
+ continue;
+ }
+ if (lruTab == null || tabs[i].lastTouchedAt < lruTab.lastTouchedAt) {
+ lruTab = tabs[i];
+ }
+ }
+ // If the tab was last touched more than browser.tabs.expireTime seconds ago,
+ // zombify it.
+ if (lruTab) {
+ if (Date.now() - lruTab.lastTouchedAt > expireTimeMs) {
+ MemoryObserver.zombify(lruTab);
+ return true;
+ }
+ }
+ return false;
+ },
+
+ get useCache() {
+ if (!Services.prefs.getBoolPref("browser.tabs.useCache")) {
+ return false;
+ }
+ return this._useCache;
+ },
+
+ set useCache(aUseCache) {
+ if (!Services.prefs.getBoolPref("browser.tabs.useCache")) {
+ return;
+ }
+
+ if (this._useCache == aUseCache) {
+ return;
+ }
+
+ BrowserApp.tabs.forEach(function(tab) {
+ if (tab.browser && tab.browser.docShell) {
+ if (aUseCache) {
+ tab.browser.docShell.defaultLoadFlags |= Ci.nsIRequest.LOAD_FROM_CACHE;
+ } else {
+ tab.browser.docShell.defaultLoadFlags &= ~Ci.nsIRequest.LOAD_FROM_CACHE;
+ }
+ }
+ });
+ this._useCache = aUseCache;
+ },
+
+ // For debugging
+ dump: function(aPrefix) {
+ let tabs = BrowserApp.tabs;
+ for (let i = 0; i < tabs.length; i++) {
+ dump(aPrefix + " | " + "Tab [" + tabs[i].browser.contentWindow.location.href + "]: lastTouchedAt:" + tabs[i].lastTouchedAt + ", zombie:" + tabs[i].browser.__SS_restore);
+ }
+ },
+};
+
+function ContextMenuItem(args) {
+ this.id = uuidgen.generateUUID().toString();
+ this.args = args;
+}
+
+ContextMenuItem.prototype = {
+ get order() {
+ return this.args.order || 0;
+ },
+
+ matches: function(elt, x, y) {
+ return this.args.selector.matches(elt, x, y);
+ },
+
+ callback: function(elt) {
+ this.args.callback(elt);
+ },
+
+ addVal: function(name, elt, defaultValue) {
+ if (!(name in this.args))
+ return defaultValue;
+
+ if (typeof this.args[name] == "function")
+ return this.args[name](elt);
+
+ return this.args[name];
+ },
+
+ getValue: function(elt) {
+ return {
+ id: this.id,
+ label: this.addVal("label", elt),
+ showAsActions: this.addVal("showAsActions", elt),
+ icon: this.addVal("icon", elt),
+ isGroup: this.addVal("isGroup", elt, false),
+ inGroup: this.addVal("inGroup", elt, false),
+ disabled: this.addVal("disabled", elt, false),
+ selected: this.addVal("selected", elt, false),
+ isParent: this.addVal("isParent", elt, false),
+ };
+ }
+}
+
+function HTMLContextMenuItem(elt, target) {
+ ContextMenuItem.call(this, { });
+
+ this.menuElementRef = Cu.getWeakReference(elt);
+ this.targetElementRef = Cu.getWeakReference(target);
+}
+
+HTMLContextMenuItem.prototype = Object.create(ContextMenuItem.prototype, {
+ order: {
+ value: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER
+ },
+
+ matches: {
+ value: function(target) {
+ let t = this.targetElementRef.get();
+ return t === target;
+ },
+ },
+
+ callback: {
+ value: function(target) {
+ let elt = this.menuElementRef.get();
+ if (!elt) {
+ return;
+ }
+
+ // If this is a menu item, show a new context menu with the submenu in it
+ if (elt instanceof Ci.nsIDOMHTMLMenuElement) {
+ try {
+ NativeWindow.contextmenus.menus = {};
+
+ let elt = this.menuElementRef.get();
+ let target = this.targetElementRef.get();
+ if (!elt) {
+ return;
+ }
+
+ var items = NativeWindow.contextmenus._getHTMLContextMenuItemsForMenu(elt, target);
+ // This menu will always only have one context, but we still make sure its the "right" one.
+ var context = NativeWindow.contextmenus._getContextType(target);
+ if (items.length > 0) {
+ NativeWindow.contextmenus._addMenuItems(items, context);
+ }
+
+ } catch(ex) {
+ Cu.reportError(ex);
+ }
+ } else {
+ // otherwise just click the menu item
+ elt.click();
+ }
+ },
+ },
+
+ getValue: {
+ value: function(target) {
+ let elt = this.menuElementRef.get();
+ if (!elt) {
+ return null;
+ }
+
+ if (elt.hasAttribute("hidden")) {
+ return null;
+ }
+
+ return {
+ id: this.id,
+ icon: elt.icon,
+ label: elt.label,
+ disabled: elt.disabled,
+ menu: elt instanceof Ci.nsIDOMHTMLMenuElement
+ };
+ }
+ },
+});
+
diff --git a/mobile/android/chrome/content/browser.xul b/mobile/android/chrome/content/browser.xul
new file mode 100644
index 000000000..8072a7a1c
--- /dev/null
+++ b/mobile/android/chrome/content/browser.xul
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://browser/content/browser.css" type="text/css"?>
+
+<window id="main-window"
+ onload="BrowserApp.startup();"
+ windowtype="navigator:browser"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script type="application/javascript" src="chrome://browser/content/browser.js"/>
+
+ <deck id="browsers" flex="1"/>
+
+</window>
diff --git a/mobile/android/chrome/content/config.js b/mobile/android/chrome/content/config.js
new file mode 100644
index 000000000..2c868f175
--- /dev/null
+++ b/mobile/android/chrome/content/config.js
@@ -0,0 +1,673 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var {classes: Cc, interfaces: Ci, manager: Cm, utils: Cu} = Components;
+Cu.import("resource://gre/modules/Services.jsm");
+
+const VKB_ENTER_KEY = 13; // User press of VKB enter key
+const INITIAL_PAGE_DELAY = 500; // Initial pause on program start for scroll alignment
+const PREFS_BUFFER_MAX = 30; // Max prefs buffer size for getPrefsBuffer()
+const PAGE_SCROLL_TRIGGER = 200; // Triggers additional getPrefsBuffer() on user scroll-to-bottom
+const FILTER_CHANGE_TRIGGER = 200; // Delay between responses to filterInput changes
+const INNERHTML_VALUE_DELAY = 100; // Delay before providing prefs innerHTML value
+
+var gStringBundle = Services.strings.createBundle("chrome://browser/locale/config.properties");
+var gClipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
+
+
+/* ============================== NewPrefDialog ==============================
+ *
+ * New Preference Dialog Object and methods
+ *
+ * Implements User Interfaces for creation of a single(new) Preference setting
+ *
+ */
+var NewPrefDialog = {
+
+ _prefsShield: null,
+
+ _newPrefsDialog: null,
+ _newPrefItem: null,
+ _prefNameInputElt: null,
+ _prefTypeSelectElt: null,
+
+ _booleanValue: null,
+ _booleanToggle: null,
+ _stringValue: null,
+ _intValue: null,
+
+ _positiveButton: null,
+
+ get type() {
+ return this._prefTypeSelectElt.value;
+ },
+
+ set type(aType) {
+ this._prefTypeSelectElt.value = aType;
+ switch(this._prefTypeSelectElt.value) {
+ case "boolean":
+ this._prefTypeSelectElt.selectedIndex = 0;
+ break;
+ case "string":
+ this._prefTypeSelectElt.selectedIndex = 1;
+ break;
+ case "int":
+ this._prefTypeSelectElt.selectedIndex = 2;
+ break;
+ }
+
+ this._newPrefItem.setAttribute("typestyle", aType);
+ },
+
+ // Init the NewPrefDialog
+ init: function AC_init() {
+ this._prefsShield = document.getElementById("prefs-shield");
+
+ this._newPrefsDialog = document.getElementById("new-pref-container");
+ this._newPrefItem = document.getElementById("new-pref-item");
+ this._prefNameInputElt = document.getElementById("new-pref-name");
+ this._prefTypeSelectElt = document.getElementById("new-pref-type");
+
+ this._booleanValue = document.getElementById("new-pref-value-boolean");
+ this._stringValue = document.getElementById("new-pref-value-string");
+ this._intValue = document.getElementById("new-pref-value-int");
+
+ this._positiveButton = document.getElementById("positive-button");
+ },
+
+ // Called to update positive button to display text ("Create"/"Change), and enabled/disabled status
+ // As new pref name is initially displayed, re-focused, or modifed during user input
+ _updatePositiveButton: function AC_updatePositiveButton(aPrefName) {
+ this._positiveButton.textContent = gStringBundle.GetStringFromName("newPref.createButton");
+ this._positiveButton.setAttribute("disabled", true);
+ if (aPrefName == "") {
+ return;
+ }
+
+ // If item already in list, it's being changed, else added
+ let item = AboutConfig._list.filter(i => { return i.name == aPrefName });
+ if (item.length) {
+ this._positiveButton.textContent = gStringBundle.GetStringFromName("newPref.changeButton");
+ } else {
+ this._positiveButton.removeAttribute("disabled");
+ }
+ },
+
+ // When we want to cancel/hide an existing, or show a new pref dialog
+ toggleShowHide: function AC_toggleShowHide() {
+ if (this._newPrefsDialog.classList.contains("show")) {
+ this.hide();
+ } else {
+ this._show();
+ }
+ },
+
+ // When we want to show the new pref dialog / shield the prefs list
+ _show: function AC_show() {
+ this._newPrefsDialog.classList.add("show");
+ this._prefsShield.setAttribute("shown", true);
+
+ // Initial default field values
+ this._prefNameInputElt.value = "";
+ this._updatePositiveButton(this._prefNameInputElt.value);
+
+ this.type = "boolean";
+ this._booleanValue.value = "false";
+ this._stringValue.value = "";
+ this._intValue.value = "";
+
+ this._prefNameInputElt.focus();
+
+ window.addEventListener("keypress", this.handleKeypress, false);
+ },
+
+ // When we want to cancel/hide the new pref dialog / un-shield the prefs list
+ hide: function AC_hide() {
+ this._newPrefsDialog.classList.remove("show");
+ this._prefsShield.removeAttribute("shown");
+
+ window.removeEventListener("keypress", this.handleKeypress, false);
+ },
+
+ // Watch user key input so we can provide Enter key action, commit input values
+ handleKeypress: function AC_handleKeypress(aEvent) {
+ // Close our VKB on new pref enter key press
+ if (aEvent.keyCode == VKB_ENTER_KEY)
+ aEvent.target.blur();
+ },
+
+ // New prefs create dialog only allows creating a non-existing preference, doesn't allow for
+ // Changing an existing one on-the-fly, tap existing/displayed line item pref for that
+ create: function AC_create(aEvent) {
+ if (this._positiveButton.getAttribute("disabled") == "true") {
+ return;
+ }
+
+ switch(this.type) {
+ case "boolean":
+ Services.prefs.setBoolPref(this._prefNameInputElt.value, (this._booleanValue.value == "true") ? true : false);
+ break;
+ case "string":
+ Services.prefs.setCharPref(this._prefNameInputElt.value, this._stringValue.value);
+ break;
+ case "int":
+ Services.prefs.setIntPref(this._prefNameInputElt.value, this._intValue.value);
+ break;
+ }
+
+ // Ensure pref adds flushed to disk immediately
+ Services.prefs.savePrefFile(null);
+
+ this.hide();
+ },
+
+ // Display proper positive button text/state on new prefs name input focus
+ focusName: function AC_focusName(aEvent) {
+ this._updatePositiveButton(aEvent.target.value);
+ },
+
+ // Display proper positive button text/state as user changes new prefs name
+ updateName: function AC_updateName(aEvent) {
+ this._updatePositiveButton(aEvent.target.value);
+ },
+
+ // In new prefs dialog, bool prefs are <input type="text">, as they aren't yet tied to an
+ // Actual Services.prefs.*etBoolPref()
+ toggleBoolValue: function AC_toggleBoolValue() {
+ this._booleanValue.value = (this._booleanValue.value == "true" ? "false" : "true");
+ }
+}
+
+
+/* ============================== AboutConfig ==============================
+ *
+ * Main AboutConfig object and methods
+ *
+ * Implements User Interfaces for maintenance of a list of Preference settings
+ *
+ */
+var AboutConfig = {
+
+ contextMenuLINode: null,
+ filterInput: null,
+ _filterPrevInput: null,
+ _filterChangeTimer: null,
+ _prefsContainer: null,
+ _loadingContainer: null,
+ _list: null,
+
+ // Init the main AboutConfig dialog
+ init: function AC_init() {
+ this.filterInput = document.getElementById("filter-input");
+ this._prefsContainer = document.getElementById("prefs-container");
+ this._loadingContainer = document.getElementById("loading-container");
+
+ let list = Services.prefs.getChildList("");
+ this._list = list.sort().map( function AC_getMapPref(aPref) {
+ return new Pref(aPref);
+ }, this);
+
+ // Support filtering about:config via a ?filter=<string> param
+ let match = /[?&]filter=([^&]+)/i.exec(window.location.href);
+ if (match) {
+ this.filterInput.value = decodeURIComponent(match[1]);
+ }
+
+ // Display the current prefs list (retains searchFilter value)
+ this.bufferFilterInput();
+
+ // Setup the prefs observers
+ Services.prefs.addObserver("", this, false);
+ },
+
+ // Uninit the main AboutConfig dialog
+ uninit: function AC_uninit() {
+ // Remove the prefs observer
+ Services.prefs.removeObserver("", this);
+ },
+
+ // Clear the filterInput value, to display the entire list
+ clearFilterInput: function AC_clearFilterInput() {
+ this.filterInput.value = "";
+ this.bufferFilterInput();
+ },
+
+ // Buffer down rapid changes in filterInput value from keyboard
+ bufferFilterInput: function AC_bufferFilterInput() {
+ if (this._filterChangeTimer) {
+ clearTimeout(this._filterChangeTimer);
+ }
+
+ this._filterChangeTimer = setTimeout((function() {
+ this._filterChangeTimer = null;
+ // Display updated prefs list when filterInput value settles
+ this._displayNewList();
+ }).bind(this), FILTER_CHANGE_TRIGGER);
+ },
+
+ // Update displayed list when filterInput value changes
+ _displayNewList: function AC_displayNewList() {
+ // This survives the search filter value past a page refresh
+ this.filterInput.setAttribute("value", this.filterInput.value);
+
+ // Don't start new filter search if same as last
+ if (this.filterInput.value == this._filterPrevInput) {
+ return;
+ }
+ this._filterPrevInput = this.filterInput.value;
+
+ // Clear list item selection / context menu, prefs list, get first buffer, set scrolling on
+ this.selected = "";
+ this._clearPrefsContainer();
+ this._addMorePrefsToContainer();
+ window.onscroll = this.onScroll.bind(this);
+
+ // Pause for screen to settle, then ensure at top
+ setTimeout((function() {
+ window.scrollTo(0, 0);
+ }).bind(this), INITIAL_PAGE_DELAY);
+ },
+
+ // Clear the displayed preferences list
+ _clearPrefsContainer: function AC_clearPrefsContainer() {
+ // Quick clear the prefsContainer list
+ let empty = this._prefsContainer.cloneNode(false);
+ this._prefsContainer.parentNode.replaceChild(empty, this._prefsContainer);
+ this._prefsContainer = empty;
+
+ // Quick clear the prefs li.HTML list
+ this._list.forEach(function(item) {
+ delete item.li;
+ });
+ },
+
+ // Get a small manageable block of prefs items, and add them to the displayed list
+ _addMorePrefsToContainer: function AC_addMorePrefsToContainer() {
+ // Create filter regex
+ let filterExp = this.filterInput.value ?
+ new RegExp(this.filterInput.value, "i") : null;
+
+ // Get a new block for the display list
+ let prefsBuffer = [];
+ for (let i = 0; i < this._list.length && prefsBuffer.length < PREFS_BUFFER_MAX; i++) {
+ if (!this._list[i].li && this._list[i].test(filterExp)) {
+ prefsBuffer.push(this._list[i]);
+ }
+ }
+
+ // Add the new block to the displayed list
+ for (let i = 0; i < prefsBuffer.length; i++) {
+ this._prefsContainer.appendChild(prefsBuffer[i].getOrCreateNewLINode());
+ }
+
+ // Determine if anything left to add later by scrolling
+ let anotherPrefsBufferRemains = false;
+ for (let i = 0; i < this._list.length; i++) {
+ if (!this._list[i].li && this._list[i].test(filterExp)) {
+ anotherPrefsBufferRemains = true;
+ break;
+ }
+ }
+
+ if (anotherPrefsBufferRemains) {
+ // If still more could be displayed, show the throbber
+ this._loadingContainer.style.display = "block";
+ } else {
+ // If no more could be displayed, hide the throbber, and stop noticing scroll events
+ this._loadingContainer.style.display = "none";
+ window.onscroll = null;
+ }
+ },
+
+ // If scrolling at the bottom, maybe add some more entries
+ onScroll: function AC_onScroll(aEvent) {
+ if (this._prefsContainer.scrollHeight - (window.pageYOffset + window.innerHeight) < PAGE_SCROLL_TRIGGER) {
+ if (!this._filterChangeTimer) {
+ this._addMorePrefsToContainer();
+ }
+ }
+ },
+
+
+ // Return currently selected list item node
+ get selected() {
+ return document.querySelector(".pref-item.selected");
+ },
+
+ // Set list item node as selected
+ set selected(aSelection) {
+ let currentSelection = this.selected;
+ if (aSelection == currentSelection) {
+ return;
+ }
+
+ // Clear any previous selection
+ if (currentSelection) {
+ currentSelection.classList.remove("selected");
+ currentSelection.removeEventListener("keypress", this.handleKeypress, false);
+ }
+
+ // Set any current selection
+ if (aSelection) {
+ aSelection.classList.add("selected");
+ aSelection.addEventListener("keypress", this.handleKeypress, false);
+ }
+ },
+
+ // Watch user key input so we can provide Enter key action, commit input values
+ handleKeypress: function AC_handleKeypress(aEvent) {
+ if (aEvent.keyCode == VKB_ENTER_KEY)
+ aEvent.target.blur();
+ },
+
+ // Return the target list item node of an action event
+ getLINodeForEvent: function AC_getLINodeForEvent(aEvent) {
+ let node = aEvent.target;
+ while (node && node.nodeName != "li") {
+ node = node.parentNode;
+ }
+
+ return node;
+ },
+
+ // Return a pref of a list item node
+ _getPrefForNode: function AC_getPrefForNode(aNode) {
+ let pref = aNode.getAttribute("name");
+
+ return new Pref(pref);
+ },
+
+ // When list item name or value are tapped
+ selectOrToggleBoolPref: function AC_selectOrToggleBoolPref(aEvent) {
+ let node = this.getLINodeForEvent(aEvent);
+
+ // If not already selected, just do so
+ if (this.selected != node) {
+ this.selected = node;
+ return;
+ }
+
+ // If already selected, and value is boolean, toggle it
+ let pref = this._getPrefForNode(node);
+ if (pref.type != Services.prefs.PREF_BOOL) {
+ return;
+ }
+
+ this.toggleBoolPref(aEvent);
+ },
+
+ // When finalizing list input values due to blur
+ setIntOrStringPref: function AC_setIntOrStringPref(aEvent) {
+ let node = this.getLINodeForEvent(aEvent);
+
+ // Skip if locked
+ let pref = this._getPrefForNode(node);
+ if (pref.locked) {
+ return;
+ }
+
+ // Boolean inputs blur to remove focus from "button"
+ if (pref.type == Services.prefs.PREF_BOOL) {
+ return;
+ }
+
+ // String and Int inputs change / commit on blur
+ pref.value = aEvent.target.value;
+ },
+
+ // When we reset a pref to it's default value (note resetting a user created pref will delete it)
+ resetDefaultPref: function AC_resetDefaultPref(aEvent) {
+ let node = this.getLINodeForEvent(aEvent);
+
+ // If not already selected, do so
+ if (this.selected != node) {
+ this.selected = node;
+ }
+
+ // Reset will handle any locked condition
+ let pref = this._getPrefForNode(node);
+ pref.reset();
+
+ // Ensure pref reset flushed to disk immediately
+ Services.prefs.savePrefFile(null);
+ },
+
+ // When we want to toggle a bool pref
+ toggleBoolPref: function AC_toggleBoolPref(aEvent) {
+ let node = this.getLINodeForEvent(aEvent);
+
+ // Skip if locked, or not boolean
+ let pref = this._getPrefForNode(node);
+ if (pref.locked) {
+ return;
+ }
+
+ // Toggle, and blur to remove field focus
+ pref.value = !pref.value;
+ aEvent.target.blur();
+ },
+
+ // When Int inputs have their Up or Down arrows toggled
+ incrOrDecrIntPref: function AC_incrOrDecrIntPref(aEvent, aInt) {
+ let node = this.getLINodeForEvent(aEvent);
+
+ // Skip if locked
+ let pref = this._getPrefForNode(node);
+ if (pref.locked) {
+ return;
+ }
+
+ pref.value += aInt;
+ },
+
+ // Observe preference changes
+ observe: function AC_observe(aSubject, aTopic, aPrefName) {
+ let pref = new Pref(aPrefName);
+
+ // Ignore uninteresting changes, and avoid "private" preferences
+ if (aTopic != "nsPref:changed") {
+ return;
+ }
+
+ // If pref type invalid, refresh display as user reset/removed an item from the list
+ if (pref.type == Services.prefs.PREF_INVALID) {
+ document.location.reload();
+ return;
+ }
+
+ // If pref onscreen, update in place.
+ let item = document.querySelector(".pref-item[name=\"" + CSS.escape(pref.name) + "\"]");
+ if (item) {
+ item.setAttribute("value", pref.value);
+ let input = item.querySelector("input");
+ input.setAttribute("value", pref.value);
+ input.value = pref.value;
+
+ pref.default ?
+ item.querySelector(".reset").setAttribute("disabled", "true") :
+ item.querySelector(".reset").removeAttribute("disabled");
+ return;
+ }
+
+ // If pref not already in list, refresh display as it's being added
+ let anyWhere = this._list.filter(i => { return i.name == pref.name });
+ if (!anyWhere.length) {
+ document.location.reload();
+ }
+ },
+
+ // Quick context menu helpers for about:config
+ clipboardCopy: function AC_clipboardCopy(aField) {
+ let pref = this._getPrefForNode(this.contextMenuLINode);
+ if (aField == 'name') {
+ gClipboardHelper.copyString(pref.name);
+ } else {
+ gClipboardHelper.copyString(pref.value);
+ }
+ }
+}
+
+
+/* ============================== Pref ==============================
+ *
+ * Individual Preference object / methods
+ *
+ * Defines a Pref object, a document list item tied to Preferences Services
+ * And the methods by which they interact.
+ *
+ */
+function Pref(aName) {
+ this.name = aName;
+}
+
+Pref.prototype = {
+ get type() {
+ return Services.prefs.getPrefType(this.name);
+ },
+
+ get value() {
+ switch (this.type) {
+ case Services.prefs.PREF_BOOL:
+ return Services.prefs.getBoolPref(this.name);
+ case Services.prefs.PREF_INT:
+ return Services.prefs.getIntPref(this.name);
+ case Services.prefs.PREF_STRING:
+ default:
+ return Services.prefs.getCharPref(this.name);
+ }
+
+ },
+ set value(aPrefValue) {
+ switch (this.type) {
+ case Services.prefs.PREF_BOOL:
+ Services.prefs.setBoolPref(this.name, aPrefValue);
+ break;
+ case Services.prefs.PREF_INT:
+ Services.prefs.setIntPref(this.name, aPrefValue);
+ break;
+ case Services.prefs.PREF_STRING:
+ default:
+ Services.prefs.setCharPref(this.name, aPrefValue);
+ }
+
+ // Ensure pref change flushed to disk immediately
+ Services.prefs.savePrefFile(null);
+ },
+
+ get default() {
+ return !Services.prefs.prefHasUserValue(this.name);
+ },
+
+ get locked() {
+ return Services.prefs.prefIsLocked(this.name);
+ },
+
+ reset: function AC_reset() {
+ Services.prefs.clearUserPref(this.name);
+ },
+
+ test: function AC_test(aValue) {
+ return aValue ? aValue.test(this.name) : true;
+ },
+
+ // Get existing or create new LI node for the pref
+ getOrCreateNewLINode: function AC_getOrCreateNewLINode() {
+ if (!this.li) {
+ this.li = document.createElement("li");
+
+ this.li.className = "pref-item";
+ this.li.setAttribute("name", this.name);
+
+ // Click callback to ensure list item selected even on no-action tap events
+ this.li.addEventListener("click",
+ function(aEvent) {
+ AboutConfig.selected = AboutConfig.getLINodeForEvent(aEvent);
+ },
+ false
+ );
+
+ // Contextmenu callback to identify selected list item
+ this.li.addEventListener("contextmenu",
+ function(aEvent) {
+ AboutConfig.contextMenuLINode = AboutConfig.getLINodeForEvent(aEvent);
+ },
+ false
+ );
+
+ this.li.setAttribute("contextmenu", "prefs-context-menu");
+
+ // Create list item outline, bind to object actions
+ this.li.innerHTML =
+ "<div class='pref-name' " +
+ "onclick='AboutConfig.selectOrToggleBoolPref(event);'>" +
+ this.name +
+ "</div>" +
+ "<div class='pref-item-line'>" +
+ "<input class='pref-value' value='' " +
+ "onblur='AboutConfig.setIntOrStringPref(event);' " +
+ "onclick='AboutConfig.selectOrToggleBoolPref(event);'>" +
+ "</input>" +
+ "<div class='pref-button reset' " +
+ "onclick='AboutConfig.resetDefaultPref(event);'>" +
+ gStringBundle.GetStringFromName("pref.resetButton") +
+ "</div>" +
+ "<div class='pref-button toggle' " +
+ "onclick='AboutConfig.toggleBoolPref(event);'>" +
+ gStringBundle.GetStringFromName("pref.toggleButton") +
+ "</div>" +
+ "<div class='pref-button up' " +
+ "onclick='AboutConfig.incrOrDecrIntPref(event, 1);'>" +
+ "</div>" +
+ "<div class='pref-button down' " +
+ "onclick='AboutConfig.incrOrDecrIntPref(event, -1);'>" +
+ "</div>" +
+ "</div>";
+
+ // Delay providing the list item values, until the LI is returned and added to the document
+ setTimeout(this._valueSetup.bind(this), INNERHTML_VALUE_DELAY);
+ }
+
+ return this.li;
+ },
+
+ // Initialize list item object values
+ _valueSetup: function AC_valueSetup() {
+
+ this.li.setAttribute("type", this.type);
+ this.li.setAttribute("value", this.value);
+
+ let valDiv = this.li.querySelector(".pref-value");
+ valDiv.value = this.value;
+
+ switch(this.type) {
+ case Services.prefs.PREF_BOOL:
+ valDiv.setAttribute("type", "button");
+ this.li.querySelector(".up").setAttribute("disabled", true);
+ this.li.querySelector(".down").setAttribute("disabled", true);
+ break;
+ case Services.prefs.PREF_STRING:
+ valDiv.setAttribute("type", "text");
+ this.li.querySelector(".up").setAttribute("disabled", true);
+ this.li.querySelector(".down").setAttribute("disabled", true);
+ this.li.querySelector(".toggle").setAttribute("disabled", true);
+ break;
+ case Services.prefs.PREF_INT:
+ valDiv.setAttribute("type", "number");
+ this.li.querySelector(".toggle").setAttribute("disabled", true);
+ break;
+ }
+
+ this.li.setAttribute("default", this.default);
+ if (this.default) {
+ this.li.querySelector(".reset").setAttribute("disabled", true);
+ }
+
+ if (this.locked) {
+ valDiv.setAttribute("disabled", this.locked);
+ this.li.querySelector(".pref-name").setAttribute("locked", true);
+ }
+ }
+}
+
diff --git a/mobile/android/chrome/content/config.xhtml b/mobile/android/chrome/content/config.xhtml
new file mode 100644
index 000000000..fd40bb517
--- /dev/null
+++ b/mobile/android/chrome/content/config.xhtml
@@ -0,0 +1,86 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [
+<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" >
+%globalDTD;
+<!ENTITY % configDTD SYSTEM "chrome://browser/locale/config.dtd">
+%configDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+<head>
+ <meta name="viewport" content="width=device-width; user-scalable=0" />
+
+ <link rel="stylesheet" href="chrome://browser/skin/config.css" type="text/css"/>
+ <script type="text/javascript;version=1.8" src="chrome://browser/content/config.js"></script>
+</head>
+
+<body dir="&locale.dir;" onload="NewPrefDialog.init(); AboutConfig.init();"
+ onunload="AboutConfig.uninit();">
+
+ <div class="toolbar">
+ <div class="toolbar-container">
+ <div id="new-pref-toggle-button" onclick="NewPrefDialog.toggleShowHide();"/>
+
+ <div class="toolbar-item" id="filter-container">
+ <div id="filter-search-button"/>
+ <input id="filter-input" type="search" placeholder="&toolbar.searchPlaceholder;" value=""
+ oninput="AboutConfig.bufferFilterInput();"/>
+ <div id="filter-input-clear-button" onclick="AboutConfig.clearFilterInput();"/>
+ </div>
+ </div>
+ </div>
+
+ <div id="content" ontouchstart="AboutConfig.filterInput.blur();">
+
+ <div id="new-pref-container">
+ <li class="pref-item" id="new-pref-item">
+ <div class="pref-item-line">
+ <input class="pref-name" id="new-pref-name" type="text" placeholder="&newPref.namePlaceholder;"
+ onfocus="NewPrefDialog.focusName(event);"
+ oninput="NewPrefDialog.updateName(event);"/>
+ <select class="pref-value" id="new-pref-type" onchange="NewPrefDialog.type = event.target.value;">
+ <option value="boolean">&newPref.valueBoolean;</option>
+ <option value="string">&newPref.valueString;</option>
+ <option value="int">&newPref.valueInteger;</option>
+ </select>
+ </div>
+
+ <div class="pref-item-line" id="new-pref-line-boolean">
+ <input class="pref-value" id="new-pref-value-boolean" disabled="disabled"/>
+ <div class="pref-button toggle" onclick="NewPrefDialog.toggleBoolValue();">&newPref.toggleButton;</div>
+ </div>
+
+ <div class="pref-item-line">
+ <input class="pref-value" id="new-pref-value-string" placeholder="&newPref.stringPlaceholder;"/>
+ <input class="pref-value" id="new-pref-value-int" placeholder="&newPref.numberPlaceholder;" type="number"/>
+ </div>
+
+ <div class="pref-item-line">
+ <div class="pref-button cancel" id="negative-button" onclick="NewPrefDialog.hide();">&newPref.cancelButton;</div>
+ <div class="pref-button create" id="positive-button" onclick="NewPrefDialog.create(event);"></div>
+ </div>
+ </li>
+ </div>
+
+ <div id="prefs-shield"></div>
+
+ <ul id="prefs-container"/>
+
+ <ul id="loading-container"><li></li></ul>
+
+ </div>
+
+ <menu type="context" id="prefs-context-menu">
+ <menuitem label="&contextMenu.copyPrefName;" onclick="AboutConfig.clipboardCopy('name');"></menuitem>
+ <menuitem label="&contextMenu.copyPrefValue;" onclick="AboutConfig.clipboardCopy('value');"></menuitem>
+ </menu>
+
+</body>
+</html>
diff --git a/mobile/android/chrome/content/content.js b/mobile/android/chrome/content/content.js
new file mode 100644
index 000000000..7cac22bd1
--- /dev/null
+++ b/mobile/android/chrome/content/content.js
@@ -0,0 +1,159 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/ExtensionContent.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AboutReader", "resource://gre/modules/AboutReader.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent", "resource://gre/modules/LoginManagerContent.jsm");
+
+var dump = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.d.bind(null, "Content");
+
+var global = this;
+
+// This is copied from desktop's tab-content.js. See bug 1153485 about sharing this code somehow.
+var AboutReaderListener = {
+
+ _articlePromise: null,
+
+ _isLeavingReaderMode: false,
+
+ init: function() {
+ addEventListener("AboutReaderContentLoaded", this, false, true);
+ addEventListener("DOMContentLoaded", this, false);
+ addEventListener("pageshow", this, false);
+ addEventListener("pagehide", this, false);
+ addMessageListener("Reader:ToggleReaderMode", this);
+ addMessageListener("Reader:PushState", this);
+ },
+
+ receiveMessage: function(message) {
+ switch (message.name) {
+ case "Reader:ToggleReaderMode":
+ let url = content.document.location.href;
+ if (!this.isAboutReader) {
+ this._articlePromise = ReaderMode.parseDocument(content.document).catch(Cu.reportError);
+ ReaderMode.enterReaderMode(docShell, content);
+ } else {
+ this._isLeavingReaderMode = true;
+ ReaderMode.leaveReaderMode(docShell, content);
+ }
+ break;
+
+ case "Reader:PushState":
+ this.updateReaderButton(!!(message.data && message.data.isArticle));
+ break;
+ }
+ },
+
+ get isAboutReader() {
+ return content.document.documentURI.startsWith("about:reader");
+ },
+
+ handleEvent: function(aEvent) {
+ if (aEvent.originalTarget.defaultView != content) {
+ return;
+ }
+
+ switch (aEvent.type) {
+ case "AboutReaderContentLoaded":
+ if (!this.isAboutReader) {
+ return;
+ }
+
+ // If we are restoring multiple reader mode tabs during session restore, duplicate "DOMContentLoaded"
+ // events may be fired for the visible tab. The inital "DOMContentLoaded" may be received before the
+ // document body is available, so we avoid instantiating an AboutReader object, expecting that a
+ // valid message will follow. See bug 925983.
+ if (content.document.body) {
+ new AboutReader(global, content, this._articlePromise);
+ this._articlePromise = null;
+ }
+ break;
+
+ case "pagehide":
+ // this._isLeavingReaderMode is used here to keep the Reader Mode icon
+ // visible in the location bar when transitioning from reader-mode page
+ // back to the source page.
+ sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: this._isLeavingReaderMode });
+ if (this._isLeavingReaderMode) {
+ this._isLeavingReaderMode = false;
+ }
+ break;
+
+ case "pageshow":
+ // If a page is loaded from the bfcache, we won't get a "DOMContentLoaded"
+ // event, so we need to rely on "pageshow" in this case.
+ if (aEvent.persisted) {
+ this.updateReaderButton();
+ }
+ break;
+ case "DOMContentLoaded":
+ this.updateReaderButton();
+ break;
+ }
+ },
+ updateReaderButton: function(forceNonArticle) {
+ if (!ReaderMode.isEnabledForParseOnLoad || this.isAboutReader ||
+ !(content.document instanceof content.HTMLDocument) ||
+ content.document.mozSyntheticDocument) {
+ return;
+ }
+
+ this.scheduleReadabilityCheckPostPaint(forceNonArticle);
+ },
+
+ cancelPotentialPendingReadabilityCheck: function() {
+ if (this._pendingReadabilityCheck) {
+ removeEventListener("MozAfterPaint", this._pendingReadabilityCheck);
+ delete this._pendingReadabilityCheck;
+ }
+ },
+
+ scheduleReadabilityCheckPostPaint: function(forceNonArticle) {
+ if (this._pendingReadabilityCheck) {
+ // We need to stop this check before we re-add one because we don't know
+ // if forceNonArticle was true or false last time.
+ this.cancelPotentialPendingReadabilityCheck();
+ }
+ this._pendingReadabilityCheck = this.onPaintWhenWaitedFor.bind(this, forceNonArticle);
+ addEventListener("MozAfterPaint", this._pendingReadabilityCheck);
+ },
+
+ onPaintWhenWaitedFor: function(forceNonArticle, event) {
+ // In non-e10s, we'll get called for paints other than ours, and so it's
+ // possible that this page hasn't been laid out yet, in which case we
+ // should wait until we get an event that does relate to our layout. We
+ // determine whether any of our content got painted by checking if there
+ // are any painted rects.
+ if (!event.clientRects.length) {
+ return;
+ }
+
+ this.cancelPotentialPendingReadabilityCheck();
+
+ // Only send updates when there are articles; there's no point updating with
+ // |false| all the time.
+ if (ReaderMode.isProbablyReaderable(content.document)) {
+ sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: true });
+ } else if (forceNonArticle) {
+ sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: false });
+ }
+ },
+};
+AboutReaderListener.init();
+
+addMessageListener("RemoteLogins:fillForm", function(message) {
+ LoginManagerContent.receiveMessage(message, content);
+});
+
+ExtensionContent.init(this);
+addEventListener("unload", () => {
+ ExtensionContent.uninit(this);
+});
diff --git a/mobile/android/chrome/content/geckoview.js b/mobile/android/chrome/content/geckoview.js
new file mode 100644
index 000000000..b4685a8d3
--- /dev/null
+++ b/mobile/android/chrome/content/geckoview.js
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Log",
+ "resource://gre/modules/AndroidLog.jsm", "AndroidLog");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging",
+ "resource://gre/modules/Messaging.jsm", "Messaging");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm", "Services");
+
+function dump(msg) {
+ Log.d("View", msg);
+}
+
+function startup() {
+ dump("zerdatime " + Date.now() + " - geckoivew chrome startup finished.");
+
+ // Notify Java that Gecko has loaded.
+ Messaging.sendRequest({ type: "Gecko:Ready" });
+}
diff --git a/mobile/android/chrome/content/geckoview.xul b/mobile/android/chrome/content/geckoview.xul
new file mode 100644
index 000000000..a3d4d1290
--- /dev/null
+++ b/mobile/android/chrome/content/geckoview.xul
@@ -0,0 +1,16 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://browser/content/browser.css" type="text/css"?>
+
+<window id="main-window"
+ onload="startup();"
+ windowtype="navigator:browser"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <browser id="content" type="content-primary" src="https://mozilla.com" flex="1" remote="true"/>
+
+ <script type="application/javascript" src="chrome://browser/content/geckoview.js"/>
+</window>
diff --git a/mobile/android/chrome/content/healthreport-prefs.js b/mobile/android/chrome/content/healthreport-prefs.js
new file mode 100644
index 000000000..5c4a50d38
--- /dev/null
+++ b/mobile/android/chrome/content/healthreport-prefs.js
@@ -0,0 +1,6 @@
+#filter substitution
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+pref("datareporting.healthreport.about.reportUrl", "https://fhr.cdn.mozilla.net/%LOCALE%/mobile/");
diff --git a/mobile/android/chrome/content/languages.properties b/mobile/android/chrome/content/languages.properties
new file mode 100644
index 000000000..53d30d125
--- /dev/null
+++ b/mobile/android/chrome/content/languages.properties
@@ -0,0 +1,114 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# LOCALIZATION NOTE: do not localize
+af=Afrikaans
+ak=Akan
+ar=عربي
+as=অসমীয়া
+ast-ES=Asturianu
+be=Беларуская
+bg=Български
+bn-BD=বাংলা (বাংলাদেশ)
+bn-IN=বাংলা (ভারত)
+br-FR=Brezhoneg
+ca=català
+ca-valencia=català (valencià)
+cs=Čeština
+cy=Cymraeg
+da=Dansk
+de=Deutsch
+de-AT=Deutsch (Österreich)
+de-CH=Deutsch (Schweiz)
+de-DE=Deutsch (Deutschland)
+el=Ελληνικά
+en-AU=English (Australian)
+en-CA=English (Canadian)
+en-GB=English (British)
+en-NZ=English (New Zealand)
+en-US=English (US)
+en-ZA=English (South African)
+eo=Esperanto
+es-AR=Español (de Argentina)
+es-CL=Español (de Chile)
+es-ES=Español (de España)
+es-MX=Español (de México)
+et=Eesti keel
+eu=Euskara
+fa=فارسی
+fi=suomi
+fr=Français
+fur-IT=Furlan
+fy-NL=Frysk
+ga-IE=Gaeilge
+gl=Galego
+gu-IN=ગુજરાતી
+he=עברית
+hi=हिन्दी
+hi-IN=हिन्दी (भारत)
+hr=Hrvatski
+hsb=Hornjoserbsce
+hu=Magyar
+hy-AM=Հայերեն
+id=Bahasa Indonesia
+is=íslenska
+it=Italiano
+ja=日本語
+ka=ქართული
+kk=Қазақ
+kn=ಕನ್ನಡ
+ko=한국어
+ku=Kurdî
+la=Latina
+lt=lietuvių
+lv=Latviešu
+mg=Malagasy
+mi=Māori (Aotearoa)
+mk=Македонски
+ml=മലയാളം
+mn=Монгол
+mr=मराठी
+nb-NO=Norsk bokmål
+ne-NP=नेपाली
+nl=Nederlands
+nn-NO=Norsk nynorsk
+nr=isiNdebele Sepumalanga
+nso=Sepedi
+oc=occitan (lengadocian)
+or=ଓଡ଼ିଆ
+pa-IN=ਪੰਜਾਬੀ
+pl=Polski
+pt-BR=Português (do Brasil)
+pt-PT=Português (Europeu)
+rm=rumantsch
+ro=română
+ru=Русский
+rw=Ikinyarwanda
+si=සිංහල
+sk=slovenčina
+sl=slovensko
+sq=Shqip
+sr=Српски
+sr-Latn=Srpski
+ss=Siswati
+st=Sesotho
+sv-SE=Svenska
+ta=தமிழ்
+ta-IN=தமிழ் (இந்தியா)
+ta-LK=தமிழ் (இலங்கை)
+te=తెలుగు
+th=ไทย
+tn=Setswana
+tr=Türkçe
+ts=Mutsonga
+tt-RU=Tatarça
+uk=Українська
+ur=اُردو
+ve=Tshivenḓa
+vi=Tiếng Việt
+wo=Wolof
+xh=isiXhosa
+zh-CN=中文 (简体)
+zh-TW=正體中文 (繁體)
+zu=isiZulu
diff --git a/mobile/android/chrome/content/netError.xhtml b/mobile/android/chrome/content/netError.xhtml
new file mode 100644
index 000000000..f4c727c06
--- /dev/null
+++ b/mobile/android/chrome/content/netError.xhtml
@@ -0,0 +1,406 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD
+ PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % netErrorDTD
+ SYSTEM "chrome://global/locale/netError.dtd">
+ %netErrorDTD;
+ <!ENTITY % globalDTD
+ SYSTEM "chrome://global/locale/global.dtd">
+ %globalDTD;
+]>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta name="viewport" content="width=device-width; user-scalable=false;" />
+ <title>&loadError.label;</title>
+ <link rel="stylesheet" href="chrome://global/skin/netError.css" type="text/css" media="all" />
+ <!-- If the location of the favicon is changed here, the FAVICON_ERRORPAGE_URL symbol in
+ toolkit/components/places/src/nsFaviconService.h should be updated. -->
+ <link rel="icon" type="image/png" id="favicon" sizes="64x64" href="chrome://browser/skin/images/errorpage-warning.png"/>
+
+ <script type="application/javascript"><![CDATA[
+ // Error url MUST be formatted like this:
+ // moz-neterror:page?e=error&u=url&d=desc
+ //
+ // or optionally, to specify an alternate CSS class to allow for
+ // custom styling and favicon:
+ //
+ // moz-neterror:page?e=error&u=url&s=classname&d=desc
+
+ // Note that this file uses document.documentURI to get
+ // the URL (with the format from above). This is because
+ // document.location.href gets the current URI off the docshell,
+ // which is the URL displayed in the location bar, i.e.
+ // the URI that the user attempted to load.
+
+ function getErrorCode()
+ {
+ var url = document.documentURI;
+ var error = url.search(/e\=/);
+ var duffUrl = url.search(/\&u\=/);
+ return decodeURIComponent(url.slice(error + 2, duffUrl));
+ }
+
+ function getCSSClass()
+ {
+ var url = document.documentURI;
+ var matches = url.match(/s\=([^&]+)\&/);
+ // s is optional, if no match just return nothing
+ if (!matches || matches.length < 2)
+ return "";
+
+ // parenthetical match is the second entry
+ return decodeURIComponent(matches[1]);
+ }
+
+ function getDescription()
+ {
+ var url = document.documentURI;
+ var desc = url.search(/d\=/);
+
+ // desc == -1 if not found; if so, return an empty string
+ // instead of what would turn out to be portions of the URI
+ if (desc == -1)
+ return "";
+
+ return decodeURIComponent(url.slice(desc + 2));
+ }
+
+ function retryThis(buttonEl)
+ {
+ // Note: The application may wish to handle switching off "offline mode"
+ // before this event handler runs, but using a capturing event handler.
+
+ // Session history has the URL of the page that failed
+ // to load, not the one of the error page. So, just call
+ // reload(), which will also repost POST data correctly.
+ try {
+ location.reload();
+ } catch (e) {
+ // We probably tried to reload a URI that caused an exception to
+ // occur; e.g. a nonexistent file.
+ }
+ }
+
+ function initPage()
+ {
+ var err = getErrorCode();
+
+ // if it's an unknown error or there's no title or description
+ // defined, get the generic message
+ var errTitle = document.getElementById("et_" + err);
+ var errDesc = document.getElementById("ed_" + err);
+ if (!errTitle || !errDesc)
+ {
+ errTitle = document.getElementById("et_generic");
+ errDesc = document.getElementById("ed_generic");
+ }
+
+ var title = document.getElementsByClassName("errorTitleText")[0];
+ if (title)
+ {
+ title.parentNode.replaceChild(errTitle, title);
+ // change id to the replaced child's id so styling works
+ errTitle.classList.add("errorTitleText");
+ }
+
+ var sd = document.getElementById("errorShortDescText");
+ if (sd)
+ sd.textContent = getDescription();
+
+ var ld = document.getElementById("errorLongDesc");
+ if (ld)
+ {
+ ld.parentNode.replaceChild(errDesc, ld);
+ // change id to the replaced child's id so styling works
+ errDesc.id = "errorLongDesc";
+ }
+
+ // remove undisplayed errors to avoid bug 39098
+ var errContainer = document.getElementById("errorContainer");
+ errContainer.parentNode.removeChild(errContainer);
+
+ var className = getCSSClass();
+ if (className && className != "expertBadCert") {
+ // Associate a CSS class with the root of the page, if one was passed in,
+ // to allow custom styling.
+ // Not "expertBadCert" though, don't want to deal with the favicon
+ document.documentElement.className = className;
+
+ // Also, if they specified a CSS class, they must supply their own
+ // favicon. In order to trigger the browser to repaint though, we
+ // need to remove/add the link element.
+ var favicon = document.getElementById("favicon");
+ var faviconParent = favicon.parentNode;
+ faviconParent.removeChild(favicon);
+ favicon.setAttribute("href", "chrome://global/skin/icons/" + className + "_favicon.png");
+ faviconParent.appendChild(favicon);
+ }
+ if (className == "expertBadCert") {
+ showSecuritySection();
+ }
+
+ if (err == "remoteXUL") {
+ // Remove the "Try again" button for remote XUL errors given that
+ // it is useless.
+ document.getElementById("errorTryAgain").style.display = "none";
+ }
+
+ if (err == "cspBlocked") {
+ // Remove the "Try again" button for CSP violations, since it's
+ // almost certainly useless. (Bug 553180)
+ document.getElementById("errorTryAgain").style.display = "none";
+ }
+
+ if (err == "nssBadCert") {
+ // Remove the "Try again" button for security exceptions, since it's
+ // almost certainly useless.
+ document.getElementById("errorTryAgain").style.display = "none";
+ document.getElementById("errorPage").setAttribute("class", "certerror");
+ }
+ else {
+ // Remove the override block for non-certificate errors. CSS-hiding
+ // isn't good enough here, because of bug 39098
+ var secOverride = document.getElementById("securityOverrideDiv");
+ secOverride.parentNode.removeChild(secOverride);
+ }
+
+ if (err == "inadequateSecurityError") {
+ // Remove the "Try again" button for HTTP/2 inadequate security as it
+ // is useless.
+ document.getElementById("errorTryAgain").style.display = "none";
+
+ var container = document.getElementById("errorLongDesc");
+ for (var span of container.querySelectorAll("span.hostname")) {
+ span.textContent = document.location.hostname;
+ }
+ }
+
+ addDomainErrorLinks();
+ }
+
+ function showSecuritySection() {
+ // Swap link out, content in
+ document.getElementById('securityOverrideContent').style.display = '';
+ document.getElementById('securityOverrideLink').style.display = 'none';
+ }
+
+ /* Try to preserve the links contained in the error description, like
+ the error code.
+
+ Also, in the case of SSL error pages about domain mismatch, see if
+ we can hyperlink the user to the correct site. We don't want
+ to do this generically since it allows MitM attacks to redirect
+ users to a site under attacker control, but in certain cases
+ it is safe (and helpful!) to do so. Bug 402210
+ */
+ function addDomainErrorLinks() {
+ // Rather than textContent, we need to treat description as HTML
+ var sd = document.getElementById("errorShortDescText");
+ if (sd) {
+ var desc = getDescription();
+
+ // sanitize description text - see bug 441169
+
+ // First, find the index of the <a> tags we care about, being
+ // careful not to use an over-greedy regex.
+ var codeRe = /<a id="errorCode" title="([^"]+)">/;
+ var codeResult = codeRe.exec(desc);
+ var domainRe = /<a id="cert_domain_link" title="([^"]+)">/;
+ var domainResult = domainRe.exec(desc);
+
+ // The order of these links in the description is fixed in
+ // TransportSecurityInfo.cpp:formatOverridableCertErrorMessage.
+ var firstResult = domainResult;
+ if(!domainResult)
+ firstResult = codeResult;
+ if (!firstResult)
+ return;
+ // Remove sd's existing children
+ sd.textContent = "";
+
+ // Everything up to the first link should be text content.
+ sd.appendChild(document.createTextNode(desc.slice(0, firstResult.index)));
+
+ // Now create the actual links.
+ if (domainResult) {
+ createLink(sd, "cert_domain_link", domainResult[1])
+ // Append text for anything between the two links.
+ sd.appendChild(document.createTextNode(desc.slice(desc.indexOf("</a>") + "</a>".length, codeResult.index)));
+ }
+ createLink(sd, "errorCode", codeResult[1])
+
+ // Finally, append text for anything after the last closing </a>.
+ sd.appendChild(document.createTextNode(desc.slice(desc.lastIndexOf("</a>") + "</a>".length)));
+ }
+
+ // Initialize the cert domain link.
+ var link = document.getElementById('cert_domain_link');
+ if (!link)
+ return;
+
+ var okHost = link.getAttribute("title");
+ var thisHost = document.location.hostname;
+ var proto = document.location.protocol;
+
+ // If okHost is a wildcard domain ("*.example.com") let's
+ // use "www" instead. "*.example.com" isn't going to
+ // get anyone anywhere useful. bug 432491
+ okHost = okHost.replace(/^\*\./, "www.");
+
+ /* case #1:
+ * example.com uses an invalid security certificate.
+ *
+ * The certificate is only valid for www.example.com
+ *
+ * Make sure to include the "." ahead of thisHost so that
+ * a MitM attack on paypal.com doesn't hyperlink to "notpaypal.com"
+ *
+ * We'd normally just use a RegExp here except that we lack a
+ * library function to escape them properly (bug 248062), and
+ * domain names are famous for having '.' characters in them,
+ * which would allow spurious and possibly hostile matches.
+ */
+ if (okHost.endsWith("." + thisHost))
+ link.href = proto + okHost;
+
+ /* case #2:
+ * browser.garage.maemo.org uses an invalid security certificate.
+ *
+ * The certificate is only valid for garage.maemo.org
+ */
+ if (thisHost.endsWith("." + okHost))
+ link.href = proto + okHost;
+ }
+
+ function createLink(el, id, text) {
+ var anchorEl = document.createElement("a");
+ anchorEl.setAttribute("id", id);
+ anchorEl.setAttribute("title", text);
+ anchorEl.appendChild(document.createTextNode(text));
+ el.appendChild(anchorEl);
+ }
+ ]]></script>
+ </head>
+
+ <body id="errorPage" dir="&locale.dir;">
+
+ <!-- ERROR ITEM CONTAINER (removed during loading to avoid bug 39098) -->
+ <div id="errorContainer">
+ <div id="errorTitlesContainer">
+ <h1 id="et_generic">&generic.title;</h1>
+ <h1 id="et_dnsNotFound">&dnsNotFound.title;</h1>
+ <h1 id="et_fileNotFound">&fileNotFound.title;</h1>
+ <h1 id="et_fileAccessDenied">&fileAccessDenied.title;</h1>
+ <h1 id="et_malformedURI">&malformedURI.title;</h1>
+ <h1 id="et_unknownProtocolFound">&unknownProtocolFound.title;</h1>
+ <h1 id="et_connectionFailure">&connectionFailure.title;</h1>
+ <h1 id="et_netTimeout">&netTimeout.title;</h1>
+ <h1 id="et_redirectLoop">&redirectLoop.title;</h1>
+ <h1 id="et_unknownSocketType">&unknownSocketType.title;</h1>
+ <h1 id="et_netReset">&netReset.title;</h1>
+ <h1 id="et_notCached">&notCached.title;</h1>
+
+ <!-- Since Fennec not yet have offline mode, change the title to
+ connectionFailure to prevent confusion -->
+ <h1 id="et_netOffline">&connectionFailure.title;</h1>
+
+ <h1 id="et_netInterrupt">&netInterrupt.title;</h1>
+ <h1 id="et_deniedPortAccess">&deniedPortAccess.title;</h1>
+ <h1 id="et_proxyResolveFailure">&proxyResolveFailure.title;</h1>
+ <h1 id="et_proxyConnectFailure">&proxyConnectFailure.title;</h1>
+ <h1 id="et_contentEncodingError">&contentEncodingError.title;</h1>
+ <h1 id="et_unsafeContentType">&unsafeContentType.title;</h1>
+ <h1 id="et_nssFailure2">&nssFailure2.title;</h1>
+ <h1 id="et_nssBadCert">&nssBadCert.title;</h1>
+ <h1 id="et_cspBlocked">&cspBlocked.title;</h1>
+ <h1 id="et_remoteXUL">&remoteXUL.title;</h1>
+ <h1 id="et_corruptedContentErrorv2">&corruptedContentErrorv2.title;</h1>
+ <h1 id="et_sslv3Used">&sslv3Used.title;</h1>
+ <h1 id="et_weakCryptoUsed">&weakCryptoUsed.title;</h1>
+ <h1 id="et_inadequateSecurityError">&inadequateSecurityError.title;</h1>
+ </div>
+ <div id="errorDescriptionsContainer">
+ <div id="ed_generic">&generic.longDesc;</div>
+ <div id="ed_dnsNotFound">&dnsNotFound.longDesc4;</div>
+ <div id="ed_fileNotFound">&fileNotFound.longDesc;</div>
+ <div id="ed_fileAccessDenied">&fileAccessDenied.longDesc;</div>
+ <div id="ed_malformedURI">&malformedURI.longDesc2;</div>
+ <div id="ed_unknownProtocolFound">&unknownProtocolFound.longDesc;</div>
+ <div id="ed_connectionFailure">&connectionFailure.longDesc2;</div>
+ <div id="ed_netTimeout">&netTimeout.longDesc2;</div>
+ <div id="ed_redirectLoop">&redirectLoop.longDesc;</div>
+ <div id="ed_unknownSocketType">&unknownSocketType.longDesc;</div>
+ <div id="ed_netReset">&netReset.longDesc2;</div>
+ <div id="ed_notCached">&notCached.longDesc;</div>
+
+ <!-- Change longDesc from netOffline to connectionFailure,
+ suggesting user to check their wifi/cell_data connection -->
+ <div id="ed_netOffline">&connectionFailure.longDesc2;</div>
+
+ <div id="ed_netInterrupt">&netInterrupt.longDesc2;</div>
+ <div id="ed_deniedPortAccess">&deniedPortAccess.longDesc;</div>
+ <div id="ed_proxyResolveFailure">&proxyResolveFailure.longDesc3;</div>
+ <div id="ed_proxyConnectFailure">&proxyConnectFailure.longDesc;</div>
+ <div id="ed_contentEncodingError">&contentEncodingError.longDesc;</div>
+ <div id="ed_unsafeContentType">&unsafeContentType.longDesc;</div>
+ <div id="ed_nssFailure2">&nssFailure2.longDesc2;</div>
+ <div id="ed_nssBadCert">&nssBadCert.longDesc2;</div>
+ <div id="ed_cspBlocked">&cspBlocked.longDesc;</div>
+ <div id="ed_remoteXUL">&remoteXUL.longDesc;</div>
+ <div id="ed_corruptedContentErrorv2">&corruptedContentErrorv2.longDesc;</div>
+ <div id="ed_sslv3Used">&sslv3Used.longDesc;</div>
+ <div id="ed_weakCryptoUsed">&weakCryptoUsed.longDesc;</div>
+ <div id="ed_inadequateSecurityError">&inadequateSecurityError.longDesc;</div>
+ </div>
+ </div>
+
+ <!-- PAGE CONTAINER (for styling purposes only) -->
+ <div id="errorPageContainer">
+
+ <!-- Error Title -->
+ <div id="errorTitle">
+ <h1 class="errorTitleText" />
+ </div>
+
+ <!-- LONG CONTENT (the section most likely to require scrolling) -->
+ <div id="errorLongContent">
+
+ <!-- Short Description -->
+ <div id="errorShortDesc">
+ <p id="errorShortDescText" />
+ </div>
+
+ <!-- Long Description (Note: See netError.dtd for used XHTML tags) -->
+ <div id="errorLongDesc" />
+
+ <!-- Override section - For ssl errors only. Removed on init for other
+ error types. -->
+ <div id="securityOverrideDiv">
+ <a id="securityOverrideLink" href="javascript:showSecuritySection();" >&securityOverride.linkText;</a>
+ <div id="securityOverrideContent" style="display: none;">&securityOverride.warningContent;</div>
+ </div>
+ </div>
+
+ <!-- Retry Button -->
+ <button id="errorTryAgain" onclick="retryThis(this);">&retry.label;</button>
+
+ </div>
+
+ <!--
+ - Note: It is important to run the script this way, instead of using
+ - an onload handler. This is because error pages are loaded as
+ - LOAD_BACKGROUND, which means that onload handlers will not be executed.
+ -->
+ <script type="application/javascript">initPage();</script>
+
+ </body>
+</html>
diff --git a/mobile/android/chrome/jar.mn b/mobile/android/chrome/jar.mn
new file mode 100644
index 000000000..538a025bd
--- /dev/null
+++ b/mobile/android/chrome/jar.mn
@@ -0,0 +1,71 @@
+#filter substitution
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+chrome.jar:
+% content browser %content/ contentaccessible=yes
+
+* content/about.xhtml (content/about.xhtml)
+* content/about.js (content/about.js)
+ content/config.xhtml (content/config.xhtml)
+ content/config.js (content/config.js)
+ content/content.js (content/content.js)
+ content/aboutAddons.xhtml (content/aboutAddons.xhtml)
+ content/aboutAddons.js (content/aboutAddons.js)
+ content/aboutCertError.xhtml (content/aboutCertError.xhtml)
+ content/aboutDownloads.xhtml (content/aboutDownloads.xhtml)
+ content/aboutDownloads.js (content/aboutDownloads.js)
+ content/aboutPrivateBrowsing.xhtml (content/aboutPrivateBrowsing.xhtml)
+ content/aboutPrivateBrowsing.js (content/aboutPrivateBrowsing.js)
+ content/Reader.js (content/Reader.js)
+ content/aboutHome.xhtml (content/aboutHome.xhtml)
+ content/aboutRights.xhtml (content/aboutRights.xhtml)
+ content/blockedSite.xhtml (content/blockedSite.xhtml)
+ content/languages.properties (content/languages.properties)
+ content/browser.xul (content/browser.xul)
+ content/browser.css (content/browser.css)
+ content/browser.js (content/browser.js)
+ content/geckoview.xul (content/geckoview.xul)
+ content/geckoview.js (content/geckoview.js)
+ content/PresentationView.xul (content/PresentationView.xul)
+ content/PresentationView.js (content/PresentationView.js)
+ content/bindings/checkbox.xml (content/bindings/checkbox.xml)
+ content/bindings/settings.xml (content/bindings/settings.xml)
+ content/netError.xhtml (content/netError.xhtml)
+ content/SelectHelper.js (content/SelectHelper.js)
+ content/ActionBarHandler.js (content/ActionBarHandler.js)
+ content/EmbedRT.js (content/EmbedRT.js)
+ content/InputWidgetHelper.js (content/InputWidgetHelper.js)
+ content/WebrtcUI.js (content/WebrtcUI.js)
+ content/MemoryObserver.js (content/MemoryObserver.js)
+ content/ConsoleAPI.js (content/ConsoleAPI.js)
+ content/PluginHelper.js (content/PluginHelper.js)
+ content/PrintHelper.js (content/PrintHelper.js)
+ content/OfflineApps.js (content/OfflineApps.js)
+ content/MasterPassword.js (content/MasterPassword.js)
+ content/FindHelper.js (content/FindHelper.js)
+ content/PermissionsHelper.js (content/PermissionsHelper.js)
+ content/FeedHandler.js (content/FeedHandler.js)
+ content/Feedback.js (content/Feedback.js)
+ content/Linkify.js (content/Linkify.js)
+ content/CastingApps.js (content/CastingApps.js)
+ content/RemoteDebugger.js (content/RemoteDebugger.js)
+#ifdef MOZ_SERVICES_HEALTHREPORT
+ content/aboutHealthReport.xhtml (content/aboutHealthReport.xhtml)
+ content/aboutHealthReport.js (content/aboutHealthReport.js)
+#endif
+ content/aboutAccounts.xhtml (content/aboutAccounts.xhtml)
+ content/aboutAccounts.js (content/aboutAccounts.js)
+ content/aboutLogins.xhtml (content/aboutLogins.xhtml)
+ content/aboutLogins.js (content/aboutLogins.js)
+#ifndef RELEASE_OR_BETA
+ content/WebcompatReporter.js (content/WebcompatReporter.js)
+#endif
+
+% content branding %content/branding/
+
+% override chrome://global/content/config.xul chrome://browser/content/config.xhtml
+% override chrome://global/content/netError.xhtml chrome://browser/content/netError.xhtml
+% override chrome://mozapps/content/extensions/extensions.xul chrome://browser/content/aboutAddons.xhtml
diff --git a/mobile/android/chrome/moz.build b/mobile/android/chrome/moz.build
new file mode 100644
index 000000000..e1610c2c7
--- /dev/null
+++ b/mobile/android/chrome/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEFINES['AB_CD'] = CONFIG['MOZ_UI_LOCALE']
+DEFINES['PACKAGE'] = 'browser'
+DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
+DEFINES['MOZ_APP_VERSION_DISPLAY'] = CONFIG['MOZ_APP_VERSION_DISPLAY']
+DEFINES['ANDROID_PACKAGE_NAME'] = CONFIG['ANDROID_PACKAGE_NAME']
+
+JAR_MANIFESTS += ['jar.mn']