merge fx-team to mozilla-central a=merge

This commit is contained in:
Carsten "Tomcat" Book 2015-11-18 14:47:25 +01:00
commit 66749ba7be
27 changed files with 640 additions and 116 deletions

View File

@ -194,9 +194,9 @@ const Frame = Class({
dispose: function() {
emit(this, 'detach', this);
ns(this).messageManager.removeMessageListener('sdk/remote/frame/message', ns(this).messageReceived);
ns(this).messageManager = null;
frameMap.delete(ns(this).messageManager);
ns(this).messageManager = null;
},
// Returns the browser or iframe element this frame displays in

View File

@ -318,15 +318,8 @@ var RemoteTabViewer = {
}
}
// if Clients hasn't synced yet this session, we need to sync it as well.
if (Weave.Service.clientsEngine.lastSync == 0) {
Weave.Service.clientsEngine.sync();
}
// Force a sync only for the tabs engine
let engine = Weave.Service.engineManager.get("tabs");
engine.lastModified = null;
engine.sync();
// Ask Sync to just do the tabs engine if it can.
Weave.Service.sync(["tabs"]);
Services.prefs.setIntPref("services.sync.lastTabFetch",
Math.floor(Date.now() / 1000));

View File

@ -505,6 +505,7 @@ tags = mcb
skip-if = e10s # Bug 1100687 - test directly manipulates content (content.document.getElementById)
[browser_bug1045809.js]
tags = mcb
[browser_bug1225194-remotetab.js]
[browser_e10s_switchbrowser.js]
[browser_e10s_about_process.js]
[browser_e10s_chrome_process.js]

View File

@ -0,0 +1,18 @@
add_task(function* test_remotetab_opens() {
const url = "http://example.org/browser/browser/base/content/test/general/dummy_page.html";
yield BrowserTestUtils.withNewTab({url: "about:robots", gBrowser}, function* () {
// Set the urlbar to include the moz-action
gURLBar.value = "moz-action:remotetab," + JSON.stringify({ url });
// Focus the urlbar so we can press enter
gURLBar.focus();
// The URL is going to open in the current tab as it is currently about:blank
let promiseTabLoaded = promiseTabLoadEvent(gBrowser.selectedTab);
EventUtils.synthesizeKey("VK_RETURN", {});
yield promiseTabLoaded;
Assert.equal(gBrowser.selectedTab.linkedBrowser.currentURI.spec, url, "correct URL loaded");
});
});

View File

@ -4,6 +4,8 @@
* 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/. */
requestLongerTimeout(2);
const TEST_URL_BASES = [
"http://example.org/browser/browser/base/content/test/general/dummy_page.html#tabmatch",
"http://example.org/browser/browser/base/content/test/general/moz.png#tabmatch"

View File

@ -356,6 +356,8 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
gBrowser.removeTab(prevTab);
return;
}
} else if (action.type == "remotetab") {
url = action.params.url;
} else if (action.type == "keyword") {
url = action.params.url;
} else if (action.type == "searchengine") {

View File

@ -65,7 +65,10 @@ global.IconDetails = {
Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
extension.principal, url,
Services.scriptSecurityManager.DISALLOW_SCRIPT);
} catch (e if !context) {
} catch (e) {
if (context) {
throw e;
}
// If there's no context, it's because we're handling this
// as a manifest directive. Log a warning rather than
// raising an error, but don't accept the URL in any case.
@ -172,7 +175,10 @@ global.openPanel = (node, popupURL, extension) => {
GlobalManager.injectInDocShell(browser.docShell, extension, context);
browser.setAttribute("src", context.uri.spec);
let contentLoadListener = () => {
let contentLoadListener = event => {
if (event.target != browser.contentDocument) {
return;
}
browser.removeEventListener("load", contentLoadListener, true);
let contentViewer = browser.docShell.contentViewer;
@ -343,7 +349,7 @@ global.TabManager = {
if (!window.gBrowser) {
return [];
}
return [ for (tab of window.gBrowser.tabs) this.convert(extension, tab) ];
return Array.map(window.gBrowser.tabs, tab => this.convert(extension, tab));
},
};
@ -459,8 +465,8 @@ global.WindowListManager = {
},
handleEvent(event) {
event.currentTarget.removeEventListener(event.type, this);
let window = event.target.defaultView;
window.removeEventListener("load", this);
if (window.document.documentElement.getAttribute("windowtype") != "navigator:browser") {
return;
}

View File

@ -694,8 +694,8 @@ pref("ui.scrolling.overscroll_snap_limit", -1);
pref("ui.scrolling.min_scrollable_distance", -1);
// The axis lock mode for panning behaviour - set between standard, free and sticky
pref("ui.scrolling.axis_lock_mode", "standard");
// Negate scrollY, true will make the mouse scroll wheel move the screen the same direction as with most desktops or laptops.
pref("ui.scrolling.negate_wheel_scrollY", true);
// Negate scroll, true will make the mouse scroll wheel move the screen the same direction as with most desktops or laptops.
pref("ui.scrolling.negate_wheel_scroll", true);
// Determine the dead zone for gamepad joysticks. Higher values result in larger dead zones; use a negative value to
// auto-detect based on reported hardware values
pref("ui.scrolling.gamepad_dead_zone", 115);

View File

@ -689,8 +689,8 @@ pref("ui.scrolling.overscroll_snap_limit", -1);
pref("ui.scrolling.min_scrollable_distance", -1);
// The axis lock mode for panning behaviour - set between standard, free and sticky
pref("ui.scrolling.axis_lock_mode", "standard");
// Negate scrollY, true will make the mouse scroll wheel move the screen the same direction as with most desktops or laptops.
pref("ui.scrolling.negate_wheel_scrollY", true);
// Negate scroll, true will make the mouse scroll wheel move the screen the same direction as with most desktops or laptops.
pref("ui.scrolling.negate_wheel_scroll", true);
// Determine the dead zone for gamepad joysticks. Higher values result in larger dead zones; use a negative value to
// auto-detect based on reported hardware values
pref("ui.scrolling.gamepad_dead_zone", 115);

View File

@ -1180,7 +1180,7 @@ public class GeckoAppShell
context.getResources().getString(R.string.share_title));
}
final Uri uri = normalizeUriScheme(targetURI.indexOf(':') >= 0 ? Uri.parse(targetURI) : new Uri.Builder().scheme(targetURI).build());
Uri uri = normalizeUriScheme(targetURI.indexOf(':') >= 0 ? Uri.parse(targetURI) : new Uri.Builder().scheme(targetURI).build());
if (!TextUtils.isEmpty(mimeType)) {
Intent intent = getIntentForActionString(action);
intent.setDataAndType(uri, mimeType);
@ -1226,9 +1226,9 @@ public class GeckoAppShell
return intent;
}
// Have a special handling for SMS, as the message body
// is not extracted from the URI automatically.
if (!"sms".equals(scheme) && !"smsto".equals(scheme)) {
// Have a special handling for SMS based schemes, as the query parameters
// are not extracted from the URI automatically.
if (!"sms".equals(scheme) && !"smsto".equals(scheme) && !"mms".equals(scheme) && !"mmsto".equals(scheme)) {
return intent;
}
@ -1237,27 +1237,44 @@ public class GeckoAppShell
return intent;
}
final String[] fields = query.split("&");
boolean foundBody = false;
String resultQuery = "";
for (String field : fields) {
if (foundBody || !field.startsWith("body=")) {
resultQuery = resultQuery.concat(resultQuery.length() > 0 ? "&" + field : field);
continue;
}
// Found the first body param. Put it into the intent.
final String body = Uri.decode(field.substring(5));
intent.putExtra("sms_body", body);
foundBody = true;
// It is common to see sms*/mms* uris on the web without '//', it is W3C standard not to have the slashes,
// but android's Uri builder & Uri require the slashes and will interpret those without as malformed.
String currentUri = uri.toString();
String correctlyFormattedDataURIScheme = scheme + "://";
if (!currentUri.contains(correctlyFormattedDataURIScheme)) {
uri = Uri.parse(currentUri.replaceFirst(scheme + ":", correctlyFormattedDataURIScheme));
}
if (!foundBody) {
final String[] fields = query.split("&");
boolean shouldUpdateIntent = false;
String resultQuery = "";
for (String field : fields) {
if (field.startsWith("body=")) {
final String body = Uri.decode(field.substring(5));
intent.putExtra("sms_body", body);
shouldUpdateIntent = true;
} else if (field.startsWith("subject=")) {
final String subject = Uri.decode(field.substring(8));
intent.putExtra("subject", subject);
shouldUpdateIntent = true;
} else if (field.startsWith("cc=")) {
final String ccNumber = Uri.decode(field.substring(3));
String phoneNumber = uri.getAuthority();
if (phoneNumber != null) {
uri = uri.buildUpon().encodedAuthority(phoneNumber + ";" + ccNumber).build();
}
shouldUpdateIntent = true;
} else {
resultQuery = resultQuery.concat(resultQuery.length() > 0 ? "&" + field : field);
}
}
if (!shouldUpdateIntent) {
// No need to rewrite the URI, then.
return intent;
}
// Form a new URI without the body field in the query part, and
// Form a new URI without the extracted fields in the query part, and
// push that into the new Intent.
final String newQuery = resultQuery.length() > 0 ? "?" + resultQuery : "";
final Uri pruned = uri.buildUpon().encodedQuery(newQuery).build();

View File

@ -22,6 +22,7 @@ import org.mozilla.gecko.util.ThreadUtils;
import android.graphics.PointF;
import android.graphics.RectF;
import android.util.Log;
import android.util.TypedValue;
import android.view.GestureDetector;
import android.view.InputDevice;
import android.view.KeyEvent;
@ -120,14 +121,16 @@ class JavaPanZoomController
private AxisLockMode mMode;
/* Whether or not to wait for a double-tap before dispatching a single-tap */
private boolean mWaitForDoubleTap;
/* Used to change the scrollY direction */
private boolean mNegateWheelScrollY;
/* Used to change the scroll direction */
private boolean mNegateWheelScroll;
/* Whether the current event has been default-prevented. */
private boolean mDefaultPrevented;
/* Whether longpress events are enabled, or suppressed by robocop tests. */
private boolean isLongpressEnabled;
/* Whether longpress detection should be ignored */
private boolean mIgnoreLongPress;
/* Pointer scrolling delta, scaled by the preferred list item height which matches Android platform behavior */
private float mPointerScrollFactor;
// Handler to be notified when overscroll occurs
private Overscroll mOverscroll;
@ -153,7 +156,7 @@ class JavaPanZoomController
mMode = AxisLockMode.STANDARD;
String[] prefs = { "ui.scrolling.axis_lock_mode",
"ui.scrolling.negate_wheel_scrollY",
"ui.scrolling.negate_wheel_scroll",
"ui.scrolling.gamepad_dead_zone" };
PrefsHelper.getPrefs(prefs, new PrefsHelper.PrefHandlerBase() {
@Override public void prefValue(String pref, String value) {
@ -175,8 +178,8 @@ class JavaPanZoomController
}
@Override public void prefValue(String pref, boolean value) {
if (pref.equals("ui.scrolling.negate_wheel_scrollY")) {
mNegateWheelScrollY = value;
if (pref.equals("ui.scrolling.negate_wheel_scroll")) {
mNegateWheelScroll = value;
}
}
@ -188,6 +191,13 @@ class JavaPanZoomController
});
Axis.initPrefs();
TypedValue outValue = new TypedValue();
if (view.getContext().getTheme().resolveAttribute(android.R.attr.listPreferredItemHeight, outValue, true)) {
mPointerScrollFactor = outValue.getDimension(view.getContext().getResources().getDisplayMetrics());
} else {
mPointerScrollFactor = MAX_SCROLL;
}
}
@Override
@ -577,10 +587,11 @@ class JavaPanZoomController
if (mState == PanZoomState.NOTHING || mState == PanZoomState.FLING) {
float scrollX = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
float scrollY = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
if (mNegateWheelScrollY) {
if (mNegateWheelScroll) {
scrollX *= -1.0;
scrollY *= -1.0;
}
scrollBy(scrollX * MAX_SCROLL, scrollY * MAX_SCROLL);
scrollBy(scrollX * mPointerScrollFactor, scrollY * mPointerScrollFactor);
bounce();
return true;
}

View File

@ -34,9 +34,7 @@ var modules = {
},
rights: {
uri: AppConstants.MOZ_OFFICIAL_BRANDING ?
"chrome://browser/content/aboutRights.xhtml" :
"chrome://global/content/aboutRights.xhtml",
uri: "chrome://browser/content/aboutRights.xhtml",
privileged: false
},
blocked: {

View File

@ -1268,7 +1268,7 @@ Sync11Service.prototype = {
return reason;
},
sync: function sync() {
sync: function sync(engineNamesToSync) {
if (!this.enabled) {
this._log.debug("Not syncing as Sync is disabled.");
return;
@ -1288,14 +1288,14 @@ Sync11Service.prototype = {
else {
this._log.trace("In sync: no need to login.");
}
return this._lockedSync.apply(this, arguments);
return this._lockedSync(engineNamesToSync);
})();
},
/**
* Sync up engines with the server.
*/
_lockedSync: function _lockedSync() {
_lockedSync: function _lockedSync(engineNamesToSync) {
return this._lock("service.js: sync",
this._notify("sync", "", function onNotify() {
@ -1306,7 +1306,7 @@ Sync11Service.prototype = {
let cb = Async.makeSpinningCallback();
synchronizer.onComplete = cb;
synchronizer.sync();
synchronizer.sync(engineNamesToSync);
// wait() throws if the first argument is truthy, which is exactly what
// we want.
let result = cb.wait();

View File

@ -31,7 +31,7 @@ this.EngineSynchronizer = function EngineSynchronizer(service) {
}
EngineSynchronizer.prototype = {
sync: function sync() {
sync: function sync(engineNamesToSync) {
if (!this.onComplete) {
throw new Error("onComplete handler not installed.");
}
@ -96,6 +96,9 @@ EngineSynchronizer.prototype = {
return;
}
// We only honor the "hint" of what engines to Sync if this isn't
// a first sync.
let allowEnginesHint = false;
// Wipe data in the desired direction if necessary
switch (Svc.Prefs.get("firstSync")) {
case "resetClient":
@ -107,6 +110,9 @@ EngineSynchronizer.prototype = {
case "wipeRemote":
this.service.wipeRemote(engineManager.enabledEngineNames);
break;
default:
allowEnginesHint = true;
break;
}
if (this.service.clientsEngine.localCommands) {
@ -143,8 +149,17 @@ EngineSynchronizer.prototype = {
return;
}
// If the engines to sync has been specified, we sync in the order specified.
let enginesToSync;
if (allowEnginesHint && engineNamesToSync) {
this._log.info("Syncing specified engines", engineNamesToSync);
enginesToSync = engineManager.get(engineNamesToSync).filter(e => e.enabled);
} else {
this._log.info("Syncing all enabled engines.");
enginesToSync = engineManager.getEnabled();
}
try {
for (let engine of engineManager.getEnabled()) {
for (let engine of enginesToSync) {
// If there's any problems with syncing the engine, report the failure
if (!(this._syncEngine(engine)) || this.service.status.enforceBackoff) {
this._log.info("Aborting sync for failure in " + engine.name);

View File

@ -0,0 +1,159 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/engines/clients.js");
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://testing-common/services/sync/utils.js");
initTestLogging();
Service.engineManager.clear();
let syncedEngines = []
function SteamEngine() {
SyncEngine.call(this, "Steam", Service);
}
SteamEngine.prototype = {
__proto__: SyncEngine.prototype,
_sync: function _sync() {
syncedEngines.push(this.name);
}
};
Service.engineManager.register(SteamEngine);
function StirlingEngine() {
SyncEngine.call(this, "Stirling", Service);
}
StirlingEngine.prototype = {
__proto__: SteamEngine.prototype,
_sync: function _sync() {
syncedEngines.push(this.name);
}
};
Service.engineManager.register(StirlingEngine);
// Tracking info/collections.
var collectionsHelper = track_collections_helper();
var upd = collectionsHelper.with_updated_collection;
function sync_httpd_setup(handlers) {
handlers["/1.1/johndoe/info/collections"] = collectionsHelper.handler;
delete collectionsHelper.collections.crypto;
delete collectionsHelper.collections.meta;
let cr = new ServerWBO("keys");
handlers["/1.1/johndoe/storage/crypto/keys"] =
upd("crypto", cr.handler());
let cl = new ServerCollection();
handlers["/1.1/johndoe/storage/clients"] =
upd("clients", cl.handler());
return httpd_setup(handlers);
}
function setUp() {
syncedEngines = [];
let engine = Service.engineManager.get("steam");
engine.enabled = true;
engine.syncPriority = 1;
engine = Service.engineManager.get("stirling");
engine.enabled = true;
engine.syncPriority = 2;
let server = sync_httpd_setup({
"/1.1/johndoe/storage/meta/global": new ServerWBO("global", {}).handler(),
});
new SyncTestingInfrastructure(server, "johndoe", "ilovejane",
"abcdeabcdeabcdeabcdeabcdea");
return server;
}
function run_test() {
initTestLogging("Trace");
Log.repository.getLogger("Sync.Service").level = Log.Level.Trace;
Log.repository.getLogger("Sync.ErrorHandler").level = Log.Level.Trace;
run_next_test();
}
add_test(function test_noEngines() {
_("Test: An empty array of engines to sync does nothing.");
let server = setUp();
try {
_("Sync with no engines specified.");
Service.sync([]);
deepEqual(syncedEngines, [], "no engines were synced");
} finally {
Service.startOver();
server.stop(run_next_test);
}
});
add_test(function test_oneEngine() {
_("Test: Only one engine is synced.");
let server = setUp();
try {
_("Sync with 1 engine specified.");
Service.sync(["steam"]);
deepEqual(syncedEngines, ["steam"])
} finally {
Service.startOver();
server.stop(run_next_test);
}
});
add_test(function test_bothEnginesSpecified() {
_("Test: All engines are synced when specified in the correct order (1).");
let server = setUp();
try {
_("Sync with both engines specified.");
Service.sync(["steam", "stirling"]);
deepEqual(syncedEngines, ["steam", "stirling"])
} finally {
Service.startOver();
server.stop(run_next_test);
}
});
add_test(function test_bothEnginesSpecified() {
_("Test: All engines are synced when specified in the correct order (2).");
let server = setUp();
try {
_("Sync with both engines specified.");
Service.sync(["stirling", "steam"]);
deepEqual(syncedEngines, ["stirling", "steam"])
} finally {
Service.startOver();
server.stop(run_next_test);
}
});
add_test(function test_bothEnginesDefault() {
_("Test: All engines are synced when nothing is specified.");
let server = setUp();
try {
Service.sync();
deepEqual(syncedEngines, ["steam", "stirling"])
} finally {
Service.startOver();
server.stop(run_next_test);
}
});

View File

@ -93,6 +93,7 @@ skip-if = os == "mac" || os == "linux"
[test_service_sync_remoteSetup.js]
# Bug 676978: test hangs on Android (see also testing/xpcshell/xpcshell.ini)
skip-if = os == "android"
[test_service_sync_specified.js]
[test_service_sync_updateEnabledEngines.js]
# Bug 676978: test hangs on Android (see also testing/xpcshell/xpcshell.ini)
skip-if = os == "android"

View File

@ -6,6 +6,40 @@ ExtensionTestUtils.loadExtension = function(ext, id = null)
var testDone = new Promise(resolve => { testResolve = resolve; });
var messageHandler = new Map();
var messageAwaiter = new Map();
var messageQueue = [];
SimpleTest.registerCleanupFunction(() => {
if (messageQueue.length) {
SimpleTest.is(messageQueue.length, 0, "message queue is empty");
}
if (messageAwaiter.size) {
SimpleTest.is(messageAwaiter.size, 0, "no tasks awaiting on messages");
}
});
function checkMessages() {
while (messageQueue.length) {
let [msg, ...args] = messageQueue[0];
let listener = messageAwaiter.get(msg);
if (!listener) {
break;
}
messageQueue.shift();
messageAwaiter.delete(msg);
listener.resolve(...args);
}
}
function checkDuplicateListeners(msg) {
if (messageHandler.has(msg) || messageAwaiter.has(msg)) {
throw new Error("only one message handler allowed");
}
}
function testHandler(kind, pass, msg, ...args) {
if (kind == "test-eq") {
@ -29,11 +63,13 @@ ExtensionTestUtils.loadExtension = function(ext, id = null)
testMessage(msg, ...args) {
var handler = messageHandler.get(msg);
if (!handler) {
return;
if (handler) {
handler(...args);
} else {
messageQueue.push([msg, ...args]);
checkMessages();
}
handler(...args);
},
};
@ -41,21 +77,15 @@ ExtensionTestUtils.loadExtension = function(ext, id = null)
extension.awaitMessage = (msg) => {
return new Promise(resolve => {
if (messageHandler.has(msg)) {
throw new Error("only one message handler allowed");
}
checkDuplicateListeners(msg);
messageHandler.set(msg, (...args) => {
messageHandler.delete(msg);
resolve(...args);
});
messageAwaiter.set(msg, {resolve});
checkMessages();
});
};
extension.onMessage = (msg, callback) => {
if (messageHandler.has(msg)) {
throw new Error("only one message handler allowed");
}
checkDuplicateListeners(msg);
messageHandler.set(msg, callback);
};

View File

@ -66,6 +66,7 @@ var {
injectAPI,
extend,
flushJarCache,
instanceOf,
} = ExtensionUtils;
const LOGGER_ID_BASE = "addons.webextension.";
@ -322,6 +323,9 @@ var GlobalManager = {
let eventHandler = docShell.chromeEventHandler;
let listener = event => {
if (event.target != docShell.contentViewer.DOMDocument) {
return;
}
eventHandler.removeEventListener("unload", listener);
context.unload();
};
@ -343,10 +347,10 @@ this.ExtensionData = function(rootURI)
this.manifest = null;
this.id = null;
// Map(locale-name -> message-map)
// Map(locale-name -> Map(message-key -> localized-strings))
//
// Contains a key for each loaded locale, each of which is a
// JSON-compatible object with a property for each message
// in that locale.
// Map of message keys to their localized strings.
this.localeMessages = new Map();
this.selectedLocale = null;
this._promiseLocales = null;
@ -372,41 +376,36 @@ ExtensionData.prototype = {
},
// https://developer.chrome.com/extensions/i18n
localizeMessage(message, substitutions, locale = this.selectedLocale) {
let messages = {};
if (this.localeMessages.has(locale)) {
messages = this.localeMessages.get(locale);
}
localizeMessage(message, substitutions = [], locale = this.selectedLocale, defaultValue = "??") {
let locales = new Set([locale, this.defaultLocale]
.filter(locale => this.localeMessages.has(locale)));
if (message in messages) {
let str = messages[message].message;
// Message names are case-insensitive, so normalize them to lower-case.
message = message.toLowerCase();
for (let locale of locales) {
let messages = this.localeMessages.get(locale);
if (messages.has(message)) {
let str = messages.get(message)
if (!substitutions) {
substitutions = [];
} else if (!Array.isArray(substitutions)) {
substitutions = [substitutions];
}
// https://developer.chrome.com/extensions/i18n-messages
// |str| may contain substrings of the form $1 or $PLACEHOLDER$.
// In the former case, we replace $n with substitutions[n - 1].
// In the latter case, we consult the placeholders array.
// The placeholder may itself use $n to refer to substitutions.
let replacer = (matched, name) => {
if (name.length == 1 && name[0] >= '1' && name[0] <= '9') {
return substitutions[parseInt(name) - 1];
} else {
let content = messages[message].placeholders[name].content;
if (content[0] == '$') {
return replacer(matched, content[1]);
} else {
return content;
}
if (!Array.isArray(substitutions)) {
substitutions = [substitutions];
}
};
return str.replace(/\$([A-Za-z_@]+)\$/, replacer)
.replace(/\$([0-9]+)/, replacer)
.replace(/\$\$/, "$");
let replacer = (matched, index, dollarSigns) => {
if (index) {
// This is not quite Chrome-compatible. Chrome consumes any number
// of digits following the $, but only accepts 9 substitutions. We
// accept any number of substitutions.
index = parseInt(index) - 1;
return index in substitutions ? substitutions[index] : "";
} else {
// For any series of contiguous `$`s, the first is dropped, and
// the rest remain in the output string.
return dollarSigns;
}
};
return str.replace(/\$(?:([1-9]\d*)|(\$+))/g, replacer);
}
}
// Check for certain pre-defined messages.
@ -425,7 +424,7 @@ ExtensionData.prototype = {
}
Cu.reportError(`Unknown localization message ${message}`);
return "??";
return defaultValue;
},
// Localize a string, replacing all |__MSG_(.*)__| tokens with the
@ -440,7 +439,7 @@ ExtensionData.prototype = {
}
return str.replace(/__MSG_([A-Za-z0-9@_]+?)__/g, (matched, message) => {
return this.localizeMessage(message, [], locale);
return this.localizeMessage(message, [], locale, matched);
});
},
@ -565,6 +564,61 @@ ExtensionData.prototype = {
});
},
// Validates the contents of a locale JSON file, normalizes the
// messages into a Map of message key -> localized string pairs.
processLocale(locale, messages) {
let result = new Map();
// Chrome does not document the semantics of its localization
// system very well. It handles replacements by pre-processing
// messages, replacing |$[a-zA-Z0-9@_]+$| tokens with the value of their
// replacements. Later, it processes the resulting string for
// |$[0-9]| replacements.
//
// Again, it does not document this, but it accepts any number
// of sequential |$|s, and replaces them with that number minus
// 1. It also accepts |$| followed by any number of sequential
// digits, but refuses to process a localized string which
// provides more than 9 substitutions.
if (!instanceOf(messages, "Object")) {
this.packagingError(`Invalid locale data for ${locale}`);
return result;
}
for (let key of Object.keys(messages)) {
let msg = messages[key];
if (!instanceOf(msg, "Object") || typeof(msg.message) != "string") {
this.packagingError(`Invalid locale message data for ${locale}, message ${JSON.stringify(key)}`);
continue;
}
// Substitutions are case-insensitive, so normalize all of their names
// to lower-case.
let placeholders = new Map();
if (instanceOf(msg.placeholders, "Object")) {
for (let key of Object.keys(msg.placeholders)) {
placeholders.set(key.toLowerCase(), msg.placeholders[key]);
}
}
let replacer = (match, name) => {
let replacement = placeholders.get(name.toLowerCase());
if (instanceOf(replacement, "Object") && "content" in replacement) {
return replacement.content;
}
return "";
};
let value = msg.message.replace(/\$([A-Za-z0-9@_]+)\$/g, replacer);
// Message names are also case-insensitive, so normalize them to lower-case.
result.set(key.toLowerCase(), value);
}
return result;
},
// Reads the locale file for the given Gecko-compatible locale code, and
// stores its parsed contents in |this.localeMessages.get(locale)|.
readLocaleFile: Task.async(function* (locale) {
@ -572,9 +626,10 @@ ExtensionData.prototype = {
let dir = locales.get(locale);
let file = `_locales/${dir}/messages.json`;
let messages = {};
let messages = new Map();
try {
messages = yield this.readJSON(file);
messages = this.processLocale(locale, messages);
} catch (e) {
this.packagingError(`Loading locale file ${file}: ${e}`);
}
@ -638,16 +693,25 @@ ExtensionData.prototype = {
// default locale if no locale code is given, and sets it as the currently
// selected locale on success.
//
// Pre-loads the default locale for fallback message processing, regardless
// of the locale specified.
//
// If no locales are unavailable, resolves to |null|.
initLocale: Task.async(function* (locale = this.defaultLocale) {
if (locale == null) {
return null;
}
let localeData = yield this.readLocaleFile(locale);
let promises = [this.readLocaleFile(locale)];
let { defaultLocale } = this;
if (locale != defaultLocale && !this.localeMessages.has(defaultLocale)) {
promises.push(this.readLocaleFile(defaultLocale));
}
let results = yield Promise.all(promises);
this.selectedLocale = locale;
return localeData;
return results[0];
}),
};
@ -777,7 +841,7 @@ this.Extension.generate = function(id, data)
files[bgScript] = data.background;
}
provide(files, ["manifest.json"], JSON.stringify(manifest));
provide(files, ["manifest.json"], manifest);
let ZipWriter = Components.Constructor("@mozilla.org/zipwriter;1", "nsIZipWriter");
let zipW = new ZipWriter();
@ -807,6 +871,8 @@ this.Extension.generate = function(id, data)
let script = files[filename];
if (typeof(script) == "function") {
script = "(" + script.toString() + ")()";
} else if (typeof(script) == "object") {
script = JSON.stringify(script);
}
let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
@ -940,7 +1006,7 @@ Extension.prototype = extend(Object.create(ExtensionData.prototype), {
if (locale === undefined) {
let locales = yield this.promiseLocales();
let localeList = Object.keys(locales).map(locale => {
let localeList = Array.from(locales.keys(), locale => {
return { name: locale, locales: [locale] };
});
@ -971,7 +1037,7 @@ Extension.prototype = extend(Object.create(ExtensionData.prototype), {
return this.runManifest(this.manifest);
}).catch(e => {
dump(`Extension error: ${e} ${e.filename}:${e.lineNumber}\n`);
dump(`Extension error: ${e} ${e.filename || e.fileName}:${e.lineNumber}\n`);
Cu.reportError(e);
throw e;
});

View File

@ -330,7 +330,13 @@ var DocumentManager = {
},
handleEvent: function(event) {
let window = event.target.defaultView;
let window = event.currentTarget;
if (event.target != window.document) {
// We use capturing listeners so we have precedence over content script
// listeners, but only care about events targeted to the element we're
// listening on.
return;
}
window.removeEventListener(event.type, this, true);
// Need to check if we're still on the right page? Greasemonkey does this.
@ -437,7 +443,7 @@ function BrowserExtensionContent(data)
this.id = data.id;
this.uuid = data.uuid;
this.data = data;
this.scripts = [ for (scriptData of data.content_scripts) new Script(scriptData) ];
this.scripts = data.content_scripts.map(scriptData => new Script(scriptData));
this.webAccessibleResources = data.webAccessibleResources;
this.whiteListedHosts = data.whiteListedHosts;

View File

@ -64,6 +64,12 @@ function runSafe(context, f, ...args)
return runSafeWithoutClone(f, ...args);
}
// Return true if the given value is an instance of the given
// native type.
function instanceOf(value, type) {
return {}.toString.call(value) == `[object ${type}]`;
}
// Extend the object |obj| with the property descriptors of each object in
// |args|.
function extend(obj, ...args) {
@ -634,4 +640,5 @@ this.ExtensionUtils = {
Messenger,
extend,
flushJarCache,
instanceOf,
};

View File

@ -121,7 +121,7 @@ extensions.registerAPI((extension, context) => {
getAll: function(callback) {
let alarms = alarmsMap.get(extension);
result = [ for (alarm of alarms) alarm.data ];
result = alarms.map(alarm => alarm.data);
runSafe(context, callback, result);
},

View File

@ -53,7 +53,11 @@ BackgroundPage.prototype = {
// TODO: Right now we run onStartup after the background page
// finishes. See if this is what Chrome does.
window.windowRoot.addEventListener("load", () => {
let loadListener = event => {
if (event.target != window.document) {
return;
}
event.currentTarget.removeEventListener("load", loadListener, true);
if (this.scripts) {
let doc = window.document;
for (let script of this.scripts) {
@ -74,7 +78,8 @@ BackgroundPage.prototype = {
if (this.extension.onStartup) {
this.extension.onStartup();
}
}, true);
};
window.windowRoot.addEventListener("load", loadListener, true);
},
shutdown() {

View File

@ -121,7 +121,7 @@ extensions.registerPrivilegedAPI("notifications", (extension, context) => {
getAll: function(callback) {
let notifications = notificationsMap.get(extension);
notifications = [ for (notification of notifications) notification.id ];
notifications = notifications.map(notification => notification.id);
runSafe(context, callback, notifications);
},

View File

@ -35,5 +35,7 @@ support-files =
[test_ext_bookmarks.html]
[test_ext_alarms.html]
[test_ext_background_window_properties.html]
[test_ext_background_sub_windows.html]
[test_ext_jsversion.html]
skip-if = e10s # Uses a console monitor which doesn't work from a content process. The code being tested doesn't run in a tab content process in any case.
[test_ext_i18n.html]

View File

@ -0,0 +1,64 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test for sub-frames of WebExtension background pages</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
<script type="text/javascript" src="head.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<script type="application/javascript;version=1.8">
add_task(function* testBackgroundWindow() {
let extension = ExtensionTestUtils.loadExtension({
background: "new " + function() {
browser.test.log("background script executed");
browser.test.sendMessage("background-script-load");
let img = document.createElement("img");
img.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
document.body.appendChild(img);
img.onload = () => {
browser.test.log("image loaded");
let iframe = document.createElement("iframe");
iframe.src = "about:blank?1";
iframe.onload = () => {
browser.test.log("iframe loaded");
setTimeout(() => {
browser.test.notifyPass("background sub-window test done");
}, 0);
};
document.body.appendChild(iframe);
};
},
});
info("extension loaded");
let loadCount = 0;
extension.onMessage("background-script-load", () => {
loadCount++;
});
yield extension.startup();
info("startup complete loaded");
yield extension.awaitFinish("background sub-window test done")
is(loadCount, 1, "background script loaded only once");
yield extension.unload();
});
</script>
</body>
</html>

View File

@ -0,0 +1,121 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test for WebExtension localization APIs</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
<script type="text/javascript" src="head.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<script type="application/javascript;version=1.8">
add_task(function* test_i18n() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
"default_locale": "jp",
},
files: {
"_locales/en_US/messages.json": {
"foo": {
"message": "Foo.",
"description": "foo",
},
"basic_substitutions": {
"message": "'$0' '$14' '$1' '$5' '$$$$$' '$$'.",
"description": "foo",
},
"Named_placeholder_substitutions": {
"message": "$Foo$\n$2\n$bad name$\n$bad_value$\n$bad_content_value$\n$foo",
"description": "foo",
"placeholders": {
"foO": {
"content": "_foo_ $1 _bar_",
"description": "foo",
},
"bad name": {
"content": "Nope.",
"description": "bad name",
},
"bad_value": "Nope.",
"bad_content_value": {
"content": ["Accepted, but shouldn't break."],
"description": "bad value",
},
},
},
"broken_placeholders": {
"message": "$broken$",
"description": "broken placeholders",
"placeholders": "foo.",
},
},
"_locales/jp/messages.json": {
"foo": {
"message": "(foo)",
"description": "foo",
},
"bar": {
"message": "(bar)",
"description": "bar",
},
},
},
background: "new " + function() {
let _ = browser.i18n.getMessage.bind(browser.i18n);
browser.test.assertEq("Foo.", _("Foo"), "Simple message in selected locale.");
browser.test.assertEq("(bar)", _("bar"), "Simple message fallback in default locale.");
let substitutions = [];
substitutions[4] = "5";
substitutions[13] = "14";
browser.test.assertEq("'$0' '14' '' '5' '$$$$' '$'.",
_("basic_substitutions", substitutions),
"Basic numeric substitutions");
browser.test.assertEq("'$0' '' 'just a string' '' '$$$$' '$'.",
_("basic_substitutions", "just a string"),
"Basic numeric substitutions, with non-array value");
let values = _("named_placeholder_substitutions", ["(subst $1 $2)", "(2 $1 $2)"]).split("\n");
browser.test.assertEq("_foo_ (subst $1 $2) _bar_", values[0], "Named and numeric substitution");
browser.test.assertEq("(2 $1 $2)", values[1], "Numeric substitution amid named placeholders");
browser.test.assertEq("$bad name$", values[2], "Named placeholder with invalid key");
browser.test.assertEq("", values[3], "Named placeholder with an invalid value");
browser.test.assertEq("Accepted, but shouldn't break.", values[4], "Named placeholder with a strange content value");
browser.test.assertEq("$foo", values[5], "Non-placeholder token that should be ignored");
browser.test.notifyPass("l10n");
},
});
yield extension.startup();
yield extension.awaitFinish("l10n");
yield extension.unload();
});
</script>
</body>
</html>

View File

@ -1104,7 +1104,7 @@ var Histogram = {
+ " ".repeat(Math.max(0, labelPadTo - String(label).length)) + label // Right-aligned label
+ " |" + "#".repeat(Math.round(MAX_BAR_CHARS * barValue / maxBarValue)) // Bar
+ " " + value // Value
+ " " + Math.round(100 * value / aHgram.sum) + "%"; // Percentage
+ " " + Math.round(100 * value / aHgram.sample_count) + "%"; // Percentage
// Construct the HTML labels + bars
let belowEm = Math.round(MAX_BAR_HEIGHT * (barValue / maxBarValue) * 10) / 10;