import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo

This commit is contained in:
Roy Tam 2018-01-19 03:59:58 +08:00
commit dcd9973243
150858 changed files with 23884658 additions and 0 deletions

View file

@ -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

View file

@ -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));
},
};

View file

@ -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;
}
}
};

View file

@ -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;
}
};

View file

@ -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);
}
}
}
};

View file

@ -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 });
});
}
};

View file

@ -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;
}
};

View file

@ -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;
}
}
};

View file

@ -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;
}
};

View file

@ -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);
}
};

View file

@ -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;
}
};

View file

@ -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);
},
};

View file

@ -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);
}
};

View file

@ -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);
}
}
};

View file

@ -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;
}
}
};

View file

@ -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 }));
}
}
};

View file

@ -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>

View file

@ -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 () {},
});
});
}
};

View file

@ -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");
}),
};

View file

@ -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);
}
}
};

View file

@ -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;
}
};

View file

@ -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");
});

View file

@ -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");
}
}

View file

@ -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);

View file

@ -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>

View file

@ -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());
}
});

View file

@ -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>

View file

@ -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);

View file

@ -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>

View file

@ -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>

View file

@ -0,0 +1,373 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
XPCOMUtils.defineLazyModuleGetter(this, "Downloads", "resource://gre/modules/Downloads.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils", "resource://gre/modules/DownloadUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
var gStrings = Services.strings.createBundle("chrome://browser/locale/aboutDownloads.properties");
XPCOMUtils.defineLazyGetter(this, "strings",
() => Services.strings.createBundle("chrome://browser/locale/aboutDownloads.properties"));
function deleteDownload(download) {
download.finalize(true).then(null, Cu.reportError);
OS.File.remove(download.target.path).then(null, ex => {
if (!(ex instanceof OS.File.Error && ex.becauseNoSuchFile)) {
Cu.reportError(ex);
}
});
}
var contextMenu = {
_items: [],
_targetDownload: null,
init: function () {
let element = document.getElementById("downloadmenu");
element.addEventListener("click",
event => event.download = this._targetDownload,
true);
this._items = [
new ContextMenuItem("open",
download => download.succeeded,
download => download.launch().then(null, Cu.reportError)),
new ContextMenuItem("retry",
download => download.error ||
(download.canceled && !download.hasPartialData),
download => download.start().then(null, Cu.reportError)),
new ContextMenuItem("remove",
download => download.stopped,
download => {
Downloads.getList(Downloads.ALL)
.then(list => list.remove(download))
.then(null, Cu.reportError);
deleteDownload(download);
}),
new ContextMenuItem("pause",
download => !download.stopped && download.hasPartialData,
download => download.cancel().then(null, Cu.reportError)),
new ContextMenuItem("resume",
download => download.canceled && download.hasPartialData,
download => download.start().then(null, Cu.reportError)),
new ContextMenuItem("cancel",
download => !download.stopped ||
(download.canceled && download.hasPartialData),
download => {
download.cancel().then(null, Cu.reportError);
download.removePartialData().then(null, Cu.reportError);
}),
// following menu item is a global action
new ContextMenuItem("removeall",
() => downloadLists.finished.length > 0,
() => downloadLists.removeFinished())
];
},
addContextMenuEventListener: function (element) {
element.addEventListener("contextmenu", this.onContextMenu.bind(this));
},
onContextMenu: function (event) {
let target = event.target;
while (target && !target.download) {
target = target.parentNode;
}
if (!target) {
Cu.reportError("No download found for context menu target");
event.preventDefault();
return;
}
// capture the target download for menu items to use in a click event
this._targetDownload = target.download;
for (let item of this._items) {
item.updateVisibility(target.download);
}
}
};
function ContextMenuItem(name, isVisible, action) {
this.element = document.getElementById("contextmenu-" + name);
this.isVisible = isVisible;
this.element.addEventListener("click", event => action(event.download));
}
ContextMenuItem.prototype = {
updateVisibility: function (download) {
this.element.hidden = !this.isVisible(download);
}
};
function DownloadListView(type, listElementId) {
this.listElement = document.getElementById(listElementId);
contextMenu.addContextMenuEventListener(this.listElement);
this.items = new Map();
Downloads.getList(type)
.then(list => list.addView(this))
.then(null, Cu.reportError);
window.addEventListener("unload", event => {
Downloads.getList(type)
.then(list => list.removeView(this))
.then(null, Cu.reportError);
});
}
DownloadListView.prototype = {
get finished() {
let finished = [];
for (let download of this.items.keys()) {
if (download.stopped && (!download.hasPartialData || download.error)) {
finished.push(download);
}
}
return finished;
},
insertOrMoveItem: function (item) {
var compare = (a, b) => {
// active downloads always before stopped downloads
if (a.stopped != b.stopped) {
return b.stopped ? -1 : 1
}
// most recent downloads first
return b.startTime - a.startTime;
};
let insertLocation = this.listElement.firstChild;
while (insertLocation && compare(item.download, insertLocation.download) > 0) {
insertLocation = insertLocation.nextElementSibling;
}
this.listElement.insertBefore(item.element, insertLocation);
},
onDownloadAdded: function (download) {
let item = new DownloadItem(download);
this.items.set(download, item);
this.insertOrMoveItem(item);
},
onDownloadChanged: function (download) {
let item = this.items.get(download);
if (!item) {
Cu.reportError("No DownloadItem found for download");
return;
}
if (item.stateChanged) {
this.insertOrMoveItem(item);
}
item.onDownloadChanged();
},
onDownloadRemoved: function (download) {
let item = this.items.get(download);
if (!item) {
Cu.reportError("No DownloadItem found for download");
return;
}
this.items.delete(download);
this.listElement.removeChild(item.element);
Messaging.sendRequest({
type: "Download:Remove",
path: download.target.path
});
}
};
var downloadLists = {
init: function () {
this.publicDownloads = new DownloadListView(Downloads.PUBLIC, "public-downloads-list");
this.privateDownloads = new DownloadListView(Downloads.PRIVATE, "private-downloads-list");
},
get finished() {
return this.publicDownloads.finished.concat(this.privateDownloads.finished);
},
removeFinished: function () {
let finished = this.finished;
if (finished.length == 0) {
return;
}
let title = strings.GetStringFromName("downloadAction.deleteAll");
let messageForm = strings.GetStringFromName("downloadMessage.deleteAll");
let message = PluralForm.get(finished.length, messageForm).replace("#1", finished.length);
if (Services.prompt.confirm(null, title, message)) {
Downloads.getList(Downloads.ALL)
.then(list => {
for (let download of finished) {
list.remove(download).then(null, Cu.reportError);
deleteDownload(download);
}
}, Cu.reportError);
}
}
};
function DownloadItem(download) {
this._download = download;
this._updateFromDownload();
this._domain = DownloadUtils.getURIHost(download.source.url)[0];
this._fileName = this._htmlEscape(OS.Path.basename(download.target.path));
this._iconUrl = "moz-icon://" + this._fileName + "?size=64";
this._startDate = this._htmlEscape(DownloadUtils.getReadableDates(download.startTime)[0]);
this._element = this.createElement();
}
const kDownloadStatePropertyNames = [
"stopped",
"succeeded",
"canceled",
"error",
"startTime"
];
DownloadItem.prototype = {
_htmlEscape : function (s) {
s = s.replace(/&/g, "&amp;");
s = s.replace(/>/g, "&gt;");
s = s.replace(/</g, "&lt;");
s = s.replace(/"/g, "&quot;");
s = s.replace(/'/g, "&apos;");
return s;
},
_updateFromDownload: function () {
this._state = {};
kDownloadStatePropertyNames.forEach(
name => this._state[name] = this._download[name],
this);
},
get stateChanged() {
return kDownloadStatePropertyNames.some(
name => this._state[name] != this._download[name],
this);
},
get download() {
return this._download;
},
get element() {
return this._element;
},
createElement: function() {
let template = document.getElementById("download-item");
// TODO: use this once <template> is working
// let element = document.importNode(template.content, true);
// simulate a <template> node...
let element = template.cloneNode(true);
element.removeAttribute("id");
element.removeAttribute("style");
// launch the download if clicked
element.addEventListener("click", this.onClick.bind(this));
// set download as an expando property for the context menu
element.download = this.download;
// fill in template placeholders
this.updateElement(element);
return element;
},
updateElement: function (element) {
element.querySelector(".date").textContent = this.startDate;
element.querySelector(".domain").textContent = this.domain;
element.querySelector(".icon").src = this.iconUrl;
element.querySelector(".size").textContent = this.size;
element.querySelector(".state").textContent = this.stateDescription;
element.querySelector(".title").setAttribute("value", this.fileName);
},
onClick: function (event) {
if (this.download.succeeded) {
this.download.launch().then(null, Cu.reportError);
}
},
onDownloadChanged: function () {
this._updateFromDownload();
this.updateElement(this.element);
},
// template properties below
get domain() {
return this._domain;
},
get fileName() {
return this._fileName;
},
get id() {
return this._id;
},
get iconUrl() {
return this._iconUrl;
},
get size() {
if (this.download.succeeded && this.download.target.exists) {
return DownloadUtils.convertByteUnits(this.download.target.size).join("");
} else if (this.download.hasProgress) {
return DownloadUtils.convertByteUnits(this.download.totalBytes).join("");
}
return strings.GetStringFromName("downloadState.unknownSize");
},
get startDate() {
return this._startDate;
},
get stateDescription() {
let name;
if (this.download.error) {
name = "downloadState.failed";
} else if (this.download.canceled) {
if (this.download.hasPartialData) {
name = "downloadState.paused";
} else {
name = "downloadState.canceled";
}
} else if (!this.download.stopped) {
if (this.download.currentBytes > 0) {
name = "downloadState.downloading";
} else {
name = "downloadState.starting";
}
}
if (name) {
return strings.GetStringFromName(name);
}
return "";
}
};
window.addEventListener("DOMContentLoaded", event => {
contextMenu.init();
downloadLists.init()
});

View file

@ -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>

View file

@ -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);

View file

@ -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>

View file

@ -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>

View file

@ -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);

View file

@ -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>

View file

@ -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);

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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");
}

