From 74083aac1d69fc66e1db6941fae3016fefa07f30 Mon Sep 17 00:00:00 2001 From: Margaret Leibovic Date: Mon, 16 Nov 2015 17:59:19 -0500 Subject: [PATCH 01/17] Bug 1212889 - Always use chrome://browser/content/aboutRights.xhtml to ensure mobile version of about:rights. r=mfinkle --HG-- extra : commitid : IQWuv47V5So extra : rebase_source : ffb1eff77a20af19c5f876977064336b68202cfe --- mobile/android/components/AboutRedirector.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mobile/android/components/AboutRedirector.js b/mobile/android/components/AboutRedirector.js index df88f5b68f77..6b544258cf40 100644 --- a/mobile/android/components/AboutRedirector.js +++ b/mobile/android/components/AboutRedirector.js @@ -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: { From ff22ab370fcec7e2e1ebc595e0cc9984c7e61800 Mon Sep 17 00:00:00 2001 From: Sebastian Hengst Date: Fri, 6 Nov 2015 13:35:03 +0100 Subject: [PATCH 02/17] Bug 1222419 - copying histogram: percental distribution values should have correct values. r=gfritzsche --- toolkit/content/aboutTelemetry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolkit/content/aboutTelemetry.js b/toolkit/content/aboutTelemetry.js index 6b5df6f66b0e..7282e0703624 100644 --- a/toolkit/content/aboutTelemetry.js +++ b/toolkit/content/aboutTelemetry.js @@ -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; From ef3b6a71071a1b76c38a2c2b358a1122f6542133 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Mon, 2 Nov 2015 18:09:09 -0800 Subject: [PATCH 03/17] Bug 1224961: Remove non-standard JS features from WebExtension code --HG-- extra : commitid : uASoVfuiAC extra : rebase_source : ba0dfb11eda8d1c3345463e3a8c25796ddcfb5dd --- browser/components/extensions/ext-utils.js | 7 +++++-- toolkit/components/extensions/ExtensionContent.jsm | 2 +- toolkit/components/extensions/ext-alarms.js | 2 +- toolkit/components/extensions/ext-notifications.js | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/browser/components/extensions/ext-utils.js b/browser/components/extensions/ext-utils.js index 6d4c95074b93..31bedad73c73 100644 --- a/browser/components/extensions/ext-utils.js +++ b/browser/components/extensions/ext-utils.js @@ -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. @@ -343,7 +346,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)); }, }; diff --git a/toolkit/components/extensions/ExtensionContent.jsm b/toolkit/components/extensions/ExtensionContent.jsm index c4c80de1b48e..4c6200ded669 100644 --- a/toolkit/components/extensions/ExtensionContent.jsm +++ b/toolkit/components/extensions/ExtensionContent.jsm @@ -437,7 +437,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; diff --git a/toolkit/components/extensions/ext-alarms.js b/toolkit/components/extensions/ext-alarms.js index e294ff092567..6ee906e1db38 100644 --- a/toolkit/components/extensions/ext-alarms.js +++ b/toolkit/components/extensions/ext-alarms.js @@ -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); }, diff --git a/toolkit/components/extensions/ext-notifications.js b/toolkit/components/extensions/ext-notifications.js index bb8c4be9739b..76755ac66a0a 100644 --- a/toolkit/components/extensions/ext-notifications.js +++ b/toolkit/components/extensions/ext-notifications.js @@ -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); }, From 3a3698677dbded14f6209744eb23bb9b63d394bf Mon Sep 17 00:00:00 2001 From: Andy Chen Date: Fri, 30 Oct 2015 14:53:57 -0400 Subject: [PATCH 04/17] Bug 1225566 - For pointer scrolling, we need to match Android platform behavior, to use the provided deltas, scaled by the preferred list item height. r=kats --HG-- extra : commitid : K2KoKjKAsYo --- mobile/android/base/gfx/JavaPanZoomController.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/mobile/android/base/gfx/JavaPanZoomController.java b/mobile/android/base/gfx/JavaPanZoomController.java index b06ec4dcc056..9c850dd34e88 100644 --- a/mobile/android/base/gfx/JavaPanZoomController.java +++ b/mobile/android/base/gfx/JavaPanZoomController.java @@ -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; @@ -128,6 +129,8 @@ class JavaPanZoomController 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; @@ -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 @@ -580,7 +590,7 @@ class JavaPanZoomController if (mNegateWheelScrollY) { scrollY *= -1.0; } - scrollBy(scrollX * MAX_SCROLL, scrollY * MAX_SCROLL); + scrollBy(scrollX * mPointerScrollFactor, scrollY * mPointerScrollFactor); bounce(); return true; } From 0f81205c25ff597b270ad209c4665c599fb4f15b Mon Sep 17 00:00:00 2001 From: Andy Chen Date: Fri, 30 Oct 2015 15:32:59 -0400 Subject: [PATCH 05/17] Bug 1225590 - Negate both X and Y for mouse wheel scrolling, which matches Android platform behaviour. r=kats --HG-- extra : commitid : IiuCMKQ68oI --- mobile/android/app/mobile.js | 4 ++-- mobile/android/b2gdroid/app/b2gdroid.js | 4 ++-- mobile/android/base/gfx/JavaPanZoomController.java | 13 +++++++------ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/mobile/android/app/mobile.js b/mobile/android/app/mobile.js index ff413bbeafdd..a0e28b898473 100644 --- a/mobile/android/app/mobile.js +++ b/mobile/android/app/mobile.js @@ -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); diff --git a/mobile/android/b2gdroid/app/b2gdroid.js b/mobile/android/b2gdroid/app/b2gdroid.js index a1cd10a848c6..52423abac57b 100644 --- a/mobile/android/b2gdroid/app/b2gdroid.js +++ b/mobile/android/b2gdroid/app/b2gdroid.js @@ -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); diff --git a/mobile/android/base/gfx/JavaPanZoomController.java b/mobile/android/base/gfx/JavaPanZoomController.java index 9c850dd34e88..04d3feb0e983 100644 --- a/mobile/android/base/gfx/JavaPanZoomController.java +++ b/mobile/android/base/gfx/JavaPanZoomController.java @@ -121,8 +121,8 @@ 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. */ @@ -156,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) { @@ -178,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; } } @@ -587,7 +587,8 @@ 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 * mPointerScrollFactor, scrollY * mPointerScrollFactor); From 6b321abca2f409fa8b87932b8b2c4fdac9f3b83c Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Sun, 15 Nov 2015 00:29:55 -0800 Subject: [PATCH 06/17] Bug 1224893: [webext] Queue unexpected messages for subsequent calls to awaitMessage --HG-- extra : commitid : CjtZ0fLwmsI extra : rebase_source : a0d7eaaaa8143fcc43deedae9bdc12d8174d3d8c --- .../tests/SimpleTest/ExtensionTestUtils.js | 54 ++++++++++++++----- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js b/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js index 43e0e75c32ab..761dca14e646 100644 --- a/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js +++ b/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js @@ -6,6 +6,38 @@ 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() { + if (messageQueue.length) { + let [msg, ...args] = messageQueue[0]; + + let listener = messageAwaiter.get(msg); + if (listener) { + 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 +61,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 +75,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); }; From f53316bcb8c81fa2df21da3edd8906962da12867 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Tue, 17 Nov 2015 14:04:53 -0800 Subject: [PATCH 07/17] Bug 1224893: [webext] Queue unexpected messages for subsequent calls to awaitMessage, follow-up. r=me r=billm --HG-- extra : commitid : 1CwK1XzJye --- .../tests/SimpleTest/ExtensionTestUtils.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js b/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js index 761dca14e646..586a19c62163 100644 --- a/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js +++ b/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js @@ -20,16 +20,18 @@ ExtensionTestUtils.loadExtension = function(ext, id = null) }); function checkMessages() { - if (messageQueue.length) { + while (messageQueue.length) { let [msg, ...args] = messageQueue[0]; let listener = messageAwaiter.get(msg); - if (listener) { - messageQueue.shift(); - messageAwaiter.delete(msg); - - listener.resolve(...args); + if (!listener) { + break; } + + messageQueue.shift(); + messageAwaiter.delete(msg); + + listener.resolve(...args); } } From d9e4d372624ebabf019a9e1637237626cd67508e Mon Sep 17 00:00:00 2001 From: Dave Townsend Date: Tue, 17 Nov 2015 12:39:08 -0800 Subject: [PATCH 08/17] Bug 1224577: Fix removing frames from the map so chrome windows aren't leaked from every add-on. r=bz --HG-- extra : commitid : 5ULh3d5M5aV extra : rebase_source : 310922f621d4099711637ee94134435aa4769a22 --- addon-sdk/source/lib/sdk/remote/parent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon-sdk/source/lib/sdk/remote/parent.js b/addon-sdk/source/lib/sdk/remote/parent.js index fac6770967fc..063b138b3728 100644 --- a/addon-sdk/source/lib/sdk/remote/parent.js +++ b/addon-sdk/source/lib/sdk/remote/parent.js @@ -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 From 6bf3a72a8e0c91f9bb12160398c70c2e09e73a17 Mon Sep 17 00:00:00 2001 From: Allison Naaktgeboren Date: Tue, 17 Nov 2015 14:56:06 -0800 Subject: [PATCH 09/17] Bug 1224446 - Partner sms(to)/mms(to) links missing // lose phone number in Uri pruning.r=sebastian --- mobile/android/base/GeckoAppShell.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/mobile/android/base/GeckoAppShell.java b/mobile/android/base/GeckoAppShell.java index 8a3f50bf9e68..9b1901a3ca9d 100644 --- a/mobile/android/base/GeckoAppShell.java +++ b/mobile/android/base/GeckoAppShell.java @@ -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); @@ -1237,6 +1237,14 @@ public class GeckoAppShell return intent; } + // 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)); + } + final String[] fields = query.split("&"); boolean foundBody = false; String resultQuery = ""; From fc8d4c9e8d6f0d3fefc9060a28273b9e9ef8d711 Mon Sep 17 00:00:00 2001 From: Allison Naaktgeboren Date: Tue, 17 Nov 2015 15:09:11 -0800 Subject: [PATCH 10/17] Bug 1209133 - Add mms/mmsto: support.r=sebastian --- mobile/android/base/GeckoAppShell.java | 35 ++++++++++++++++---------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/mobile/android/base/GeckoAppShell.java b/mobile/android/base/GeckoAppShell.java index 9b1901a3ca9d..e218391f67dd 100644 --- a/mobile/android/base/GeckoAppShell.java +++ b/mobile/android/base/GeckoAppShell.java @@ -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; } @@ -1246,26 +1246,35 @@ public class GeckoAppShell } final String[] fields = query.split("&"); - boolean foundBody = false; + boolean shouldUpdateIntent = false; String resultQuery = ""; for (String field : fields) { - if (foundBody || !field.startsWith("body=")) { + 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); - 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; } - if (!foundBody) { + 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(); From 9b2c61cbae32c7261afc798a9d3687247eadc6d1 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Sat, 14 Nov 2015 23:30:52 -0800 Subject: [PATCH 11/17] Bug 1215893: [webext] Check capturing event listeners for the correct target. r=billm --HG-- extra : commitid : IYgT9fAiHJX extra : rebase_source : c672622b7f2e931ceab45b5569c9de0bfd40602a --- browser/components/extensions/ext-utils.js | 7 +- toolkit/components/extensions/Extension.jsm | 3 + .../extensions/ExtensionContent.jsm | 8 ++- .../extensions/ext-backgroundPage.js | 9 ++- .../extensions/test/mochitest/mochitest.ini | 1 + .../test_ext_background_sub_windows.html | 64 +++++++++++++++++++ 6 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_background_sub_windows.html diff --git a/browser/components/extensions/ext-utils.js b/browser/components/extensions/ext-utils.js index 31bedad73c73..d54918cdfccd 100644 --- a/browser/components/extensions/ext-utils.js +++ b/browser/components/extensions/ext-utils.js @@ -175,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; @@ -462,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; } diff --git a/toolkit/components/extensions/Extension.jsm b/toolkit/components/extensions/Extension.jsm index 21add17c536b..4338ccbb6787 100644 --- a/toolkit/components/extensions/Extension.jsm +++ b/toolkit/components/extensions/Extension.jsm @@ -322,6 +322,9 @@ var GlobalManager = { let eventHandler = docShell.chromeEventHandler; let listener = event => { + if (event.target != docShell.contentViewer.DOMDocument) { + return; + } eventHandler.removeEventListener("unload", listener); context.unload(); }; diff --git a/toolkit/components/extensions/ExtensionContent.jsm b/toolkit/components/extensions/ExtensionContent.jsm index 4c6200ded669..ba580292145f 100644 --- a/toolkit/components/extensions/ExtensionContent.jsm +++ b/toolkit/components/extensions/ExtensionContent.jsm @@ -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. diff --git a/toolkit/components/extensions/ext-backgroundPage.js b/toolkit/components/extensions/ext-backgroundPage.js index ef4aa0c52852..f0a38c6cebbe 100644 --- a/toolkit/components/extensions/ext-backgroundPage.js +++ b/toolkit/components/extensions/ext-backgroundPage.js @@ -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() { diff --git a/toolkit/components/extensions/test/mochitest/mochitest.ini b/toolkit/components/extensions/test/mochitest/mochitest.ini index 37a4958af467..f4530969f165 100644 --- a/toolkit/components/extensions/test/mochitest/mochitest.ini +++ b/toolkit/components/extensions/test/mochitest/mochitest.ini @@ -35,5 +35,6 @@ 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. diff --git a/toolkit/components/extensions/test/mochitest/test_ext_background_sub_windows.html b/toolkit/components/extensions/test/mochitest/test_ext_background_sub_windows.html new file mode 100644 index 000000000000..87d340717244 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_background_sub_windows.html @@ -0,0 +1,64 @@ + + + + Test for sub-frames of WebExtension background pages + + + + + + + + + + + + From 58510803fe3041a00e08a088a742707713811df0 Mon Sep 17 00:00:00 2001 From: Mark Hammond Date: Wed, 18 Nov 2015 12:44:37 +1100 Subject: [PATCH 12/17] Bug 1225194 - hook up remotetab action to tabs from other devices are actually opened. r=mak --- browser/base/content/test/general/browser.ini | 1 + .../general/browser_bug1225194-remotetab.js | 18 ++++++++++++++++++ browser/base/content/urlbarBindings.xml | 2 ++ 3 files changed, 21 insertions(+) create mode 100644 browser/base/content/test/general/browser_bug1225194-remotetab.js diff --git a/browser/base/content/test/general/browser.ini b/browser/base/content/test/general/browser.ini index 1e75dfe31c62..ab9f47f4d1c6 100644 --- a/browser/base/content/test/general/browser.ini +++ b/browser/base/content/test/general/browser.ini @@ -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] diff --git a/browser/base/content/test/general/browser_bug1225194-remotetab.js b/browser/base/content/test/general/browser_bug1225194-remotetab.js new file mode 100644 index 000000000000..587fb15bf6c2 --- /dev/null +++ b/browser/base/content/test/general/browser_bug1225194-remotetab.js @@ -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"); + }); +}); diff --git a/browser/base/content/urlbarBindings.xml b/browser/base/content/urlbarBindings.xml index eb6f3043d1b6..e34c83ea13bd 100644 --- a/browser/base/content/urlbarBindings.xml +++ b/browser/base/content/urlbarBindings.xml @@ -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") { From 2edde3bf986acc6334c20d617689342ea902bd15 Mon Sep 17 00:00:00 2001 From: Mark Hammond Date: Wed, 18 Nov 2015 12:49:54 +1100 Subject: [PATCH 13/17] Bug 1221906 - Allow Sync to only sync specified engines. r=rnewman --- browser/base/content/sync/aboutSyncTabs.js | 11 +- services/sync/modules/service.js | 8 +- services/sync/modules/stages/enginesync.js | 19 ++- .../tests/unit/test_service_sync_specified.js | 159 ++++++++++++++++++ services/sync/tests/unit/xpcshell.ini | 1 + 5 files changed, 183 insertions(+), 15 deletions(-) create mode 100644 services/sync/tests/unit/test_service_sync_specified.js diff --git a/browser/base/content/sync/aboutSyncTabs.js b/browser/base/content/sync/aboutSyncTabs.js index 35ea52296639..01057047f9b8 100644 --- a/browser/base/content/sync/aboutSyncTabs.js +++ b/browser/base/content/sync/aboutSyncTabs.js @@ -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)); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 5e78051cece3..9cd067fe27e1 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -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(); diff --git a/services/sync/modules/stages/enginesync.js b/services/sync/modules/stages/enginesync.js index a2fe31b0cef7..d4296b7f89d6 100644 --- a/services/sync/modules/stages/enginesync.js +++ b/services/sync/modules/stages/enginesync.js @@ -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); diff --git a/services/sync/tests/unit/test_service_sync_specified.js b/services/sync/tests/unit/test_service_sync_specified.js new file mode 100644 index 000000000000..bfd0c8bfc6ef --- /dev/null +++ b/services/sync/tests/unit/test_service_sync_specified.js @@ -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); + } +}); diff --git a/services/sync/tests/unit/xpcshell.ini b/services/sync/tests/unit/xpcshell.ini index 76dd945c0c6d..715e5f1de233 100644 --- a/services/sync/tests/unit/xpcshell.ini +++ b/services/sync/tests/unit/xpcshell.ini @@ -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" From efe250798d7d156baac47f58caf18573f1161610 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Sun, 15 Nov 2015 19:34:09 -0800 Subject: [PATCH 14/17] Bug 1218443: [webext] Fix some instances of window listeners not being added correctly. r=billm --HG-- extra : commitid : GkZzr4Rpp81 extra : rebase_source : 364da950cb8d50dcf990b529753d4e2cb3b0acf3 --- browser/components/extensions/ext-utils.js | 31 ++++++++--- .../browser/browser_ext_pageAction_context.js | 55 ++++++++++++++----- .../extensions/test/browser/head.js | 4 +- 3 files changed, 67 insertions(+), 23 deletions(-) diff --git a/browser/components/extensions/ext-utils.js b/browser/components/extensions/ext-utils.js index d54918cdfccd..27b735a19bdd 100644 --- a/browser/components/extensions/ext-utils.js +++ b/browser/components/extensions/ext-utils.js @@ -421,17 +421,32 @@ global.WindowListManager = { // Returns an iterator for all browser windows. Unless |includeIncomplete| is // true, only fully-loaded windows are returned. *browserWindows(includeIncomplete = false) { - let e = Services.wm.getEnumerator("navigator:browser"); + // The window type parameter is only available once the window's document + // element has been created. This means that, when looking for incomplete + // browser windows, we need to ignore the type entirely for windows which + // haven't finished loading, since we would otherwise skip browser windows + // in their early loading stages. + // This is particularly important given that the "domwindowcreated" event + // fires for browser windows when they're in that in-between state, and just + // before we register our own "domwindowcreated" listener. + + let e = Services.wm.getEnumerator(""); while (e.hasMoreElements()) { let window = e.getNext(); - if (includeIncomplete || window.document.readyState == "complete") { + + let ok = includeIncomplete; + if (window.document.readyState == "complete") { + ok = window.document.documentElement.getAttribute("windowtype") == "navigator:browser"; + } + + if (ok) { yield window; } } }, addOpenListener(listener) { - if (this._openListeners.length == 0 && this._closeListeners.length == 0) { + if (this._openListeners.size == 0 && this._closeListeners.size == 0) { Services.ww.registerNotification(this); } this._openListeners.add(listener); @@ -445,13 +460,13 @@ global.WindowListManager = { removeOpenListener(listener) { this._openListeners.delete(listener); - if (this._openListeners.length == 0 && this._closeListeners.length == 0) { + if (this._openListeners.size == 0 && this._closeListeners.size == 0) { Services.ww.unregisterNotification(this); } }, addCloseListener(listener) { - if (this._openListeners.length == 0 && this._closeListeners.length == 0) { + if (this._openListeners.size == 0 && this._closeListeners.size == 0) { Services.ww.registerNotification(this); } this._closeListeners.add(listener); @@ -459,7 +474,7 @@ global.WindowListManager = { removeCloseListener(listener) { this._closeListeners.delete(listener); - if (this._openListeners.length == 0 && this._closeListeners.length == 0) { + if (this._openListeners.size == 0 && this._closeListeners.size == 0) { Services.ww.unregisterNotification(this); } }, @@ -476,8 +491,6 @@ global.WindowListManager = { } }, - queryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]), - observe(window, topic, data) { if (topic == "domwindowclosed") { if (window.document.documentElement.getAttribute("windowtype") != "navigator:browser") { @@ -568,6 +581,8 @@ global.AllWindowEvents = { }, }; +AllWindowEvents.openListener = AllWindowEvents.openListener.bind(AllWindowEvents); + // Subclass of EventManager where we just need to call // add/removeEventListener on each XUL window. global.WindowEventManager = function(context, name, event, listener) diff --git a/browser/components/extensions/test/browser/browser_ext_pageAction_context.js b/browser/components/extensions/test/browser/browser_ext_pageAction_context.js index 7a495ecdad36..08a8521a84bf 100644 --- a/browser/components/extensions/test/browser/browser_ext_pageAction_context.js +++ b/browser/components/extensions/test/browser/browser_ext_pageAction_context.js @@ -27,9 +27,9 @@ add_task(function* testTabSwitchContext() { "title": "Title 2" }, ]; - var tabs = []; - - var tests = [ + var tabs; + var tests; + var allTests = [ expect => { browser.test.log("Initial state. No icon visible."); expect(null); @@ -113,7 +113,6 @@ add_task(function* testTabSwitchContext() { return browser.tabs.query({ active: true, currentWindow: true }, resolve); }).then(tabs => { var tabId = tabs[0].id; - return Promise.all([ new Promise(resolve => browser.pageAction.getTitle({tabId}, resolve)), new Promise(resolve => browser.pageAction.getPopup({tabId}, resolve))]) @@ -155,25 +154,36 @@ add_task(function* testTabSwitchContext() { } browser.test.onMessage.addListener((msg) => { - if (msg != "runNextTest") { - browser.test.fail("Expecting 'runNextTest' message"); + if (msg == "runTests") { + runTests(); + } else if (msg == "runNextTest") { + nextTest(); + } else { + browser.test.fail(`Unexpected message: ${msg}`); } - - nextTest(); }); - browser.tabs.query({ active: true, currentWindow: true }, resultTabs => { - tabs[0] = resultTabs[0].id; + function runTests() { + tabs = []; + tests = allTests.slice(); - nextTest(); - }); + browser.tabs.query({ active: true, currentWindow: true }, resultTabs => { + tabs[0] = resultTabs[0].id; + + nextTest(); + }); + } + + runTests(); }, }); let pageActionId = makeWidgetId(extension.id) + "-page-action"; + let currentWindow = window; + let windows = []; function checkDetails(details) { - let image = document.getElementById(pageActionId); + let image = currentWindow.document.getElementById(pageActionId); if (details == null) { ok(image == null || image.hidden, "image is hidden"); } else { @@ -186,12 +196,24 @@ add_task(function* testTabSwitchContext() { } } + let testNewWindows = 1; + let awaitFinish = new Promise(resolve => { extension.onMessage("nextTest", (expecting, testsRemaining) => { checkDetails(expecting); if (testsRemaining) { extension.sendMessage("runNextTest") + } else if (testNewWindows) { + testNewWindows--; + + BrowserTestUtils.openNewBrowserWindow().then(window => { + windows.push(window); + currentWindow = window; + return focusWindow(window); + }).then(() => { + extension.sendMessage("runTests") + }); } else { resolve(); } @@ -206,4 +228,11 @@ add_task(function* testTabSwitchContext() { let node = document.getElementById(pageActionId); is(node, undefined, "pageAction image removed from document"); + + for (let win of windows) { + node = win.document.getElementById(pageActionId); + is(node, undefined, "pageAction image removed from second document"); + + yield BrowserTestUtils.closeWindow(win); + } }); diff --git a/browser/components/extensions/test/browser/head.js b/browser/components/extensions/test/browser/head.js index cd47bcd7b7d4..bc3799b3d12b 100644 --- a/browser/components/extensions/test/browser/head.js +++ b/browser/components/extensions/test/browser/head.js @@ -6,7 +6,7 @@ function makeWidgetId(id) return id.replace(/[^a-z0-9_-]/g, "_"); } -function* focusWindow(win) +var focusWindow = Task.async(function* focusWindow(win) { let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager); if (fm.activeWindow == win) { @@ -22,4 +22,4 @@ function* focusWindow(win) win.focus(); yield promise; -} +}); From 76d1ae510c251a06a51a0d91c9a2630cf120a272 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Sun, 15 Nov 2015 16:54:41 -0800 Subject: [PATCH 15/17] Bug 1221415: [webext] Improve error checking and Chrome-compatibility of i18n API. r=billm --HG-- extra : commitid : 493PXRrLfdL extra : rebase_source : 81a053d889e64e8f5685fe7f7a2a656c396807a0 --- toolkit/components/extensions/Extension.jsm | 149 +++++++++++++----- .../components/extensions/ExtensionUtils.jsm | 7 + .../extensions/test/mochitest/mochitest.ini | 1 + .../test/mochitest/test_ext_i18n.html | 121 ++++++++++++++ 4 files changed, 235 insertions(+), 43 deletions(-) create mode 100644 toolkit/components/extensions/test/mochitest/test_ext_i18n.html diff --git a/toolkit/components/extensions/Extension.jsm b/toolkit/components/extensions/Extension.jsm index 4338ccbb6787..1eee82344917 100644 --- a/toolkit/components/extensions/Extension.jsm +++ b/toolkit/components/extensions/Extension.jsm @@ -66,6 +66,7 @@ var { injectAPI, extend, flushJarCache, + instanceOf, } = ExtensionUtils; const LOGGER_ID_BASE = "addons.webextension."; @@ -346,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; @@ -375,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. @@ -428,7 +424,7 @@ ExtensionData.prototype = { } Cu.reportError(`Unknown localization message ${message}`); - return "??"; + return defaultValue; }, // Localize a string, replacing all |__MSG_(.*)__| tokens with the @@ -443,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); }); }, @@ -568,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) { @@ -575,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}`); } @@ -641,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]; }), }; @@ -780,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(); @@ -810,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); @@ -943,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] }; }); @@ -974,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; }); diff --git a/toolkit/components/extensions/ExtensionUtils.jsm b/toolkit/components/extensions/ExtensionUtils.jsm index f4393302ff1e..78548c95c553 100644 --- a/toolkit/components/extensions/ExtensionUtils.jsm +++ b/toolkit/components/extensions/ExtensionUtils.jsm @@ -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, }; diff --git a/toolkit/components/extensions/test/mochitest/mochitest.ini b/toolkit/components/extensions/test/mochitest/mochitest.ini index f4530969f165..03fae5b7db14 100644 --- a/toolkit/components/extensions/test/mochitest/mochitest.ini +++ b/toolkit/components/extensions/test/mochitest/mochitest.ini @@ -38,3 +38,4 @@ support-files = [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] diff --git a/toolkit/components/extensions/test/mochitest/test_ext_i18n.html b/toolkit/components/extensions/test/mochitest/test_ext_i18n.html new file mode 100644 index 000000000000..24c1898da4bb --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_i18n.html @@ -0,0 +1,121 @@ + + + + Test for WebExtension localization APIs + + + + + + + + + + + + From bef4c1c018bf060fb4d9ab03baacc82c28883111 Mon Sep 17 00:00:00 2001 From: Phil Ringnalda Date: Tue, 17 Nov 2015 21:00:35 -0800 Subject: [PATCH 16/17] Bug 1166297 - As a last resort, use requestLongerTimeout() in browser_tabMatchesInAwesomebar.js --HG-- extra : rebase_source : 6f12455aa500b6f75b01938c547e454ce037e20f --- .../base/content/test/general/browser_tabMatchesInAwesomebar.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/browser/base/content/test/general/browser_tabMatchesInAwesomebar.js b/browser/base/content/test/general/browser_tabMatchesInAwesomebar.js index 70afadd0c327..209f0a9d176e 100644 --- a/browser/base/content/test/general/browser_tabMatchesInAwesomebar.js +++ b/browser/base/content/test/general/browser_tabMatchesInAwesomebar.js @@ -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" From b3918f8ac1c81cd7b90f823a2fbdb0e5e4f4dbbc Mon Sep 17 00:00:00 2001 From: Phil Ringnalda Date: Tue, 17 Nov 2015 22:38:20 -0800 Subject: [PATCH 17/17] Back out 6fa3b2df62cc (bug 1218443) for browser_ext_pageAction_context.js leaks CLOSED TREE --- browser/components/extensions/ext-utils.js | 31 +++-------- .../browser/browser_ext_pageAction_context.js | 55 +++++-------------- .../extensions/test/browser/head.js | 4 +- 3 files changed, 23 insertions(+), 67 deletions(-) diff --git a/browser/components/extensions/ext-utils.js b/browser/components/extensions/ext-utils.js index 27b735a19bdd..d54918cdfccd 100644 --- a/browser/components/extensions/ext-utils.js +++ b/browser/components/extensions/ext-utils.js @@ -421,32 +421,17 @@ global.WindowListManager = { // Returns an iterator for all browser windows. Unless |includeIncomplete| is // true, only fully-loaded windows are returned. *browserWindows(includeIncomplete = false) { - // The window type parameter is only available once the window's document - // element has been created. This means that, when looking for incomplete - // browser windows, we need to ignore the type entirely for windows which - // haven't finished loading, since we would otherwise skip browser windows - // in their early loading stages. - // This is particularly important given that the "domwindowcreated" event - // fires for browser windows when they're in that in-between state, and just - // before we register our own "domwindowcreated" listener. - - let e = Services.wm.getEnumerator(""); + let e = Services.wm.getEnumerator("navigator:browser"); while (e.hasMoreElements()) { let window = e.getNext(); - - let ok = includeIncomplete; - if (window.document.readyState == "complete") { - ok = window.document.documentElement.getAttribute("windowtype") == "navigator:browser"; - } - - if (ok) { + if (includeIncomplete || window.document.readyState == "complete") { yield window; } } }, addOpenListener(listener) { - if (this._openListeners.size == 0 && this._closeListeners.size == 0) { + if (this._openListeners.length == 0 && this._closeListeners.length == 0) { Services.ww.registerNotification(this); } this._openListeners.add(listener); @@ -460,13 +445,13 @@ global.WindowListManager = { removeOpenListener(listener) { this._openListeners.delete(listener); - if (this._openListeners.size == 0 && this._closeListeners.size == 0) { + if (this._openListeners.length == 0 && this._closeListeners.length == 0) { Services.ww.unregisterNotification(this); } }, addCloseListener(listener) { - if (this._openListeners.size == 0 && this._closeListeners.size == 0) { + if (this._openListeners.length == 0 && this._closeListeners.length == 0) { Services.ww.registerNotification(this); } this._closeListeners.add(listener); @@ -474,7 +459,7 @@ global.WindowListManager = { removeCloseListener(listener) { this._closeListeners.delete(listener); - if (this._openListeners.size == 0 && this._closeListeners.size == 0) { + if (this._openListeners.length == 0 && this._closeListeners.length == 0) { Services.ww.unregisterNotification(this); } }, @@ -491,6 +476,8 @@ global.WindowListManager = { } }, + queryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]), + observe(window, topic, data) { if (topic == "domwindowclosed") { if (window.document.documentElement.getAttribute("windowtype") != "navigator:browser") { @@ -581,8 +568,6 @@ global.AllWindowEvents = { }, }; -AllWindowEvents.openListener = AllWindowEvents.openListener.bind(AllWindowEvents); - // Subclass of EventManager where we just need to call // add/removeEventListener on each XUL window. global.WindowEventManager = function(context, name, event, listener) diff --git a/browser/components/extensions/test/browser/browser_ext_pageAction_context.js b/browser/components/extensions/test/browser/browser_ext_pageAction_context.js index 08a8521a84bf..7a495ecdad36 100644 --- a/browser/components/extensions/test/browser/browser_ext_pageAction_context.js +++ b/browser/components/extensions/test/browser/browser_ext_pageAction_context.js @@ -27,9 +27,9 @@ add_task(function* testTabSwitchContext() { "title": "Title 2" }, ]; - var tabs; - var tests; - var allTests = [ + var tabs = []; + + var tests = [ expect => { browser.test.log("Initial state. No icon visible."); expect(null); @@ -113,6 +113,7 @@ add_task(function* testTabSwitchContext() { return browser.tabs.query({ active: true, currentWindow: true }, resolve); }).then(tabs => { var tabId = tabs[0].id; + return Promise.all([ new Promise(resolve => browser.pageAction.getTitle({tabId}, resolve)), new Promise(resolve => browser.pageAction.getPopup({tabId}, resolve))]) @@ -154,36 +155,25 @@ add_task(function* testTabSwitchContext() { } browser.test.onMessage.addListener((msg) => { - if (msg == "runTests") { - runTests(); - } else if (msg == "runNextTest") { - nextTest(); - } else { - browser.test.fail(`Unexpected message: ${msg}`); + if (msg != "runNextTest") { + browser.test.fail("Expecting 'runNextTest' message"); } + + nextTest(); }); - function runTests() { - tabs = []; - tests = allTests.slice(); + browser.tabs.query({ active: true, currentWindow: true }, resultTabs => { + tabs[0] = resultTabs[0].id; - browser.tabs.query({ active: true, currentWindow: true }, resultTabs => { - tabs[0] = resultTabs[0].id; - - nextTest(); - }); - } - - runTests(); + nextTest(); + }); }, }); let pageActionId = makeWidgetId(extension.id) + "-page-action"; - let currentWindow = window; - let windows = []; function checkDetails(details) { - let image = currentWindow.document.getElementById(pageActionId); + let image = document.getElementById(pageActionId); if (details == null) { ok(image == null || image.hidden, "image is hidden"); } else { @@ -196,24 +186,12 @@ add_task(function* testTabSwitchContext() { } } - let testNewWindows = 1; - let awaitFinish = new Promise(resolve => { extension.onMessage("nextTest", (expecting, testsRemaining) => { checkDetails(expecting); if (testsRemaining) { extension.sendMessage("runNextTest") - } else if (testNewWindows) { - testNewWindows--; - - BrowserTestUtils.openNewBrowserWindow().then(window => { - windows.push(window); - currentWindow = window; - return focusWindow(window); - }).then(() => { - extension.sendMessage("runTests") - }); } else { resolve(); } @@ -228,11 +206,4 @@ add_task(function* testTabSwitchContext() { let node = document.getElementById(pageActionId); is(node, undefined, "pageAction image removed from document"); - - for (let win of windows) { - node = win.document.getElementById(pageActionId); - is(node, undefined, "pageAction image removed from second document"); - - yield BrowserTestUtils.closeWindow(win); - } }); diff --git a/browser/components/extensions/test/browser/head.js b/browser/components/extensions/test/browser/head.js index bc3799b3d12b..cd47bcd7b7d4 100644 --- a/browser/components/extensions/test/browser/head.js +++ b/browser/components/extensions/test/browser/head.js @@ -6,7 +6,7 @@ function makeWidgetId(id) return id.replace(/[^a-z0-9_-]/g, "_"); } -var focusWindow = Task.async(function* focusWindow(win) +function* focusWindow(win) { let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager); if (fm.activeWindow == win) { @@ -22,4 +22,4 @@ var focusWindow = Task.async(function* focusWindow(win) win.focus(); yield promise; -}); +}