diff options
Diffstat (limited to 'mobile/android/chrome/content')
54 files changed, 16321 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, "&"); + s = s.replace(/>/g, ">"); + s = s.replace(/</g, "<"); + s = s.replace(/"/g, """); + s = s.replace(/'/g, "'"); + 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">¬Cached.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">¬Cached.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> |