File diff suppressed because it is too large Load diff

View file

@ -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>

View file

@ -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);
}
}
}

View file

@ -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>

View file

@ -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);
});

View file

@ -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" });
}

View file

@ -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>

View file

@ -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/");

View file

@ -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

View file

@ -0,0 +1,406 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html [
<!ENTITY % htmlDTD
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"DTD/xhtml1-strict.dtd">
%htmlDTD;
<!ENTITY % netErrorDTD
SYSTEM "chrome://global/locale/netError.dtd">
%netErrorDTD;
<!ENTITY % globalDTD
SYSTEM "chrome://global/locale/global.dtd">
%globalDTD;
]>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta name="viewport" content="width=device-width; user-scalable=false;" />
<title>&loadError.label;</title>
<link rel="stylesheet" href="chrome://global/skin/netError.css" type="text/css" media="all" />
<!-- If the location of the favicon is changed here, the FAVICON_ERRORPAGE_URL symbol in
toolkit/components/places/src/nsFaviconService.h should be updated. -->
<link rel="icon" type="image/png" id="favicon" sizes="64x64" href="chrome://browser/skin/images/errorpage-warning.png"/>
<script type="application/javascript"><![CDATA[
// Error url MUST be formatted like this:
// moz-neterror:page?e=error&u=url&d=desc
//
// or optionally, to specify an alternate CSS class to allow for
// custom styling and favicon:
//
// moz-neterror:page?e=error&u=url&s=classname&d=desc
// Note that this file uses document.documentURI to get
// the URL (with the format from above). This is because
// document.location.href gets the current URI off the docshell,
// which is the URL displayed in the location bar, i.e.
// the URI that the user attempted to load.
function getErrorCode()
{
var url = document.documentURI;
var error = url.search(/e\=/);
var duffUrl = url.search(/\&u\=/);
return decodeURIComponent(url.slice(error + 2, duffUrl));
}
function getCSSClass()
{
var url = document.documentURI;
var matches = url.match(/s\=([^&]+)\&/);
// s is optional, if no match just return nothing
if (!matches || matches.length < 2)
return "";
// parenthetical match is the second entry
return decodeURIComponent(matches[1]);
}
function getDescription()
{
var url = document.documentURI;
var desc = url.search(/d\=/);
// desc == -1 if not found; if so, return an empty string
// instead of what would turn out to be portions of the URI
if (desc == -1)
return "";
return decodeURIComponent(url.slice(desc + 2));
}
function retryThis(buttonEl)
{
// Note: The application may wish to handle switching off "offline mode"
// before this event handler runs, but using a capturing event handler.
// Session history has the URL of the page that failed
// to load, not the one of the error page. So, just call
// reload(), which will also repost POST data correctly.
try {
location.reload();
} catch (e) {
// We probably tried to reload a URI that caused an exception to
// occur; e.g. a nonexistent file.
}
}
function initPage()
{
var err = getErrorCode();
// if it's an unknown error or there's no title or description
// defined, get the generic message
var errTitle = document.getElementById("et_" + err);
var errDesc = document.getElementById("ed_" + err);
if (!errTitle || !errDesc)
{
errTitle = document.getElementById("et_generic");
errDesc = document.getElementById("ed_generic");
}
var title = document.getElementsByClassName("errorTitleText")[0];
if (title)
{
title.parentNode.replaceChild(errTitle, title);
// change id to the replaced child's id so styling works
errTitle.classList.add("errorTitleText");
}
var sd = document.getElementById("errorShortDescText");
if (sd)
sd.textContent = getDescription();
var ld = document.getElementById("errorLongDesc");
if (ld)
{
ld.parentNode.replaceChild(errDesc, ld);
// change id to the replaced child's id so styling works
errDesc.id = "errorLongDesc";
}
// remove undisplayed errors to avoid bug 39098
var errContainer = document.getElementById("errorContainer");
errContainer.parentNode.removeChild(errContainer);
var className = getCSSClass();
if (className && className != "expertBadCert") {
// Associate a CSS class with the root of the page, if one was passed in,
// to allow custom styling.
// Not "expertBadCert" though, don't want to deal with the favicon
document.documentElement.className = className;
// Also, if they specified a CSS class, they must supply their own
// favicon. In order to trigger the browser to repaint though, we
// need to remove/add the link element.
var favicon = document.getElementById("favicon");
var faviconParent = favicon.parentNode;
faviconParent.removeChild(favicon);
favicon.setAttribute("href", "chrome://global/skin/icons/" + className + "_favicon.png");
faviconParent.appendChild(favicon);
}
if (className == "expertBadCert") {
showSecuritySection();
}
if (err == "remoteXUL") {
// Remove the "Try again" button for remote XUL errors given that
// it is useless.
document.getElementById("errorTryAgain").style.display = "none";
}
if (err == "cspBlocked") {
// Remove the "Try again" button for CSP violations, since it's
// almost certainly useless. (Bug 553180)
document.getElementById("errorTryAgain").style.display = "none";
}
if (err == "nssBadCert") {
// Remove the "Try again" button for security exceptions, since it's
// almost certainly useless.
document.getElementById("errorTryAgain").style.display = "none";
document.getElementById("errorPage").setAttribute("class", "certerror");
}
else {
// Remove the override block for non-certificate errors. CSS-hiding
// isn't good enough here, because of bug 39098
var secOverride = document.getElementById("securityOverrideDiv");
secOverride.parentNode.removeChild(secOverride);
}
if (err == "inadequateSecurityError") {
// Remove the "Try again" button for HTTP/2 inadequate security as it
// is useless.
document.getElementById("errorTryAgain").style.display = "none";
var container = document.getElementById("errorLongDesc");
for (var span of container.querySelectorAll("span.hostname")) {
span.textContent = document.location.hostname;
}
}
addDomainErrorLinks();
}
function showSecuritySection() {
// Swap link out, content in
document.getElementById('securityOverrideContent').style.display = '';
document.getElementById('securityOverrideLink').style.display = 'none';
}
/* Try to preserve the links contained in the error description, like
the error code.
Also, in the case of SSL error pages about domain mismatch, see if
we can hyperlink the user to the correct site. We don't want
to do this generically since it allows MitM attacks to redirect
users to a site under attacker control, but in certain cases
it is safe (and helpful!) to do so. Bug 402210
*/
function addDomainErrorLinks() {
// Rather than textContent, we need to treat description as HTML
var sd = document.getElementById("errorShortDescText");
if (sd) {
var desc = getDescription();
// sanitize description text - see bug 441169
// First, find the index of the <a> tags we care about, being
// careful not to use an over-greedy regex.
var codeRe = /<a id="errorCode" title="([^"]+)">/;
var codeResult = codeRe.exec(desc);
var domainRe = /<a id="cert_domain_link" title="([^"]+)">/;
var domainResult = domainRe.exec(desc);
// The order of these links in the description is fixed in
// TransportSecurityInfo.cpp:formatOverridableCertErrorMessage.
var firstResult = domainResult;
if(!domainResult)
firstResult = codeResult;
if (!firstResult)
return;
// Remove sd's existing children
sd.textContent = "";
// Everything up to the first link should be text content.
sd.appendChild(document.createTextNode(desc.slice(0, firstResult.index)));
// Now create the actual links.
if (domainResult) {
createLink(sd, "cert_domain_link", domainResult[1])
// Append text for anything between the two links.
sd.appendChild(document.createTextNode(desc.slice(desc.indexOf("</a>") + "</a>".length, codeResult.index)));
}
createLink(sd, "errorCode", codeResult[1])
// Finally, append text for anything after the last closing </a>.
sd.appendChild(document.createTextNode(desc.slice(desc.lastIndexOf("</a>") + "</a>".length)));
}
// Initialize the cert domain link.
var link = document.getElementById('cert_domain_link');
if (!link)
return;
var okHost = link.getAttribute("title");
var thisHost = document.location.hostname;
var proto = document.location.protocol;
// If okHost is a wildcard domain ("*.example.com") let's
// use "www" instead. "*.example.com" isn't going to
// get anyone anywhere useful. bug 432491
okHost = okHost.replace(/^\*\./, "www.");
/* case #1:
* example.com uses an invalid security certificate.
*
* The certificate is only valid for www.example.com
*
* Make sure to include the "." ahead of thisHost so that
* a MitM attack on paypal.com doesn't hyperlink to "notpaypal.com"
*
* We'd normally just use a RegExp here except that we lack a
* library function to escape them properly (bug 248062), and
* domain names are famous for having '.' characters in them,
* which would allow spurious and possibly hostile matches.
*/
if (okHost.endsWith("." + thisHost))
link.href = proto + okHost;
/* case #2:
* browser.garage.maemo.org uses an invalid security certificate.
*
* The certificate is only valid for garage.maemo.org
*/
if (thisHost.endsWith("." + okHost))
link.href = proto + okHost;
}
function createLink(el, id, text) {
var anchorEl = document.createElement("a");
anchorEl.setAttribute("id", id);
anchorEl.setAttribute("title", text);
anchorEl.appendChild(document.createTextNode(text));
el.appendChild(anchorEl);
}
]]></script>
</head>
<body id="errorPage" dir="&locale.dir;">
<!-- ERROR ITEM CONTAINER (removed during loading to avoid bug 39098) -->
<div id="errorContainer">
<div id="errorTitlesContainer">
<h1 id="et_generic">&generic.title;</h1>
<h1 id="et_dnsNotFound">&dnsNotFound.title;</h1>
<h1 id="et_fileNotFound">&fileNotFound.title;</h1>
<h1 id="et_fileAccessDenied">&fileAccessDenied.title;</h1>
<h1 id="et_malformedURI">&malformedURI.title;</h1>
<h1 id="et_unknownProtocolFound">&unknownProtocolFound.title;</h1>
<h1 id="et_connectionFailure">&connectionFailure.title;</h1>
<h1 id="et_netTimeout">&netTimeout.title;</h1>
<h1 id="et_redirectLoop">&redirectLoop.title;</h1>
<h1 id="et_unknownSocketType">&unknownSocketType.title;</h1>
<h1 id="et_netReset">&netReset.title;</h1>
<h1 id="et_notCached">&notCached.title;</h1>
<!-- Since Fennec not yet have offline mode, change the title to
connectionFailure to prevent confusion -->
<h1 id="et_netOffline">&connectionFailure.title;</h1>
<h1 id="et_netInterrupt">&netInterrupt.title;</h1>
<h1 id="et_deniedPortAccess">&deniedPortAccess.title;</h1>
<h1 id="et_proxyResolveFailure">&proxyResolveFailure.title;</h1>
<h1 id="et_proxyConnectFailure">&proxyConnectFailure.title;</h1>
<h1 id="et_contentEncodingError">&contentEncodingError.title;</h1>
<h1 id="et_unsafeContentType">&unsafeContentType.title;</h1>
<h1 id="et_nssFailure2">&nssFailure2.title;</h1>
<h1 id="et_nssBadCert">&nssBadCert.title;</h1>
<h1 id="et_cspBlocked">&cspBlocked.title;</h1>
<h1 id="et_remoteXUL">&remoteXUL.title;</h1>
<h1 id="et_corruptedContentErrorv2">&corruptedContentErrorv2.title;</h1>
<h1 id="et_sslv3Used">&sslv3Used.title;</h1>
<h1 id="et_weakCryptoUsed">&weakCryptoUsed.title;</h1>
<h1 id="et_inadequateSecurityError">&inadequateSecurityError.title;</h1>
</div>
<div id="errorDescriptionsContainer">
<div id="ed_generic">&generic.longDesc;</div>
<div id="ed_dnsNotFound">&dnsNotFound.longDesc4;</div>
<div id="ed_fileNotFound">&fileNotFound.longDesc;</div>
<div id="ed_fileAccessDenied">&fileAccessDenied.longDesc;</div>
<div id="ed_malformedURI">&malformedURI.longDesc2;</div>
<div id="ed_unknownProtocolFound">&unknownProtocolFound.longDesc;</div>
<div id="ed_connectionFailure">&connectionFailure.longDesc2;</div>
<div id="ed_netTimeout">&netTimeout.longDesc2;</div>
<div id="ed_redirectLoop">&redirectLoop.longDesc;</div>
<div id="ed_unknownSocketType">&unknownSocketType.longDesc;</div>
<div id="ed_netReset">&netReset.longDesc2;</div>
<div id="ed_notCached">&notCached.longDesc;</div>
<!-- Change longDesc from netOffline to connectionFailure,
suggesting user to check their wifi/cell_data connection -->
<div id="ed_netOffline">&connectionFailure.longDesc2;</div>
<div id="ed_netInterrupt">&netInterrupt.longDesc2;</div>
<div id="ed_deniedPortAccess">&deniedPortAccess.longDesc;</div>
<div id="ed_proxyResolveFailure">&proxyResolveFailure.longDesc3;</div>
<div id="ed_proxyConnectFailure">&proxyConnectFailure.longDesc;</div>
<div id="ed_contentEncodingError">&contentEncodingError.longDesc;</div>
<div id="ed_unsafeContentType">&unsafeContentType.longDesc;</div>
<div id="ed_nssFailure2">&nssFailure2.longDesc2;</div>
<div id="ed_nssBadCert">&nssBadCert.longDesc2;</div>
<div id="ed_cspBlocked">&cspBlocked.longDesc;</div>
<div id="ed_remoteXUL">&remoteXUL.longDesc;</div>
<div id="ed_corruptedContentErrorv2">&corruptedContentErrorv2.longDesc;</div>
<div id="ed_sslv3Used">&sslv3Used.longDesc;</div>
<div id="ed_weakCryptoUsed">&weakCryptoUsed.longDesc;</div>
<div id="ed_inadequateSecurityError">&inadequateSecurityError.longDesc;</div>
</div>
</div>
<!-- PAGE CONTAINER (for styling purposes only) -->
<div id="errorPageContainer">
<!-- Error Title -->
<div id="errorTitle">
<h1 class="errorTitleText" />
</div>
<!-- LONG CONTENT (the section most likely to require scrolling) -->
<div id="errorLongContent">
<!-- Short Description -->
<div id="errorShortDesc">
<p id="errorShortDescText" />
</div>
<!-- Long Description (Note: See netError.dtd for used XHTML tags) -->
<div id="errorLongDesc" />
<!-- Override section - For ssl errors only. Removed on init for other
error types. -->
<div id="securityOverrideDiv">
<a id="securityOverrideLink" href="javascript:showSecuritySection();" >&securityOverride.linkText;</a>
<div id="securityOverrideContent" style="display: none;">&securityOverride.warningContent;</div>
</div>
</div>
<!-- Retry Button -->
<button id="errorTryAgain" onclick="retryThis(this);">&retry.label;</button>
</div>
<!--
- Note: It is important to run the script this way, instead of using
- an onload handler. This is because error pages are loaded as
- LOAD_BACKGROUND, which means that onload handlers will not be executed.
-->
<script type="application/javascript">initPage();</script>
</body>
</html>

