mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-10-13 18:57:29 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
23
mobile/android/chrome/content/.eslintrc
Normal file
23
mobile/android/chrome/content/.eslintrc
Normal 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
|
||||
731
mobile/android/chrome/content/ActionBarHandler.js
Normal file
731
mobile/android/chrome/content/ActionBarHandler.js
Normal 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));
|
||||
},
|
||||
};
|
||||
850
mobile/android/chrome/content/CastingApps.js
Normal file
850
mobile/android/chrome/content/CastingApps.js
Normal 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
96
mobile/android/chrome/content/ConsoleAPI.js
Normal file
96
mobile/android/chrome/content/ConsoleAPI.js
Normal 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;
|
||||
}
|
||||
};
|
||||
82
mobile/android/chrome/content/EmbedRT.js
Normal file
82
mobile/android/chrome/content/EmbedRT.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
120
mobile/android/chrome/content/FeedHandler.js
Normal file
120
mobile/android/chrome/content/FeedHandler.js
Normal 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 });
|
||||
});
|
||||
}
|
||||
};
|
||||
64
mobile/android/chrome/content/Feedback.js
Normal file
64
mobile/android/chrome/content/Feedback.js
Normal 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;
|
||||
}
|
||||
};
|
||||
197
mobile/android/chrome/content/FindHelper.js
Normal file
197
mobile/android/chrome/content/FindHelper.js
Normal 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
98
mobile/android/chrome/content/InputWidgetHelper.js
Normal file
98
mobile/android/chrome/content/InputWidgetHelper.js
Normal 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;
|
||||
}
|
||||
};
|
||||
108
mobile/android/chrome/content/Linkify.js
Normal file
108
mobile/android/chrome/content/Linkify.js
Normal 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);
|
||||
}
|
||||
};
|
||||
67
mobile/android/chrome/content/MasterPassword.js
Normal file
67
mobile/android/chrome/content/MasterPassword.js
Normal 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;
|
||||
}
|
||||
};
|
||||
88
mobile/android/chrome/content/MemoryObserver.js
Normal file
88
mobile/android/chrome/content/MemoryObserver.js
Normal 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);
|
||||
},
|
||||
};
|
||||
77
mobile/android/chrome/content/OfflineApps.js
Normal file
77
mobile/android/chrome/content/OfflineApps.js
Normal 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);
|
||||
}
|
||||
};
|
||||
188
mobile/android/chrome/content/PermissionsHelper.js
Normal file
188
mobile/android/chrome/content/PermissionsHelper.js
Normal 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
221
mobile/android/chrome/content/PluginHelper.js
Normal file
221
mobile/android/chrome/content/PluginHelper.js
Normal 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
63
mobile/android/chrome/content/PresentationView.js
Normal file
63
mobile/android/chrome/content/PresentationView.js
Normal 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 }));
|
||||
}
|
||||
}
|
||||
};
|
||||
15
mobile/android/chrome/content/PresentationView.xul
Normal file
15
mobile/android/chrome/content/PresentationView.xul
Normal 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>
|
||||
73
mobile/android/chrome/content/PrintHelper.js
Normal file
73
mobile/android/chrome/content/PrintHelper.js
Normal 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 () {},
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
290
mobile/android/chrome/content/Reader.js
Normal file
290
mobile/android/chrome/content/Reader.js
Normal 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");
|
||||
}),
|
||||
};
|
||||
355
mobile/android/chrome/content/RemoteDebugger.js
Normal file
355
mobile/android/chrome/content/RemoteDebugger.js
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
161
mobile/android/chrome/content/SelectHelper.js
Normal file
161
mobile/android/chrome/content/SelectHelper.js
Normal 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;
|
||||
}
|
||||
};
|
||||
144
mobile/android/chrome/content/WebcompatReporter.js
Normal file
144
mobile/android/chrome/content/WebcompatReporter.js
Normal 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");
|
||||
});
|
||||
302
mobile/android/chrome/content/WebrtcUI.js
Normal file
302
mobile/android/chrome/content/WebrtcUI.js
Normal 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");
|
||||
}
|
||||
}
|
||||
151
mobile/android/chrome/content/about.js
Normal file
151
mobile/android/chrome/content/about.js
Normal 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);
|
||||
77
mobile/android/chrome/content/about.xhtml
Normal file
77
mobile/android/chrome/content/about.xhtml
Normal 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>
|
||||
351
mobile/android/chrome/content/aboutAccounts.js
Normal file
351
mobile/android/chrome/content/aboutAccounts.js
Normal 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());
|
||||
}
|
||||
});
|
||||
83
mobile/android/chrome/content/aboutAccounts.xhtml
Normal file
83
mobile/android/chrome/content/aboutAccounts.xhtml
Normal 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>
|
||||
609
mobile/android/chrome/content/aboutAddons.js
Normal file
609
mobile/android/chrome/content/aboutAddons.js
Normal 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);
|
||||
63
mobile/android/chrome/content/aboutAddons.xhtml
Normal file
63
mobile/android/chrome/content/aboutAddons.xhtml
Normal 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>
|
||||
264
mobile/android/chrome/content/aboutCertError.xhtml
Normal file
264
mobile/android/chrome/content/aboutCertError.xhtml
Normal 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>
|
||||
373
mobile/android/chrome/content/aboutDownloads.js
Normal file
373
mobile/android/chrome/content/aboutDownloads.js
Normal 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, "&");
|
||||
s = s.replace(/>/g, ">");
|
||||
s = s.replace(/</g, "<");
|
||||
s = s.replace(/"/g, """);
|
||||
s = s.replace(/'/g, "'");
|
||||
return s;
|
||||
},
|
||||
|
||||
_updateFromDownload: function () {
|
||||
this._state = {};
|
||||
kDownloadStatePropertyNames.forEach(
|
||||
name => this._state[name] = this._download[name],
|
||||
this);
|
||||
},
|
||||
|
||||
get stateChanged() {
|
||||
return kDownloadStatePropertyNames.some(
|
||||
name => this._state[name] != this._download[name],
|
||||
this);
|
||||
},
|
||||
|
||||
get download() {
|
||||
return this._download;
|
||||
},
|
||||
get element() {
|
||||
return this._element;
|
||||
},
|
||||
|
||||
createElement: function() {
|
||||
let template = document.getElementById("download-item");
|
||||
// TODO: use this once <template> is working
|
||||
// let element = document.importNode(template.content, true);
|
||||
|
||||
// simulate a <template> node...
|
||||
let element = template.cloneNode(true);
|
||||
element.removeAttribute("id");
|
||||
element.removeAttribute("style");
|
||||
|
||||
// launch the download if clicked
|
||||
element.addEventListener("click", this.onClick.bind(this));
|
||||
|
||||
// set download as an expando property for the context menu
|
||||
element.download = this.download;
|
||||
|
||||
// fill in template placeholders
|
||||
this.updateElement(element);
|
||||
|
||||
return element;
|
||||
},
|
||||
|
||||
updateElement: function (element) {
|
||||
element.querySelector(".date").textContent = this.startDate;
|
||||
element.querySelector(".domain").textContent = this.domain;
|
||||
element.querySelector(".icon").src = this.iconUrl;
|
||||
element.querySelector(".size").textContent = this.size;
|
||||
element.querySelector(".state").textContent = this.stateDescription;
|
||||
element.querySelector(".title").setAttribute("value", this.fileName);
|
||||
},
|
||||
|
||||
onClick: function (event) {
|
||||
if (this.download.succeeded) {
|
||||
this.download.launch().then(null, Cu.reportError);
|
||||
}
|
||||
},
|
||||
|
||||
onDownloadChanged: function () {
|
||||
this._updateFromDownload();
|
||||
this.updateElement(this.element);
|
||||
},
|
||||
|
||||
// template properties below
|
||||
get domain() {
|
||||
return this._domain;
|
||||
},
|
||||
get fileName() {
|
||||
return this._fileName;
|
||||
},
|
||||
get id() {
|
||||
return this._id;
|
||||
},
|
||||
get iconUrl() {
|
||||
return this._iconUrl;
|
||||
},
|
||||
|
||||
get size() {
|
||||
if (this.download.succeeded && this.download.target.exists) {
|
||||
return DownloadUtils.convertByteUnits(this.download.target.size).join("");
|
||||
} else if (this.download.hasProgress) {
|
||||
return DownloadUtils.convertByteUnits(this.download.totalBytes).join("");
|
||||
}
|
||||
return strings.GetStringFromName("downloadState.unknownSize");
|
||||
},
|
||||
|
||||
get startDate() {
|
||||
return this._startDate;
|
||||
},
|
||||
|
||||
get stateDescription() {
|
||||
let name;
|
||||
if (this.download.error) {
|
||||
name = "downloadState.failed";
|
||||
} else if (this.download.canceled) {
|
||||
if (this.download.hasPartialData) {
|
||||
name = "downloadState.paused";
|
||||
} else {
|
||||
name = "downloadState.canceled";
|
||||
}
|
||||
} else if (!this.download.stopped) {
|
||||
if (this.download.currentBytes > 0) {
|
||||
name = "downloadState.downloading";
|
||||
} else {
|
||||
name = "downloadState.starting";
|
||||
}
|
||||
}
|
||||
|
||||
if (name) {
|
||||
return strings.GetStringFromName(name);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("DOMContentLoaded", event => {
|
||||
contextMenu.init();
|
||||
downloadLists.init()
|
||||
});
|
||||
62
mobile/android/chrome/content/aboutDownloads.xhtml
Normal file
62
mobile/android/chrome/content/aboutDownloads.xhtml
Normal 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>
|
||||
192
mobile/android/chrome/content/aboutHealthReport.js
Normal file
192
mobile/android/chrome/content/aboutHealthReport.js
Normal 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);
|
||||
32
mobile/android/chrome/content/aboutHealthReport.xhtml
Normal file
32
mobile/android/chrome/content/aboutHealthReport.xhtml
Normal 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>
|
||||
22
mobile/android/chrome/content/aboutHome.xhtml
Normal file
22
mobile/android/chrome/content/aboutHome.xhtml
Normal 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>
|
||||
518
mobile/android/chrome/content/aboutLogins.js
Normal file
518
mobile/android/chrome/content/aboutLogins.js
Normal 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);
|
||||
90
mobile/android/chrome/content/aboutLogins.xhtml
Normal file
90
mobile/android/chrome/content/aboutLogins.xhtml
Normal 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>
|
||||
32
mobile/android/chrome/content/aboutPrivateBrowsing.js
Normal file
32
mobile/android/chrome/content/aboutPrivateBrowsing.js
Normal 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);
|
||||
43
mobile/android/chrome/content/aboutPrivateBrowsing.xhtml
Normal file
43
mobile/android/chrome/content/aboutPrivateBrowsing.xhtml
Normal 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>
|
||||
95
mobile/android/chrome/content/aboutRights.xhtml
Normal file
95
mobile/android/chrome/content/aboutRights.xhtml
Normal 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>
|
||||
75
mobile/android/chrome/content/bindings/checkbox.xml
Normal file
75
mobile/android/chrome/content/bindings/checkbox.xml
Normal 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>
|
||||
66
mobile/android/chrome/content/bindings/settings.xml
Normal file
66
mobile/android/chrome/content/bindings/settings.xml
Normal 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>
|
||||
195
mobile/android/chrome/content/blockedSite.xhtml
Normal file
195
mobile/android/chrome/content/blockedSite.xhtml
Normal 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>
|
||||
7
mobile/android/chrome/content/browser.css
Normal file
7
mobile/android/chrome/content/browser.css
Normal 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");
|
||||
}
|
||||
6999
mobile/android/chrome/content/browser.js
Normal file
6999
mobile/android/chrome/content/browser.js
Normal file
File diff suppressed because it is too large
Load diff
17
mobile/android/chrome/content/browser.xul
Normal file
17
mobile/android/chrome/content/browser.xul
Normal 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>
|
||||
673
mobile/android/chrome/content/config.js
Normal file
673
mobile/android/chrome/content/config.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
86
mobile/android/chrome/content/config.xhtml
Normal file
86
mobile/android/chrome/content/config.xhtml
Normal 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>
|
||||
159
mobile/android/chrome/content/content.js
Normal file
159
mobile/android/chrome/content/content.js
Normal 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);
|
||||
});
|
||||
32
mobile/android/chrome/content/geckoview.js
Normal file
32
mobile/android/chrome/content/geckoview.js
Normal 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" });
|
||||
}
|
||||
16
mobile/android/chrome/content/geckoview.xul
Normal file
16
mobile/android/chrome/content/geckoview.xul
Normal 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>
|
||||
6
mobile/android/chrome/content/healthreport-prefs.js
Normal file
6
mobile/android/chrome/content/healthreport-prefs.js
Normal 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/");
|
||||
114
mobile/android/chrome/content/languages.properties
Normal file
114
mobile/android/chrome/content/languages.properties
Normal 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
|
||||
406
mobile/android/chrome/content/netError.xhtml
Normal file
406
mobile/android/chrome/content/netError.xhtml
Normal 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">¬Cached.title;</h1>
|
||||
|
||||
<!-- Since Fennec not yet have offline mode, change the title to
|
||||
connectionFailure to prevent confusion -->
|
||||
<h1 id="et_netOffline">&connectionFailure.title;</h1>
|
||||
|
||||
<h1 id="et_netInterrupt">&netInterrupt.title;</h1>
|
||||
<h1 id="et_deniedPortAccess">&deniedPortAccess.title;</h1>
|
||||
<h1 id="et_proxyResolveFailure">&proxyResolveFailure.title;</h1>
|
||||
<h1 id="et_proxyConnectFailure">&proxyConnectFailure.title;</h1>
|
||||
<h1 id="et_contentEncodingError">&contentEncodingError.title;</h1>
|
||||
<h1 id="et_unsafeContentType">&unsafeContentType.title;</h1>
|
||||
<h1 id="et_nssFailure2">&nssFailure2.title;</h1>
|
||||
<h1 id="et_nssBadCert">&nssBadCert.title;</h1>
|
||||
<h1 id="et_cspBlocked">&cspBlocked.title;</h1>
|
||||
<h1 id="et_remoteXUL">&remoteXUL.title;</h1>
|
||||
<h1 id="et_corruptedContentErrorv2">&corruptedContentErrorv2.title;</h1>
|
||||
<h1 id="et_sslv3Used">&sslv3Used.title;</h1>
|
||||
<h1 id="et_weakCryptoUsed">&weakCryptoUsed.title;</h1>
|
||||
<h1 id="et_inadequateSecurityError">&inadequateSecurityError.title;</h1>
|
||||
</div>
|
||||
<div id="errorDescriptionsContainer">
|
||||
<div id="ed_generic">&generic.longDesc;</div>
|
||||
<div id="ed_dnsNotFound">&dnsNotFound.longDesc4;</div>
|
||||
<div id="ed_fileNotFound">&fileNotFound.longDesc;</div>
|
||||
<div id="ed_fileAccessDenied">&fileAccessDenied.longDesc;</div>
|
||||
<div id="ed_malformedURI">&malformedURI.longDesc2;</div>
|
||||
<div id="ed_unknownProtocolFound">&unknownProtocolFound.longDesc;</div>
|
||||
<div id="ed_connectionFailure">&connectionFailure.longDesc2;</div>
|
||||
<div id="ed_netTimeout">&netTimeout.longDesc2;</div>
|
||||
<div id="ed_redirectLoop">&redirectLoop.longDesc;</div>
|
||||
<div id="ed_unknownSocketType">&unknownSocketType.longDesc;</div>
|
||||
<div id="ed_netReset">&netReset.longDesc2;</div>
|
||||
<div id="ed_notCached">¬Cached.longDesc;</div>
|
||||
|
||||
<!-- Change longDesc from netOffline to connectionFailure,
|
||||
suggesting user to check their wifi/cell_data connection -->
|
||||
<div id="ed_netOffline">&connectionFailure.longDesc2;</div>
|
||||
|
||||
<div id="ed_netInterrupt">&netInterrupt.longDesc2;</div>
|
||||
<div id="ed_deniedPortAccess">&deniedPortAccess.longDesc;</div>
|
||||
<div id="ed_proxyResolveFailure">&proxyResolveFailure.longDesc3;</div>
|
||||
<div id="ed_proxyConnectFailure">&proxyConnectFailure.longDesc;</div>
|
||||
<div id="ed_contentEncodingError">&contentEncodingError.longDesc;</div>
|
||||
<div id="ed_unsafeContentType">&unsafeContentType.longDesc;</div>
|
||||
<div id="ed_nssFailure2">&nssFailure2.longDesc2;</div>
|
||||
<div id="ed_nssBadCert">&nssBadCert.longDesc2;</div>
|
||||
<div id="ed_cspBlocked">&cspBlocked.longDesc;</div>
|
||||
<div id="ed_remoteXUL">&remoteXUL.longDesc;</div>
|
||||
<div id="ed_corruptedContentErrorv2">&corruptedContentErrorv2.longDesc;</div>
|
||||
<div id="ed_sslv3Used">&sslv3Used.longDesc;</div>
|
||||
<div id="ed_weakCryptoUsed">&weakCryptoUsed.longDesc;</div>
|
||||
<div id="ed_inadequateSecurityError">&inadequateSecurityError.longDesc;</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PAGE CONTAINER (for styling purposes only) -->
|
||||
<div id="errorPageContainer">
|
||||
|
||||
<!-- Error Title -->
|
||||
<div id="errorTitle">
|
||||
<h1 class="errorTitleText" />
|
||||
</div>
|
||||
|
||||
<!-- LONG CONTENT (the section most likely to require scrolling) -->
|
||||
<div id="errorLongContent">
|
||||
|
||||
<!-- Short Description -->
|
||||
<div id="errorShortDesc">
|
||||
<p id="errorShortDescText" />
|
||||
</div>
|
||||
|
||||
<!-- Long Description (Note: See netError.dtd for used XHTML tags) -->
|
||||
<div id="errorLongDesc" />
|
||||
|
||||
<!-- Override section - For ssl errors only. Removed on init for other
|
||||
error types. -->
|
||||
<div id="securityOverrideDiv">
|
||||
<a id="securityOverrideLink" href="javascript:showSecuritySection();" >&securityOverride.linkText;</a>
|
||||
<div id="securityOverrideContent" style="display: none;">&securityOverride.warningContent;</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Retry Button -->
|
||||
<button id="errorTryAgain" onclick="retryThis(this);">&retry.label;</button>
|
||||
|
||||
</div>
|
||||
|
||||
<!--
|
||||
- Note: It is important to run the script this way, instead of using
|
||||
- an onload handler. This is because error pages are loaded as
|
||||
- LOAD_BACKGROUND, which means that onload handlers will not be executed.
|
||||
-->
|
||||
<script type="application/javascript">initPage();</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
71
mobile/android/chrome/jar.mn
Normal file
71
mobile/android/chrome/jar.mn
Normal 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
|
||||
13
mobile/android/chrome/moz.build
Normal file
13
mobile/android/chrome/moz.build
Normal 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']
|
||||
Loading…
Add table
Add a link
Reference in a new issue