View file

@ -0,0 +1,71 @@
#filter substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
chrome.jar:
% content browser %content/ contentaccessible=yes
* content/about.xhtml (content/about.xhtml)
* content/about.js (content/about.js)
content/config.xhtml (content/config.xhtml)
content/config.js (content/config.js)
content/content.js (content/content.js)
content/aboutAddons.xhtml (content/aboutAddons.xhtml)
content/aboutAddons.js (content/aboutAddons.js)
content/aboutCertError.xhtml (content/aboutCertError.xhtml)
content/aboutDownloads.xhtml (content/aboutDownloads.xhtml)
content/aboutDownloads.js (content/aboutDownloads.js)
content/aboutPrivateBrowsing.xhtml (content/aboutPrivateBrowsing.xhtml)
content/aboutPrivateBrowsing.js (content/aboutPrivateBrowsing.js)
content/Reader.js (content/Reader.js)
content/aboutHome.xhtml (content/aboutHome.xhtml)
content/aboutRights.xhtml (content/aboutRights.xhtml)
content/blockedSite.xhtml (content/blockedSite.xhtml)
content/languages.properties (content/languages.properties)
content/browser.xul (content/browser.xul)
content/browser.css (content/browser.css)
content/browser.js (content/browser.js)
content/geckoview.xul (content/geckoview.xul)
content/geckoview.js (content/geckoview.js)
content/PresentationView.xul (content/PresentationView.xul)
content/PresentationView.js (content/PresentationView.js)
content/bindings/checkbox.xml (content/bindings/checkbox.xml)
content/bindings/settings.xml (content/bindings/settings.xml)
content/netError.xhtml (content/netError.xhtml)
content/SelectHelper.js (content/SelectHelper.js)
content/ActionBarHandler.js (content/ActionBarHandler.js)
content/EmbedRT.js (content/EmbedRT.js)
content/InputWidgetHelper.js (content/InputWidgetHelper.js)
content/WebrtcUI.js (content/WebrtcUI.js)
content/MemoryObserver.js (content/MemoryObserver.js)
content/ConsoleAPI.js (content/ConsoleAPI.js)
content/PluginHelper.js (content/PluginHelper.js)
content/PrintHelper.js (content/PrintHelper.js)
content/OfflineApps.js (content/OfflineApps.js)
content/MasterPassword.js (content/MasterPassword.js)
content/FindHelper.js (content/FindHelper.js)
content/PermissionsHelper.js (content/PermissionsHelper.js)
content/FeedHandler.js (content/FeedHandler.js)
content/Feedback.js (content/Feedback.js)
content/Linkify.js (content/Linkify.js)
content/CastingApps.js (content/CastingApps.js)
content/RemoteDebugger.js (content/RemoteDebugger.js)
#ifdef MOZ_SERVICES_HEALTHREPORT
content/aboutHealthReport.xhtml (content/aboutHealthReport.xhtml)
content/aboutHealthReport.js (content/aboutHealthReport.js)
#endif
content/aboutAccounts.xhtml (content/aboutAccounts.xhtml)
content/aboutAccounts.js (content/aboutAccounts.js)
content/aboutLogins.xhtml (content/aboutLogins.xhtml)
content/aboutLogins.js (content/aboutLogins.js)
#ifndef RELEASE_OR_BETA
content/WebcompatReporter.js (content/WebcompatReporter.js)
#endif
% content branding %content/branding/
% override chrome://global/content/config.xul chrome://browser/content/config.xhtml
% override chrome://global/content/netError.xhtml chrome://browser/content/netError.xhtml
% override chrome://mozapps/content/extensions/extensions.xul chrome://browser/content/aboutAddons.xhtml

View file

@ -0,0 +1,13 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DEFINES['AB_CD'] = CONFIG['MOZ_UI_LOCALE']
DEFINES['PACKAGE'] = 'browser'
DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
DEFINES['MOZ_APP_VERSION_DISPLAY'] = CONFIG['MOZ_APP_VERSION_DISPLAY']
DEFINES['ANDROID_PACKAGE_NAME'] = CONFIG['ANDROID_PACKAGE_NAME']
JAR_MANIFESTS += ['jar.mn']