From d39fe7ffd8da24ff11c958066f8abb12758d4568 Mon Sep 17 00:00:00 2001 From: Vlad Filippov Date: Fri, 1 Aug 2014 01:42:00 -0400 Subject: [PATCH 01/18] Bug 1022064 - Add WebChannel Communication API and FxAccountsOAuthClient API to facilitate Firefox Accounts OAuth authentication. r=MattN, sr=gavin --- browser/base/content/content.js | 27 ++ browser/base/content/test/general/browser.ini | 4 + .../test/general/browser_fxa_oauth.html | 28 ++ .../content/test/general/browser_fxa_oauth.js | 75 ++++++ .../test/general/browser_web_channel.html | 89 +++++++ .../test/general/browser_web_channel.js | 91 +++++++ services/fxaccounts/FxAccountsOAuthClient.jsm | 204 ++++++++++++++ services/fxaccounts/moz.build | 3 +- .../tests/xpcshell/test_oauth_client.js | 46 ++++ .../fxaccounts/tests/xpcshell/xpcshell.ini | 1 + toolkit/modules/WebChannel.jsm | 249 ++++++++++++++++++ toolkit/modules/moz.build | 1 + .../tests/xpcshell/test_web_channel.js | 104 ++++++++ .../tests/xpcshell/test_web_channel_broker.js | 85 ++++++ toolkit/modules/tests/xpcshell/xpcshell.ini | 2 + 15 files changed, 1008 insertions(+), 1 deletion(-) create mode 100644 browser/base/content/test/general/browser_fxa_oauth.html create mode 100644 browser/base/content/test/general/browser_fxa_oauth.js create mode 100644 browser/base/content/test/general/browser_web_channel.html create mode 100644 browser/base/content/test/general/browser_web_channel.js create mode 100644 services/fxaccounts/FxAccountsOAuthClient.jsm create mode 100644 services/fxaccounts/tests/xpcshell/test_oauth_client.js create mode 100644 toolkit/modules/WebChannel.jsm create mode 100644 toolkit/modules/tests/xpcshell/test_web_channel.js create mode 100644 toolkit/modules/tests/xpcshell/test_web_channel_broker.js diff --git a/browser/base/content/content.js b/browser/base/content/content.js index 9b0a41050011..3568b42bb9ac 100644 --- a/browser/base/content/content.js +++ b/browser/base/content/content.js @@ -216,6 +216,33 @@ let AboutHomeListener = { AboutHomeListener.init(this); +// An event listener for custom "WebChannelMessageToChrome" events on pages +addEventListener("WebChannelMessageToChrome", function (e) { + // if target is window then we want the document principal, otherwise fallback to target itself. + let principal = e.target.nodePrincipal ? e.target.nodePrincipal : e.target.document.nodePrincipal; + + if (e.detail) { + sendAsyncMessage("WebChannelMessageToChrome", e.detail, null, principal); + } else { + Cu.reportError("WebChannel message failed. No message detail."); + } +}, true, true); + +// Add message listener for "WebChannelMessageToContent" messages from chrome scripts +addMessageListener("WebChannelMessageToContent", function (e) { + if (e.data) { + content.dispatchEvent(new content.CustomEvent("WebChannelMessageToContent", { + detail: Cu.cloneInto({ + id: e.data.id, + message: e.data.message, + }, content), + })); + } else { + Cu.reportError("WebChannel message failed. No message data."); + } +}); + + let ContentSearchMediator = { whitelist: new Set([ diff --git a/browser/base/content/test/general/browser.ini b/browser/base/content/test/general/browser.ini index 588a30bb5f5e..03579e183f78 100644 --- a/browser/base/content/test/general/browser.ini +++ b/browser/base/content/test/general/browser.ini @@ -11,9 +11,11 @@ support-files = browser_bug678392-1.html browser_bug678392-2.html browser_bug970746.xhtml + browser_fxa_oauth.html browser_registerProtocolHandler_notification.html browser_star_hsts.sjs browser_tab_dragdrop2_frame1.xul + browser_web_channel.html bug564387.html bug564387_video1.ogv bug564387_video1.ogv^headers^ @@ -295,6 +297,7 @@ skip-if = true # browser_drag.js is disabled, as it needs to be updated for the [browser_findbarClose.js] skip-if = e10s # Bug ?????? - test directly manipulates content (tries to grab an iframe directly from content) [browser_fullscreen-window-open.js] +[browser_fxa_oauth.js] skip-if = buildapp == 'mulet' || e10s || os == "linux" # Bug 933103 - mochitest's EventUtils.synthesizeMouse functions not e10s friendly. Linux: Intermittent failures - bug 941575. [browser_gestureSupport.js] skip-if = e10s # Bug 863514 - no gesture support. @@ -428,6 +431,7 @@ skip-if = e10s # Bug 921905 - pinTab/unpinTab fail in e10s [browser_visibleTabs_contextMenu.js] skip-if = e10s # Bug 921905 - pinTab/unpinTab fail in e10s [browser_visibleTabs_tabPreview.js] +[browser_web_channel.js] skip-if = (os == "win" && !debug) || e10s # Bug 1007418 / Bug 698371 - thumbnail captures need e10s love (tabPreviews_capture fails with Argument 1 of CanvasRenderingContext2D.drawWindow does not implement interface Window.) [browser_windowopen_reflows.js] skip-if = buildapp == 'mulet' diff --git a/browser/base/content/test/general/browser_fxa_oauth.html b/browser/base/content/test/general/browser_fxa_oauth.html new file mode 100644 index 000000000000..e34276759bb0 --- /dev/null +++ b/browser/base/content/test/general/browser_fxa_oauth.html @@ -0,0 +1,28 @@ + + + + + fxa_oauth_test + + + + + diff --git a/browser/base/content/test/general/browser_fxa_oauth.js b/browser/base/content/test/general/browser_fxa_oauth.js new file mode 100644 index 000000000000..34f7f768ccc5 --- /dev/null +++ b/browser/base/content/test/general/browser_fxa_oauth.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +Cu.import("resource://gre/modules/Promise.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsOAuthClient", + "resource://gre/modules/FxAccountsOAuthClient.jsm"); + +const HTTP_PATH = "http://example.com"; +const HTTP_ENDPOINT = "/browser/browser/base/content/test/general/browser_fxa_oauth.html"; + +let gTests = [ + { + desc: "FxA OAuth - should open a new tab, complete OAuth flow", + run: function* () { + return new Promise(function(resolve, reject) { + let tabOpened = false; + let properUrl = "http://example.com/browser/browser/base/content/test/general/browser_fxa_oauth.html?" + + "webChannelId=oauth_client_id&scope=&client_id=client_id&action=signin&state=state"; + waitForTab(function (tab) { + Assert.ok("Tab successfully opened"); + Assert.equal(gBrowser.currentURI.spec, properUrl); + tabOpened = true; + }); + + let client = new FxAccountsOAuthClient({ + parameters: { + state: "state", + client_id: "client_id", + oauth_uri: HTTP_PATH, + content_uri: HTTP_PATH, + }, + authorizationEndpoint: HTTP_ENDPOINT + }); + + client.onComplete = function(tokenData) { + Assert.ok(tabOpened); + Assert.equal(tokenData.code, "code1"); + Assert.equal(tokenData.state, "state"); + resolve(); + }; + + client.launchWebFlow(); + }); + } + } +]; // gTests + +function waitForTab(aCallback) { + let container = gBrowser.tabContainer; + container.addEventListener("TabOpen", function tabOpener(event) { + container.removeEventListener("TabOpen", tabOpener, false); + gBrowser.addEventListener("load", function listener() { + gBrowser.removeEventListener("load", listener, true); + let tab = event.target; + aCallback(tab); + }, true); + }, false); +} + +function test() { + waitForExplicitFinish(); + + Task.spawn(function () { + for (let test of gTests) { + info("Running: " + test.desc); + yield test.run(); + } + }).then(finish, ex => { + Assert.ok(false, "Unexpected Exception: " + ex); + finish(); + }); +} diff --git a/browser/base/content/test/general/browser_web_channel.html b/browser/base/content/test/general/browser_web_channel.html new file mode 100644 index 000000000000..3be3876c1954 --- /dev/null +++ b/browser/base/content/test/general/browser_web_channel.html @@ -0,0 +1,89 @@ + + + + + web_channel_test + + + + + diff --git a/browser/base/content/test/general/browser_web_channel.js b/browser/base/content/test/general/browser_web_channel.js new file mode 100644 index 000000000000..0e4d8605085e --- /dev/null +++ b/browser/base/content/test/general/browser_web_channel.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +Cu.import("resource://gre/modules/Promise.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "WebChannel", + "resource://gre/modules/WebChannel.jsm"); + +const HTTP_PATH = "http://example.com"; +const HTTP_ENDPOINT = "/browser/browser/base/content/test/general/browser_web_channel.html"; + +let gTests = [ + { + desc: "WebChannel generic message", + run: function* () { + return new Promise(function(resolve, reject) { + let tab; + let channel = new WebChannel("generic", Services.io.newURI(HTTP_PATH, null, null)); + channel.listen(function (id, message, target) { + is(id, "generic"); + is(message.something.nested, "hello"); + channel.stopListening(); + gBrowser.removeTab(tab); + resolve(); + }); + + tab = gBrowser.addTab(HTTP_PATH + HTTP_ENDPOINT + "?generic"); + }); + } + }, + { + desc: "WebChannel two way communication", + run: function* () { + return new Promise(function(resolve, reject) { + let tab; + let channel = new WebChannel("twoway", Services.io.newURI(HTTP_PATH, null, null)); + + channel.listen(function (id, message, sender) { + is(id, "twoway"); + ok(message.command); + + if (message.command === "one") { + channel.send({ data: { nested: true } }, sender); + } + + if (message.command === "two") { + is(message.detail.data.nested, true); + channel.stopListening(); + gBrowser.removeTab(tab); + resolve(); + } + }); + + tab = gBrowser.addTab(HTTP_PATH + HTTP_ENDPOINT + "?twoway"); + }); + } + }, + { + desc: "WebChannel multichannel", + run: function* () { + return new Promise(function(resolve, reject) { + let tab; + let channel = new WebChannel("multichannel", Services.io.newURI(HTTP_PATH, null, null)); + + channel.listen(function (id, message, sender) { + is(id, "multichannel"); + gBrowser.removeTab(tab); + resolve(); + }); + + tab = gBrowser.addTab(HTTP_PATH + HTTP_ENDPOINT + "?multichannel"); + }); + } + } +]; // gTests + +function test() { + waitForExplicitFinish(); + + Task.spawn(function () { + for (let test of gTests) { + info("Running: " + test.desc); + yield test.run(); + } + }).then(finish, ex => { + ok(false, "Unexpected Exception: " + ex); + finish(); + }); +} diff --git a/services/fxaccounts/FxAccountsOAuthClient.jsm b/services/fxaccounts/FxAccountsOAuthClient.jsm new file mode 100644 index 000000000000..f6d03e27e88a --- /dev/null +++ b/services/fxaccounts/FxAccountsOAuthClient.jsm @@ -0,0 +1,204 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Firefox Accounts OAuth browser login helper. + * Uses the WebChannel component to receive OAuth messages and complete login flows. + */ + +this.EXPORTED_SYMBOLS = ["FxAccountsOAuthClient"]; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/Log.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/FxAccountsCommon.js"); +XPCOMUtils.defineLazyModuleGetter(this, "WebChannel", + "resource://gre/modules/WebChannel.jsm"); +Cu.importGlobalProperties(["URL"]); + +/** + * Create a new FxAccountsOAuthClient for browser some service. + * + * @param {Object} options Options + * @param {Object} options.parameters + * Opaque alphanumeric token to be included in verification links + * @param {String} options.parameters.client_id + * OAuth id returned from client registration + * @param {String} options.parameters.state + * A value that will be returned to the client as-is upon redirection + * @param {String} options.parameters.oauth_uri + * The FxA OAuth server uri + * @param {String} options.parameters.content_uri + * The FxA Content server uri + * @param {String} [options.parameters.scope] + * Optional. A colon-separated list of scopes that the user has authorized + * @param {String} [options.parameters.action] + * Optional. If provided, should be either signup or signin. + * @param [authorizationEndpoint] {String} + * Optional authorization endpoint for the OAuth server + * @constructor + */ +this.FxAccountsOAuthClient = function(options) { + this._validateOptions(options); + this.parameters = options.parameters; + this._configureChannel(); + + let authorizationEndpoint = options.authorizationEndpoint || "/authorization"; + + try { + this._fxaOAuthStartUrl = new URL(this.parameters.oauth_uri + authorizationEndpoint + "?"); + } catch (e) { + throw new Error("Invalid OAuth Url"); + } + + let params = this._fxaOAuthStartUrl.searchParams; + params.append("client_id", this.parameters.client_id); + params.append("state", this.parameters.state); + params.append("scope", this.parameters.scope || ""); + params.append("action", this.parameters.action || "signin"); + params.append("webChannelId", this._webChannelId); + +}; + +this.FxAccountsOAuthClient.prototype = { + /** + * Function that gets called once the OAuth flow is successfully complete. + */ + onComplete: null, + /** + * Configuration object that stores all OAuth parameters. + */ + parameters: null, + /** + * WebChannel that is used to communicate with content page. + */ + _channel: null, + /** + * Boolean to indicate if this client has completed an OAuth flow. + */ + _complete: false, + /** + * The url that opens the Firefox Accounts OAuth flow. + */ + _fxaOAuthStartUrl: null, + /** + * WebChannel id. + */ + _webChannelId: null, + /** + * WebChannel origin, used to validate origin of messages. + */ + _webChannelOrigin: null, + /** + * Opens a tab at "this._fxaOAuthStartUrl". + * Registers a WebChannel listener and sets up a callback if needed. + */ + launchWebFlow: function () { + if (!this._channelCallback) { + this._registerChannel(); + } + + if (this._complete) { + throw new Error("This client already completed the OAuth flow"); + } else { + let opener = Services.wm.getMostRecentWindow("navigator:browser").gBrowser; + opener.selectedTab = opener.addTab(this._fxaOAuthStartUrl.href); + } + }, + + /** + * Release all resources that are in use. + */ + tearDown: function() { + this.onComplete = null; + this._complete = true; + this._channel.stopListening(); + }, + + /** + * Configures WebChannel id and origin + * + * @private + */ + _configureChannel: function() { + this._webChannelId = "oauth_" + this.parameters.client_id; + + // if this.parameters.content_uri is present but not a valid URI, then this will throw an error. + try { + this._webChannelOrigin = Services.io.newURI(this.parameters.content_uri, null, null); + } catch (e) { + throw e; + } + }, + + /** + * Create a new channel with the WebChannelBroker, setup a callback listener + * @private + */ + _registerChannel: function() { + /** + * Processes messages that are called back from the FxAccountsChannel + * + * @param webChannelId {String} + * Command webChannelId + * @param message {Object} + * Command message + * @param target {EventTarget} + * Channel message event target + * @private + */ + let listener = function (webChannelId, message, target) { + if (message) { + let command = message.command; + let data = message.data; + + switch (command) { + case "oauth_complete": + // validate the state parameter and call onComplete + if (this.onComplete && data.code && this.parameters.state === data.state) { + log.debug("OAuth flow completed."); + this.onComplete({ + code: data.code, + state: data.state + }); + // onComplete will be called for this client only once + // calling onComplete again will result in a failure of the OAuth flow + this.tearDown(); + } + + // if the message asked to close the tab + if (data.closeWindow && target && target.contentWindow) { + target.contentWindow.close(); + } + break; + } + } + }; + + this._channelCallback = listener.bind(this); + this._channel = new WebChannel(this._webChannelId, this._webChannelOrigin); + this._channel.listen(this._channelCallback); + log.debug("Channel registered: " + this._webChannelId + " with origin " + this._webChannelOrigin.prePath); + }, + + /** + * Validates the required FxA OAuth parameters + * + * @param options {Object} + * OAuth client options + * @private + */ + _validateOptions: function (options) { + if (!options || !options.parameters) { + throw new Error("Missing 'parameters' configuration option"); + } + + ["oauth_uri", "client_id", "content_uri", "state"].forEach(option => { + if (!options.parameters[option]) { + throw new Error("Missing 'parameters." + option + "' parameter"); + } + }); + }, +}; diff --git a/services/fxaccounts/moz.build b/services/fxaccounts/moz.build index 141d23866da7..3d2e212613d4 100644 --- a/services/fxaccounts/moz.build +++ b/services/fxaccounts/moz.build @@ -12,7 +12,8 @@ EXTRA_JS_MODULES += [ 'Credentials.jsm', 'FxAccounts.jsm', 'FxAccountsClient.jsm', - 'FxAccountsCommon.js' + 'FxAccountsCommon.js', + 'FxAccountsOAuthClient.jsm', ] # For now, we will only be using the FxA manager in B2G. diff --git a/services/fxaccounts/tests/xpcshell/test_oauth_client.js b/services/fxaccounts/tests/xpcshell/test_oauth_client.js new file mode 100644 index 000000000000..74c7af28bb6b --- /dev/null +++ b/services/fxaccounts/tests/xpcshell/test_oauth_client.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Cu.import("resource://gre/modules/FxAccountsOAuthClient.jsm"); + +function run_test() { + validationHelper(undefined, + "Error: Missing 'parameters' configuration option"); + + validationHelper({}, + "Error: Missing 'parameters' configuration option"); + + validationHelper({ parameters: {} }, + "Error: Missing 'parameters.oauth_uri' parameter"); + + validationHelper({ parameters: { + oauth_uri: "http://oauth.test/v1" + }}, + "Error: Missing 'parameters.client_id' parameter"); + + validationHelper({ parameters: { + oauth_uri: "http://oauth.test/v1", + client_id: "client_id" + }}, + "Error: Missing 'parameters.content_uri' parameter"); + + validationHelper({ parameters: { + oauth_uri: "http://oauth.test/v1", + client_id: "client_id", + content_uri: "http://content.test" + }}, + "Error: Missing 'parameters.state' parameter"); + + run_next_test(); +} + +function validationHelper(params, expected) { + try { + new FxAccountsOAuthClient(params); + } catch (e) { + return do_check_eq(e.toString(), expected); + } + throw new Error("Validation helper error"); +} diff --git a/services/fxaccounts/tests/xpcshell/xpcshell.ini b/services/fxaccounts/tests/xpcshell/xpcshell.ini index 9d9bffe12544..637ee551e228 100644 --- a/services/fxaccounts/tests/xpcshell/xpcshell.ini +++ b/services/fxaccounts/tests/xpcshell/xpcshell.ini @@ -8,3 +8,4 @@ tail = [test_manager.js] run-if = appname == 'b2g' reason = FxAccountsManager is only available for B2G for now +[test_oauth_client.js] diff --git a/toolkit/modules/WebChannel.jsm b/toolkit/modules/WebChannel.jsm new file mode 100644 index 000000000000..979cd3373819 --- /dev/null +++ b/toolkit/modules/WebChannel.jsm @@ -0,0 +1,249 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * WebChannel is an abstraction that uses the Message Manager and Custom Events + * to create a two-way communication channel between chrome and content code. + */ + +this.EXPORTED_SYMBOLS = ["WebChannel", "WebChannelBroker"]; + +const ERRNO_UNKNOWN_ERROR = 999; +const ERROR_UNKNOWN = "UNKNOWN_ERROR"; + + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + + +/** + * WebChannelBroker is a global object that helps manage WebChannel objects. + * This object handles channel registration, origin validation and message multiplexing. + */ + +let WebChannelBroker = Object.create({ + /** + * Register a new channel that callbacks messages + * based on proper origin and channel name + * + * @param channel {WebChannel} + */ + registerChannel: function (channel) { + if (!this._channelMap.has(channel)) { + this._channelMap.set(channel); + } else { + Cu.reportError("Failed to register the channel. Channel already exists."); + } + + // attach the global message listener if needed + if (!this._messageListenerAttached) { + this._messageListenerAttached = true; + this._manager.addMessageListener("WebChannelMessageToChrome", this._listener.bind(this)); + } + }, + + /** + * Unregister a channel + * + * @param channelToRemove {WebChannel} + * WebChannel to remove from the channel map + * + * Removes the specified channel from the channel map + */ + unregisterChannel: function (channelToRemove) { + if (!this._channelMap.delete(channelToRemove)) { + Cu.reportError("Failed to unregister the channel. Channel not found."); + } + }, + + /** + * @param event {Event} + * Message Manager event + * @private + */ + _listener: function (event) { + let data = event.data; + let sender = event.target; + + if (data && data.id) { + if (!event.principal) { + this._sendErrorEventToContent(data.id, sender, "Message principal missing"); + } else { + let validChannelFound = false; + data.message = data.message || {}; + + for (var channel of this._channelMap.keys()) { + if (channel.id === data.id && + channel.origin.prePath === event.principal.origin) { + validChannelFound = true; + channel.deliver(data, sender); + } + } + + // if no valid origins send an event that there is no such valid channel + if (!validChannelFound) { + this._sendErrorEventToContent(data.id, sender, "No Such Channel"); + } + } + } else { + Cu.reportError("WebChannel channel id missing"); + } + }, + /** + * The global message manager operates on every + */ + _manager: Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager), + /** + * Boolean used to detect if the global message manager event is already attached + */ + _messageListenerAttached: false, + /** + * Object to store pairs of message origins and callback functions + */ + _channelMap: new Map(), + /** + * + * @param id {String} + * The WebChannel id to include in the message + * @param sender {EventTarget} + * EventTarget with a "messageManager" that will send be used to send the message + * @param [errorMsg] {String} + * Error message + * @private + */ + _sendErrorEventToContent: function (id, sender, errorMsg) { + errorMsg = errorMsg || "Web Channel Broker error"; + + if (sender.messageManager) { + sender.messageManager.sendAsyncMessage("WebChannelMessageToContent", { + id: id, + error: errorMsg, + }, sender); + } + Cu.reportError(id.toString() + " error message. " + errorMsg); + }, +}); + + +/** + * Creates a new WebChannel that listens and sends messages over some channel id + * + * @param id {String} + * WebChannel id + * @param origin {nsIURI} + * Valid origin that should be part of requests for this channel + * @constructor + */ +this.WebChannel = function(id, origin) { + if (!id || !origin) { + throw new Error("WebChannel id and origin are required."); + } + + this.id = id; + this.origin = origin; +}; + +this.WebChannel.prototype = { + + /** + * WebChannel id + */ + id: null, + + /** + * WebChannel origin + */ + origin: null, + + /** + * WebChannelBroker that manages WebChannels + */ + _broker: WebChannelBroker, + + /** + * Callback that will be called with the contents of an incoming message + */ + _deliverCallback: null, + + /** + * Registers the callback for messages on this channel + * Registers the channel itself with the WebChannelBroker + * + * @param callback {Function} + * Callback that will be called when there is a message + * @param {String} id + * The WebChannel id that was used for this message + * @param {Object} message + * The message itself + * @param {EventTarget} sender + * The source of the message + */ + listen: function (callback) { + if (this._deliverCallback) { + throw new Error("Failed to listen. Listener already attached."); + } else if (!callback) { + throw new Error("Failed to listen. Callback argument missing."); + } else { + this._deliverCallback = callback; + this._broker.registerChannel(this); + } + }, + + /** + * Resets the callback for messages on this channel + * Removes the channel from the WebChannelBroker + */ + stopListening: function () { + this._broker.unregisterChannel(this); + this._deliverCallback = null; + }, + + /** + * Sends messages over the WebChannel id using the "WebChannelMessageToContent" event + * + * @param message {Object} + * The message object that will be sent + * @param target {browser} + * The object that has a "messageManager" that sends messages + * + */ + send: function (message, target) { + if (message && target && target.messageManager) { + target.messageManager.sendAsyncMessage("WebChannelMessageToContent", { + id: this.id, + message: message + }); + } else if (!message) { + Cu.reportError("Failed to send a WebChannel message. Message not set."); + } else { + Cu.reportError("Failed to send a WebChannel message. Target invalid."); + } + }, + + /** + * Deliver WebChannel messages to the set "_channelCallback" + * + * @param data {Object} + * Message data + * @param sender {browser} + * Message sender + */ + deliver: function(data, sender) { + if (this._deliverCallback) { + try { + this._deliverCallback(data.id, data.message, sender); + } catch (ex) { + this.send({ + errno: ERRNO_UNKNOWN_ERROR, + error: ex.message ? ex.message : ERROR_UNKNOWN + }, sender); + Cu.reportError("Failed to execute callback:" + ex); + } + } else { + Cu.reportError("No callback set for this channel."); + } + } +}; diff --git a/toolkit/modules/moz.build b/toolkit/modules/moz.build index fb8a65034164..d2b1df87bb5e 100644 --- a/toolkit/modules/moz.build +++ b/toolkit/modules/moz.build @@ -55,6 +55,7 @@ EXTRA_JS_MODULES += [ 'Task.jsm', 'TelemetryTimestamps.jsm', 'Timer.jsm', + 'WebChannel.jsm', 'ZipUtils.jsm', ] diff --git a/toolkit/modules/tests/xpcshell/test_web_channel.js b/toolkit/modules/tests/xpcshell/test_web_channel.js new file mode 100644 index 000000000000..f3e6ced61210 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_web_channel.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const Cu = Components.utils; + +Cu.import("resource://services-common/async.js"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/WebChannel.jsm"); + +const ERROR_ID_ORIGIN_REQUIRED = "WebChannel id and origin are required."; +const VALID_WEB_CHANNEL_ID = "id"; +const URL_STRING = "http://example.com"; +const VALID_WEB_CHANNEL_ORIGIN = Services.io.newURI(URL_STRING, null, null); + +let MockWebChannelBroker = { + _channelMap: new Map(), + registerChannel: function(channel) { + if (!this._channelMap.has(channel)) { + this._channelMap.set(channel); + } + }, + unregisterChannel: function (channelToRemove) { + this._channelMap.delete(channelToRemove) + } +}; + +function run_test() { + run_next_test(); +} + +/** + * Web channel tests + */ + +/** + * Test channel listening + */ +add_test(function test_web_channel_listen() { + let channel = new WebChannel(VALID_WEB_CHANNEL_ID, VALID_WEB_CHANNEL_ORIGIN, { + broker: MockWebChannelBroker + }); + let cb = Async.makeSpinningCallback(); + let delivered = 0; + do_check_eq(channel.id, VALID_WEB_CHANNEL_ID); + do_check_eq(channel.origin.spec, VALID_WEB_CHANNEL_ORIGIN.spec); + do_check_eq(channel._deliverCallback, null); + + channel.listen(function(id, message, target) { + do_check_eq(id, VALID_WEB_CHANNEL_ID); + do_check_true(message); + do_check_true(message.command); + do_check_true(target.sender); + delivered++; + // 2 messages should be delivered + if (delivered === 2) { + channel.stopListening(); + do_check_eq(channel._deliverCallback, null); + cb(); + run_next_test(); + } + }); + + // send two messages + channel.deliver({ + id: VALID_WEB_CHANNEL_ID, + message: { + command: "one" + } + }, { sender: true }); + + channel.deliver({ + id: VALID_WEB_CHANNEL_ID, + message: { + command: "two" + } + }, { sender: true }); + + cb.wait(); +}); + + +/** + * Test constructor + */ +add_test(function test_web_channel_constructor() { + do_check_eq(constructorTester(), ERROR_ID_ORIGIN_REQUIRED); + do_check_eq(constructorTester(undefined), ERROR_ID_ORIGIN_REQUIRED); + do_check_eq(constructorTester(undefined, VALID_WEB_CHANNEL_ORIGIN), ERROR_ID_ORIGIN_REQUIRED); + do_check_eq(constructorTester(VALID_WEB_CHANNEL_ID, undefined), ERROR_ID_ORIGIN_REQUIRED); + do_check_false(constructorTester(VALID_WEB_CHANNEL_ID, VALID_WEB_CHANNEL_ORIGIN)); + + run_next_test(); +}); + +function constructorTester(id, origin) { + try { + new WebChannel(id, origin); + } catch (e) { + return e.message; + } + return false; +} diff --git a/toolkit/modules/tests/xpcshell/test_web_channel_broker.js b/toolkit/modules/tests/xpcshell/test_web_channel_broker.js new file mode 100644 index 000000000000..77f063806c46 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_web_channel_broker.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const Cu = Components.utils; + +Cu.import("resource://services-common/async.js"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/WebChannel.jsm"); + +const VALID_WEB_CHANNEL_ID = "id"; +const URL_STRING = "http://example.com"; +const VALID_WEB_CHANNEL_ORIGIN = Services.io.newURI(URL_STRING, null, null); + +function run_test() { + run_next_test(); +} + +/** + * Test WebChannelBroker channel map + */ +add_test(function test_web_channel_broker_channel_map() { + let channel = new Object(); + let channel2 = new Object(); + + do_check_eq(WebChannelBroker._channelMap.size, 0); + do_check_false(WebChannelBroker._messageListenerAttached); + + // make sure _channelMap works correctly + WebChannelBroker.registerChannel(channel); + do_check_eq(WebChannelBroker._channelMap.size, 1); + do_check_true(WebChannelBroker._messageListenerAttached); + + WebChannelBroker.registerChannel(channel2); + do_check_eq(WebChannelBroker._channelMap.size, 2); + + WebChannelBroker.unregisterChannel(channel); + do_check_eq(WebChannelBroker._channelMap.size, 1); + + // make sure the correct channel is unregistered + do_check_false(WebChannelBroker._channelMap.has(channel)); + do_check_true(WebChannelBroker._channelMap.has(channel2)); + + WebChannelBroker.unregisterChannel(channel2); + do_check_eq(WebChannelBroker._channelMap.size, 0); + + run_next_test(); +}); + + +/** + * Test WebChannelBroker _listener test + */ +add_test(function test_web_channel_broker_listener() { + let cb = Async.makeSpinningCallback(); + var channel = new Object({ + id: VALID_WEB_CHANNEL_ID, + origin: VALID_WEB_CHANNEL_ORIGIN, + deliver: function(data, sender) { + do_check_eq(data.id, VALID_WEB_CHANNEL_ID); + do_check_eq(data.message.command, "hello"); + WebChannelBroker.unregisterChannel(channel); + cb(); + run_next_test(); + } + }); + + WebChannelBroker.registerChannel(channel); + + var mockEvent = { + data: { + id: VALID_WEB_CHANNEL_ID, + message: { + command: "hello" + } + }, + principal: { + origin: URL_STRING + } + }; + + WebChannelBroker._listener(mockEvent); + cb.wait(); +}); diff --git a/toolkit/modules/tests/xpcshell/xpcshell.ini b/toolkit/modules/tests/xpcshell/xpcshell.ini index 05b1cf7157c3..441b6b9ffd16 100644 --- a/toolkit/modules/tests/xpcshell/xpcshell.ini +++ b/toolkit/modules/tests/xpcshell/xpcshell.ini @@ -28,4 +28,6 @@ support-files = [test_task.js] [test_TelemetryTimestamps.js] [test_timer.js] +[test_web_channel.js] +[test_web_channel_broker.js] [test_ZipUtils.js] From e0bbd250b053494d9d03cea9645fd6bdc6deb90a Mon Sep 17 00:00:00 2001 From: Jordan Santell Date: Wed, 30 Jul 2014 15:24:00 -0400 Subject: [PATCH 02/18] Bug 1046305 - Wait a frame before checking canvases in getPixel tests. r=vp --- .../devtools/shadereditor/test/browser_webgl-actor-test-18.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/browser/devtools/shadereditor/test/browser_webgl-actor-test-18.js b/browser/devtools/shadereditor/test/browser_webgl-actor-test-18.js index 6da7e3d28dff..89b396261653 100644 --- a/browser/devtools/shadereditor/test/browser_webgl-actor-test-18.js +++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-18.js @@ -11,6 +11,9 @@ function ifWebGLSupported() { yield getPrograms(front, 2); + // Wait a frame to ensure rendering + yield front.waitForFrame(); + let pixel = yield front.getPixel({ selector: "#canvas1", position: { x: 0, y: 0 }}); is(pixel.r, 255, "correct `r` value for first canvas.") is(pixel.g, 255, "correct `g` value for first canvas.") From 31325a95d3d85ff6440da860e6ca6f49289765be Mon Sep 17 00:00:00 2001 From: Margaret Leibovic Date: Fri, 1 Aug 2014 11:42:11 -0700 Subject: [PATCH 03/18] Bug 1047128 - Launch URLs from search activity in Fennec. r=rnewman --- .../search/java/org/mozilla/search/PostSearchFragment.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mobile/android/search/java/org/mozilla/search/PostSearchFragment.java b/mobile/android/search/java/org/mozilla/search/PostSearchFragment.java index 329475ce6162..4905fd3f3a49 100644 --- a/mobile/android/search/java/org/mozilla/search/PostSearchFragment.java +++ b/mobile/android/search/java/org/mozilla/search/PostSearchFragment.java @@ -18,6 +18,7 @@ import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.ProgressBar; +import org.mozilla.gecko.AppConstants; import org.mozilla.gecko.Telemetry; import org.mozilla.gecko.TelemetryContract; @@ -100,6 +101,7 @@ public class PostSearchFragment extends Fragment { TelemetryContract.Method.CONTENT, "search-result"); view.stopLoading(); Intent i = new Intent(Intent.ACTION_VIEW); + i.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.BROWSER_INTENT_CLASS_NAME); i.setData(Uri.parse(url)); startActivity(i); } From ed1e90975a5c439776acb8e00343658786e8f01a Mon Sep 17 00:00:00 2001 From: Margaret Leibovic Date: Fri, 1 Aug 2014 11:42:14 -0700 Subject: [PATCH 04/18] Bug 1042415 - Don't allow user to submit empty query. r=eedens --- .../org/mozilla/search/autocomplete/SearchFragment.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/mobile/android/search/java/org/mozilla/search/autocomplete/SearchFragment.java b/mobile/android/search/java/org/mozilla/search/autocomplete/SearchFragment.java index 55f3e9126849..1efff79a8fe3 100644 --- a/mobile/android/search/java/org/mozilla/search/autocomplete/SearchFragment.java +++ b/mobile/android/search/java/org/mozilla/search/autocomplete/SearchFragment.java @@ -13,6 +13,7 @@ import android.support.v4.app.LoaderManager; import android.support.v4.content.AsyncTaskLoader; import android.support.v4.content.Loader; import android.text.SpannableString; +import android.text.TextUtils; import android.text.style.ForegroundColorSpan; import android.view.LayoutInflater; import android.view.View; @@ -136,8 +137,12 @@ public class SearchFragment extends Fragment implements AcceptsJumpTaps { @Override public void onSubmit(String text) { - transitionToWaiting(); - searchListener.onSearch(text); + // Don't submit an empty query. + final String trimmedQuery = text.trim(); + if (!TextUtils.isEmpty(trimmedQuery)) { + transitionToWaiting(); + searchListener.onSearch(trimmedQuery); + } } }); From 93b5ed4b0cc24ae59c5f0b7938c10ed3e6c1d903 Mon Sep 17 00:00:00 2001 From: Drew Willcoxon Date: Fri, 1 Aug 2014 11:57:20 -0700 Subject: [PATCH 05/18] Bug 612453 - Provide search suggestions on Firefox Start Page (about:home) (part 1, SearchSuggestionController). r=MattN --- .../search/SearchSuggestionController.jsm | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/toolkit/components/search/SearchSuggestionController.jsm b/toolkit/components/search/SearchSuggestionController.jsm index e166c8528900..74f4413916f8 100644 --- a/toolkit/components/search/SearchSuggestionController.jsm +++ b/toolkit/components/search/SearchSuggestionController.jsm @@ -46,6 +46,16 @@ this.SearchSuggestionController.prototype = { */ maxRemoteResults: 10, + /** + * The maximum time (ms) to wait before giving up on a remote suggestions. + */ + remoteTimeout: REMOTE_TIMEOUT, + + /** + * The additional parameter used when searching form history. + */ + formHistoryParam: DEFAULT_FORM_HISTORY_PARAM, + // Private properties /** * The last form history result used to improve the performance of subsequent searches. @@ -168,7 +178,7 @@ this.SearchSuggestionController.prototype = { this._remoteResultTimer = Cc["@mozilla.org/timer;1"]. createInstance(Ci.nsITimer); this._remoteResultTimer.initWithCallback(this._onRemoteTimeout.bind(this), - REMOTE_TIMEOUT, + this.remoteTimeout || REMOTE_TIMEOUT, Ci.nsITimer.TYPE_ONE_SHOT); } @@ -199,7 +209,8 @@ this.SearchSuggestionController.prototype = { let formHistory = Cc["@mozilla.org/autocomplete/search;1?name=form-history"]. createInstance(Ci.nsIAutoCompleteSearch); - formHistory.startSearch(searchTerm, DEFAULT_FORM_HISTORY_PARAM, this._formHistoryResult, + formHistory.startSearch(searchTerm, this.formHistoryParam || DEFAULT_FORM_HISTORY_PARAM, + this._formHistoryResult, acSearchObserver); return deferredFormHistory; }, @@ -347,3 +358,13 @@ this.SearchSuggestionController.prototype = { this._searchString = null; }, }; + +/** + * Determines whether the given engine offers search suggestions. + * + * @param {nsISearchEngine} engine - The search engine + * @return {boolean} True if the engine offers suggestions and false otherwise. + */ +this.SearchSuggestionController.engineOffersSuggestions = function(engine) { + return engine.supportsResponseType(SEARCH_RESPONSE_SUGGESTION_JSON); +}; From f5a7cdd231ee759d23ed07de2766ec3a43ce989c Mon Sep 17 00:00:00 2001 From: Drew Willcoxon Date: Fri, 1 Aug 2014 12:00:44 -0700 Subject: [PATCH 06/18] Bug 612453 - Provide search suggestions on Firefox Start Page (about:home) (part 2, ContentSearch). r=MattN,felipe --- browser/base/content/content.js | 9 +- browser/modules/ContentSearch.jsm | 181 +++++++++++++++--- browser/modules/test/browser.ini | 2 + browser/modules/test/browser_ContentSearch.js | 88 ++++++++- .../modules/test/contentSearchSuggestions.sjs | 9 + .../modules/test/contentSearchSuggestions.xml | 6 + 6 files changed, 269 insertions(+), 26 deletions(-) create mode 100644 browser/modules/test/contentSearchSuggestions.sjs create mode 100644 browser/modules/test/contentSearchSuggestions.xml diff --git a/browser/base/content/content.js b/browser/base/content/content.js index 3568b42bb9ac..0a7e910dac3e 100644 --- a/browser/base/content/content.js +++ b/browser/base/content/content.js @@ -246,6 +246,7 @@ addMessageListener("WebChannelMessageToContent", function (e) { let ContentSearchMediator = { whitelist: new Set([ + "about:home", "about:newtab", ]), @@ -274,7 +275,7 @@ let ContentSearchMediator = { }, get _contentWhitelisted() { - return this.whitelist.has(content.document.documentURI.toLowerCase()); + return this.whitelist.has(content.document.documentURI); }, _sendMsg: function (type, data=null) { @@ -285,12 +286,14 @@ let ContentSearchMediator = { }, _fireEvent: function (type, data=null) { - content.dispatchEvent(new content.CustomEvent("ContentSearchService", { + let event = Cu.cloneInto({ detail: { type: type, data: data, }, - })); + }, content); + content.dispatchEvent(new content.CustomEvent("ContentSearchService", + event)); }, }; ContentSearchMediator.init(this); diff --git a/browser/modules/ContentSearch.jsm b/browser/modules/ContentSearch.jsm index 3dc9ced5cb93..0c7f33a51885 100644 --- a/browser/modules/ContentSearch.jsm +++ b/browser/modules/ContentSearch.jsm @@ -13,6 +13,14 @@ const { classes: Cc, interfaces: Ci, utils: Cu } = Components; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/Promise.jsm"); Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "FormHistory", + "resource://gre/modules/FormHistory.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "SearchSuggestionController", + "resource://gre/modules/SearchSuggestionController.jsm"); const INBOUND_MESSAGE = "ContentSearch"; const OUTBOUND_MESSAGE = INBOUND_MESSAGE; @@ -26,30 +34,45 @@ const OUTBOUND_MESSAGE = INBOUND_MESSAGE; * * Inbound messages have the following types: * + * AddFormHistoryEntry + * Adds an entry to the search form history. + * data: the entry, a string + * GetSuggestions + * Retrieves an array of search suggestions given a search string. + * data: { engineName, searchString, [remoteTimeout] } * GetState - * Retrieves the current search engine state. - * data: null + * Retrieves the current search engine state. + * data: null * ManageEngines - * Opens the search engine management window. - * data: null + * Opens the search engine management window. + * data: null + * RemoveFormHistoryEntry + * Removes an entry from the search form history. + * data: the entry, a string * Search - * Performs a search. - * data: an object { engineName, searchString, whence } + * Performs a search. + * data: { engineName, searchString, whence } * SetCurrentEngine - * Sets the current engine. - * data: the name of the engine + * Sets the current engine. + * data: the name of the engine + * SpeculativeConnect + * Speculatively connects to an engine. + * data: the name of the engine * * Outbound messages have the following types: * * CurrentEngine - * Sent when the current engine changes. + * Broadcast when the current engine changes. * data: see _currentEngineObj * CurrentState - * Sent when the current search state changes. + * Broadcast when the current search state changes. * data: see _currentStateObj * State * Sent in reply to GetState. * data: see _currentStateObj + * Suggestions + * Sent in reply to GetSuggestions. + * data: see _onMessageGetSuggestions */ this.ContentSearch = { @@ -60,6 +83,10 @@ this.ContentSearch = { _eventQueue: [], _currentEvent: null, + // This is used to handle search suggestions. It maps xul:browsers to objects + // { controller, previousFormHistoryResult }. See _onMessageGetSuggestions. + _suggestionMap: new WeakMap(), + init: function () { Cc["@mozilla.org/globalmessagemanager;1"]. getService(Ci.nsIMessageListenerManager). @@ -72,10 +99,15 @@ this.ContentSearch = { // the event queue. If the message's source docshell changes browsers in // the meantime, then we need to update msg.target. event.detail will be // the docshell's new parent element. - msg.handleEvent = function (event) { - this.target.removeEventListener("SwapDocShells", this, true); - this.target = event.detail; - this.target.addEventListener("SwapDocShells", this, true); + msg.handleEvent = event => { + let browserData = this._suggestionMap.get(msg.target); + if (browserData) { + this._suggestionMap.delete(msg.target); + this._suggestionMap.set(event.detail, browserData); + } + msg.target.removeEventListener("SwapDocShells", msg, true); + msg.target = event.detail; + msg.target.addEventListener("SwapDocShells", msg, true); }; msg.target.addEventListener("SwapDocShells", msg, true); @@ -106,6 +138,9 @@ this.ContentSearch = { try { yield this["_on" + this._currentEvent.type](this._currentEvent.data); } + catch (err) { + Cu.reportError(err); + } finally { this._currentEvent = null; this._processEventQueue(); @@ -128,17 +163,11 @@ this.ContentSearch = { }, _onMessageSearch: function (msg, data) { - let expectedDataProps = [ + this._ensureDataHasProperties(data, [ "engineName", "searchString", "whence", - ]; - for (let prop of expectedDataProps) { - if (!(prop in data)) { - Cu.reportError("Message data missing required property: " + prop); - return Promise.resolve(); - } - } + ]); let browserWin = msg.target.ownerDocument.defaultView; let engine = Services.search.getEngineByName(data.engineName); browserWin.BrowserSearch.recordSearchInHealthReport(engine, data.whence); @@ -158,6 +187,92 @@ this.ContentSearch = { return Promise.resolve(); }, + _onMessageGetSuggestions: Task.async(function* (msg, data) { + this._ensureDataHasProperties(data, [ + "engineName", + "searchString", + ]); + + let engine = Services.search.getEngineByName(data.engineName); + if (!engine) { + throw new Error("Unknown engine name: " + data.engineName); + } + + let browserData = this._suggestionDataForBrowser(msg.target, true); + let { controller } = browserData; + let ok = SearchSuggestionController.engineOffersSuggestions(engine); + controller.maxLocalResults = ok ? 2 : 6; + controller.maxRemoteResults = ok ? 6 : 0; + controller.remoteTimeout = data.remoteTimeout || undefined; + let priv = PrivateBrowsingUtils.isWindowPrivate(msg.target.contentWindow); + // fetch() rejects its promise if there's a pending request, but since we + // process our event queue serially, there's never a pending request. + let suggestions = yield controller.fetch(data.searchString, priv, engine); + + // Keep the form history result so RemoveFormHistoryEntry can remove entries + // from it. Keeping only one result isn't foolproof because the client may + // try to remove an entry from one set of suggestions after it has requested + // more but before it's received them. In that case, the entry may not + // appear in the new suggestions. But that should happen rarely. + browserData.previousFormHistoryResult = suggestions.formHistoryResult; + + this._reply(msg, "Suggestions", { + engineName: data.engineName, + searchString: suggestions.term, + formHistory: suggestions.local, + remote: suggestions.remote, + }); + }), + + _onMessageAddFormHistoryEntry: function (msg, entry) { + // There are some tests that use about:home and newtab that trigger a search + // and then immediately close the tab. In those cases, the browser may have + // been destroyed by the time we receive this message, and as a result + // contentWindow is undefined. + if (!msg.target.contentWindow || + PrivateBrowsingUtils.isWindowPrivate(msg.target.contentWindow)) { + return Promise.resolve(); + } + let browserData = this._suggestionDataForBrowser(msg.target, true); + FormHistory.update({ + op: "bump", + fieldname: browserData.controller.formHistoryParam, + value: entry, + }, { + handleCompletion: () => {}, + handleError: err => { + Cu.reportError("Error adding form history entry: " + err); + }, + }); + return Promise.resolve(); + }, + + _onMessageRemoveFormHistoryEntry: function (msg, entry) { + let browserData = this._suggestionDataForBrowser(msg.target); + if (browserData && browserData.previousFormHistoryResult) { + let { previousFormHistoryResult } = browserData; + for (let i = 0; i < previousFormHistoryResult.matchCount; i++) { + if (previousFormHistoryResult.getValueAt(i) == entry) { + previousFormHistoryResult.removeValueAt(i, true); + break; + } + } + } + return Promise.resolve(); + }, + + _onMessageSpeculativeConnect: function (msg, engineName) { + let engine = Services.search.getEngineByName(engineName); + if (!engine) { + throw new Error("Unknown engine name: " + engineName); + } + if (msg.target.contentWindow) { + engine.speculativeConnect({ + window: msg.target.contentWindow, + }); + } + }, + _onObserve: Task.async(function* (data) { if (data == "engine-current") { let engine = yield this._currentEngineObj(); @@ -171,6 +286,20 @@ this.ContentSearch = { } }), + _suggestionDataForBrowser: function (browser, create=false) { + let data = this._suggestionMap.get(browser); + if (!data && create) { + // Since one SearchSuggestionController instance is meant to be used per + // autocomplete widget, this means that we assume each xul:browser has at + // most one such widget. + data = { + controller: new SearchSuggestionController(), + }; + this._suggestionMap.set(browser, data); + } + return data; + }, + _reply: function (msg, type, data) { // We reply asyncly to messages, and by the time we reply the browser we're // responding to may have been destroyed. messageManager is null then. @@ -241,6 +370,14 @@ this.ContentSearch = { return deferred.promise; }, + _ensureDataHasProperties: function (data, requiredProperties) { + for (let prop of requiredProperties) { + if (!(prop in data)) { + throw new Error("Message data missing required property: " + prop); + } + } + }, + _initService: function () { if (!this._initServicePromise) { let deferred = Promise.defer(); diff --git a/browser/modules/test/browser.ini b/browser/modules/test/browser.ini index 02d79bc3d174..7a1960ea9d69 100644 --- a/browser/modules/test/browser.ini +++ b/browser/modules/test/browser.ini @@ -9,6 +9,8 @@ support-files = support-files = contentSearch.js contentSearchBadImage.xml + contentSearchSuggestions.sjs + contentSearchSuggestions.xml [browser_NetworkPrioritizer.js] skip-if = e10s # Bug 666804 - Support NetworkPrioritizer in e10s [browser_SignInToWebsite.js] diff --git a/browser/modules/test/browser_ContentSearch.js b/browser/modules/test/browser_ContentSearch.js index 7e4dbe8e5d01..4bee1a734998 100644 --- a/browser/modules/test/browser_ContentSearch.js +++ b/browser/modules/test/browser_ContentSearch.js @@ -171,6 +171,92 @@ add_task(function* badImage() { yield waitForTestMsg("CurrentState"); }); +add_task(function* GetSuggestions_AddFormHistoryEntry_RemoveFormHistoryEntry() { + yield addTab(); + + // Add the test engine that provides suggestions. + let vals = yield waitForNewEngine("contentSearchSuggestions.xml", 0); + let engine = vals[0]; + + let searchStr = "browser_ContentSearch.js-suggestions-"; + + // Add a form history suggestion and wait for Satchel to notify about it. + gMsgMan.sendAsyncMessage(TEST_MSG, { + type: "AddFormHistoryEntry", + data: searchStr + "form", + }); + let deferred = Promise.defer(); + Services.obs.addObserver(function onAdd(subj, topic, data) { + if (data == "formhistory-add") { + executeSoon(() => deferred.resolve()); + } + }, "satchel-storage-changed", false); + yield deferred.promise; + + // Send GetSuggestions using the test engine. Its suggestions should appear + // in the remote suggestions in the Suggestions response below. + gMsgMan.sendAsyncMessage(TEST_MSG, { + type: "GetSuggestions", + data: { + engineName: engine.name, + searchString: searchStr, + remoteTimeout: 5000, + }, + }); + + // Check the Suggestions response. + let msg = yield waitForTestMsg("Suggestions"); + checkMsg(msg, { + type: "Suggestions", + data: { + engineName: engine.name, + searchString: searchStr, + formHistory: [searchStr + "form"], + remote: [searchStr + "foo", searchStr + "bar"], + }, + }); + + // Delete the form history suggestion and wait for Satchel to notify about it. + gMsgMan.sendAsyncMessage(TEST_MSG, { + type: "RemoveFormHistoryEntry", + data: searchStr + "form", + }); + deferred = Promise.defer(); + Services.obs.addObserver(function onRemove(subj, topic, data) { + if (data == "formhistory-remove") { + executeSoon(() => deferred.resolve()); + } + }, "satchel-storage-changed", false); + yield deferred.promise; + + // Send GetSuggestions again. + gMsgMan.sendAsyncMessage(TEST_MSG, { + type: "GetSuggestions", + data: { + engineName: engine.name, + searchString: searchStr, + remoteTimeout: 5000, + }, + }); + + // The formHistory suggestions in the Suggestions response should be empty. + msg = yield waitForTestMsg("Suggestions"); + checkMsg(msg, { + type: "Suggestions", + data: { + engineName: engine.name, + searchString: searchStr, + formHistory: [], + remote: [searchStr + "foo", searchStr + "bar"], + }, + }); + + // Finally, clean up by removing the test engine. + Services.search.removeEngine(engine); + yield waitForTestMsg("CurrentState"); +}); + + function checkMsg(actualMsg, expectedMsgData) { SimpleTest.isDeeply(actualMsg.data, expectedMsgData, "Checking message"); } @@ -226,7 +312,7 @@ function addTab() { let tab = gBrowser.addTab(); gBrowser.selectedTab = tab; tab.linkedBrowser.addEventListener("load", function load() { - tab.removeEventListener("load", load, true); + tab.linkedBrowser.removeEventListener("load", load, true); let url = getRootDirectory(gTestPath) + TEST_CONTENT_SCRIPT_BASENAME; gMsgMan = tab.linkedBrowser.messageManager; gMsgMan.sendAsyncMessage(CONTENT_SEARCH_MSG, { diff --git a/browser/modules/test/contentSearchSuggestions.sjs b/browser/modules/test/contentSearchSuggestions.sjs new file mode 100644 index 000000000000..1978b4f66512 --- /dev/null +++ b/browser/modules/test/contentSearchSuggestions.sjs @@ -0,0 +1,9 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function handleRequest(req, resp) { + let suffixes = ["foo", "bar"]; + let data = [req.queryString, suffixes.map(s => req.queryString + s)]; + resp.setHeader("Content-Type", "application/json", false); + resp.write(JSON.stringify(data)); +} diff --git a/browser/modules/test/contentSearchSuggestions.xml b/browser/modules/test/contentSearchSuggestions.xml new file mode 100644 index 000000000000..81c23379cada --- /dev/null +++ b/browser/modules/test/contentSearchSuggestions.xml @@ -0,0 +1,6 @@ + + +browser_ContentSearch contentSearchSuggestions.xml + + + From 94376cba8e3893a32d7d90d8e9f10bc704e2442c Mon Sep 17 00:00:00 2001 From: Drew Willcoxon Date: Fri, 1 Aug 2014 12:00:47 -0700 Subject: [PATCH 07/18] Bug 612453 - Provide search suggestions on Firefox Start Page (about:home) (part 3, searchSuggestionUI and about:home). r=MattN --- browser/base/content/abouthome/aboutHome.js | 14 +- .../base/content/abouthome/aboutHome.xhtml | 4 + browser/base/content/searchSuggestionUI.css | 48 +++ browser/base/content/searchSuggestionUI.js | 379 ++++++++++++++++++ browser/base/content/test/general/browser.ini | 6 + .../content/test/general/browser_aboutHome.js | 69 +++- .../general/browser_searchSuggestionUI.js | 305 ++++++++++++++ .../test/general/searchSuggestionEngine.sjs | 9 + .../test/general/searchSuggestionEngine.xml | 9 + .../test/general/searchSuggestionUI.html | 20 + .../test/general/searchSuggestionUI.js | 138 +++++++ browser/base/jar.mn | 2 + 12 files changed, 1001 insertions(+), 2 deletions(-) create mode 100644 browser/base/content/searchSuggestionUI.css create mode 100644 browser/base/content/searchSuggestionUI.js create mode 100644 browser/base/content/test/general/browser_searchSuggestionUI.js create mode 100644 browser/base/content/test/general/searchSuggestionEngine.sjs create mode 100644 browser/base/content/test/general/searchSuggestionEngine.xml create mode 100644 browser/base/content/test/general/searchSuggestionUI.html create mode 100644 browser/base/content/test/general/searchSuggestionUI.js diff --git a/browser/base/content/abouthome/aboutHome.js b/browser/base/content/abouthome/aboutHome.js index 60702027b931..61b940a96cf9 100644 --- a/browser/base/content/abouthome/aboutHome.js +++ b/browser/base/content/abouthome/aboutHome.js @@ -310,10 +310,16 @@ function onSearchSubmit(aEvent) document.dispatchEvent(event); } - aEvent.preventDefault(); + gSearchSuggestionController.addInputValueToFormHistory(); + + if (aEvent) { + aEvent.preventDefault(); + } } +let gSearchSuggestionController; + function setupSearchEngine() { // The "autofocus" attribute doesn't focus the form element @@ -341,6 +347,12 @@ function setupSearchEngine() searchText.placeholder = searchEngineName; } + if (!gSearchSuggestionController) { + gSearchSuggestionController = + new SearchSuggestionUIController(searchText, searchText.parentNode, + onSearchSubmit); + } + gSearchSuggestionController.engineName = searchEngineName; } /** diff --git a/browser/base/content/abouthome/aboutHome.xhtml b/browser/base/content/abouthome/aboutHome.xhtml index 3051452264bc..867092d6d907 100644 --- a/browser/base/content/abouthome/aboutHome.xhtml +++ b/browser/base/content/abouthome/aboutHome.xhtml @@ -24,10 +24,14 @@ + + + + + + + + + diff --git a/browser/base/content/test/general/searchSuggestionUI.js b/browser/base/content/test/general/searchSuggestionUI.js new file mode 100644 index 000000000000..4fba92d6c196 --- /dev/null +++ b/browser/base/content/test/general/searchSuggestionUI.js @@ -0,0 +1,138 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +(function () { + +const TEST_MSG = "SearchSuggestionUIControllerTest"; +const ENGINE_NAME = "browser_searchSuggestionEngine searchSuggestionEngine.xml"; + +let input = content.document.querySelector("input"); +let gController = + new content.SearchSuggestionUIController(input, input.parentNode); +gController.engineName = ENGINE_NAME; +gController.remoteTimeout = 5000; + +addMessageListener(TEST_MSG, msg => { + messageHandlers[msg.data.type](msg.data.data); +}); + +let messageHandlers = { + + key: function (arg) { + let keyName = typeof(arg) == "string" ? arg : arg.key; + content.synthesizeKey(keyName, {}); + let wait = arg.waitForSuggestions ? waitForSuggestions : cb => cb(); + wait(ack); + }, + + focus: function () { + gController.input.focus(); + ack(); + }, + + blur: function () { + gController.input.blur(); + ack(); + }, + + mousemove: function (suggestionIdx) { + // Copied from widget/tests/test_panel_mouse_coords.xul and + // browser/base/content/test/newtab/head.js + let row = gController._table.children[suggestionIdx]; + let rect = row.getBoundingClientRect(); + let left = content.mozInnerScreenX + rect.left; + let x = left + rect.width / 2; + let y = content.mozInnerScreenY + rect.top + rect.height / 2; + + let utils = content.SpecialPowers.getDOMWindowUtils(content); + let scale = utils.screenPixelsPerCSSPixel; + + let widgetToolkit = content.SpecialPowers. + Cc["@mozilla.org/xre/app-info;1"]. + getService(content.SpecialPowers.Ci.nsIXULRuntime). + widgetToolkit; + let nativeMsg = widgetToolkit == "cocoa" ? 5 : // NSMouseMoved + widgetToolkit == "windows" ? 1 : // MOUSEEVENTF_MOVE + 3; // GDK_MOTION_NOTIFY + + row.addEventListener("mousemove", function onMove() { + row.removeEventListener("mousemove", onMove); + ack(); + }); + utils.sendNativeMouseEvent(x * scale, y * scale, nativeMsg, 0, null); + }, + + mousedown: function (suggestionIdx) { + gController.onClick = () => { + gController.onClick = null; + ack(); + }; + let row = gController._table.children[suggestionIdx]; + content.sendMouseEvent({ type: "mousedown" }, row); + }, + + addInputValueToFormHistory: function () { + gController.addInputValueToFormHistory(); + ack(); + }, + + reset: function () { + // Reset both the input and suggestions by select all + delete. + gController.input.focus(); + content.synthesizeKey("a", { accelKey: true }); + content.synthesizeKey("VK_DELETE", {}); + ack(); + }, +}; + +function ack() { + sendAsyncMessage(TEST_MSG, currentState()); +} + +function waitForSuggestions(cb) { + let observer = new content.MutationObserver(() => { + if (gController.input.getAttribute("aria-expanded") == "true") { + observer.disconnect(); + cb(); + } + }); + observer.observe(gController.input, { + attributes: true, + attributeFilter: ["aria-expanded"], + }); +} + +function currentState() { + let state = { + selectedIndex: gController.selectedIndex, + numSuggestions: gController.numSuggestions, + suggestionAtIndex: [], + isFormHistorySuggestionAtIndex: [], + + tableHidden: gController._table.hidden, + tableChildrenLength: gController._table.children.length, + tableChildren: [], + + inputValue: gController.input.value, + ariaExpanded: gController.input.getAttribute("aria-expanded"), + }; + + for (let i = 0; i < gController.numSuggestions; i++) { + state.suggestionAtIndex.push(gController.suggestionAtIndex(i)); + state.isFormHistorySuggestionAtIndex.push( + gController.isFormHistorySuggestionAtIndex(i)); + } + + for (let child of gController._table.children) { + state.tableChildren.push({ + textContent: child.textContent, + classes: new Set(child.className.split(/\s+/)), + }); + } + + return state; +} + +})(); diff --git a/browser/base/jar.mn b/browser/base/jar.mn index 0fd92c1cccd9..bcddb21baa9e 100644 --- a/browser/base/jar.mn +++ b/browser/base/jar.mn @@ -118,6 +118,8 @@ browser.jar: * content/browser/sanitize.xul (content/sanitize.xul) * content/browser/sanitizeDialog.js (content/sanitizeDialog.js) content/browser/sanitizeDialog.css (content/sanitizeDialog.css) + content/browser/searchSuggestionUI.js (content/searchSuggestionUI.js) + content/browser/searchSuggestionUI.css (content/searchSuggestionUI.css) content/browser/tabbrowser.css (content/tabbrowser.css) * content/browser/tabbrowser.xml (content/tabbrowser.xml) * content/browser/urlbarBindings.xml (content/urlbarBindings.xml) From b63e5479859ecb956430737f8930409ca106a7d2 Mon Sep 17 00:00:00 2001 From: Drew Willcoxon Date: Fri, 1 Aug 2014 12:00:49 -0700 Subject: [PATCH 08/18] Bug 1028985 - Provide search suggestions on Firefox new tab page (about:newtab). r=MattN --- browser/base/content/newtab/newTab.css | 5 ++ browser/base/content/newtab/newTab.xul | 3 ++ browser/base/content/newtab/search.js | 19 ++++++- browser/base/content/test/newtab/browser.ini | 2 + .../test/newtab/browser_newtab_search.js | 52 ++++++++++++++++++- 5 files changed, 78 insertions(+), 3 deletions(-) diff --git a/browser/base/content/newtab/newTab.css b/browser/base/content/newtab/newTab.css index ca3a2c7ac046..4b4a0efeed0d 100644 --- a/browser/base/content/newtab/newTab.css +++ b/browser/base/content/newtab/newTab.css @@ -392,3 +392,8 @@ input[type=button] { .newtab-search-panel-engine[selected] { background: url("chrome://global/skin/menu/shared-menu-check.png") center left 4px no-repeat transparent; } + +.searchSuggestionTable { + font: message-box; + font-size: 16px; +} diff --git a/browser/base/content/newtab/newTab.xul b/browser/base/content/newtab/newTab.xul index cb8be859190c..f2a1fd41289d 100644 --- a/browser/base/content/newtab/newTab.xul +++ b/browser/base/content/newtab/newTab.xul @@ -5,6 +5,7 @@ - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + @@ -97,4 +98,6 @@ + diff --git a/browser/base/content/newtab/search.js b/browser/base/content/newtab/search.js index e56c01dcdfc8..2d29c65a4a7d 100644 --- a/browser/base/content/newtab/search.js +++ b/browser/base/content/newtab/search.js @@ -30,7 +30,9 @@ let gSearch = { }, search: function (event) { - event.preventDefault(); + if (event) { + event.preventDefault(); + } let searchStr = this._nodes.text.value; if (this.currentEngineName && searchStr.length) { this._send("Search", { @@ -39,6 +41,7 @@ let gSearch = { whence: "newtab", }); } + this._suggestionController.addInputValueToFormHistory(); }, manageEngines: function () { @@ -47,7 +50,10 @@ let gSearch = { }, handleEvent: function (event) { - this["on" + event.detail.type](event.detail.data); + let methodName = "on" + event.detail.type; + if (this.hasOwnProperty(methodName)) { + this[methodName](event.detail.data); + } }, onState: function (data) { @@ -183,5 +189,14 @@ let gSearch = { this._nodes.logo.hidden = true; this._nodes.text.placeholder = engine.name; } + + // Set up the suggestion controller. + if (!this._suggestionController) { + let parent = document.getElementById("newtab-scrollbox"); + this._suggestionController = + new SearchSuggestionUIController(this._nodes.text, parent, + () => this.search()); + } + this._suggestionController.engineName = engine.name; }, }; diff --git a/browser/base/content/test/newtab/browser.ini b/browser/base/content/test/newtab/browser.ini index f06ab842a886..506b41b2f12e 100644 --- a/browser/base/content/test/newtab/browser.ini +++ b/browser/base/content/test/newtab/browser.ini @@ -34,6 +34,8 @@ support-files = searchEngine1xLogo.xml searchEngine2xLogo.xml searchEngine1x2xLogo.xml + ../general/searchSuggestionEngine.xml + ../general/searchSuggestionEngine.sjs [browser_newtab_sponsored_icon_click.js] [browser_newtab_tabsync.js] [browser_newtab_undo.js] diff --git a/browser/base/content/test/newtab/browser_newtab_search.js b/browser/base/content/test/newtab/browser_newtab_search.js index c6584ef36535..1e7a65da4c0e 100644 --- a/browser/base/content/test/newtab/browser_newtab_search.js +++ b/browser/base/content/test/newtab/browser_newtab_search.js @@ -8,6 +8,7 @@ const ENGINE_NO_LOGO = "searchEngineNoLogo.xml"; const ENGINE_1X_LOGO = "searchEngine1xLogo.xml"; const ENGINE_2X_LOGO = "searchEngine2xLogo.xml"; const ENGINE_1X_2X_LOGO = "searchEngine1x2xLogo.xml"; +const ENGINE_SUGGESTIONS = "searchSuggestionEngine.xml"; const SERVICE_EVENT_NAME = "ContentSearchService"; @@ -141,6 +142,50 @@ function runTests() { promiseClick(manageBox), ]).then(TestRunner.next); + // Add the engine that provides search suggestions and switch to it. + let suggestionEngine = null; + yield promiseNewSearchEngine(ENGINE_SUGGESTIONS, 0).then(engine => { + suggestionEngine = engine; + TestRunner.next(); + }); + Services.search.currentEngine = suggestionEngine; + yield promiseSearchEvents(["CurrentEngine"]).then(TestRunner.next); + yield checkCurrentEngine(ENGINE_SUGGESTIONS, false, false); + + // Avoid intermittent failures. + gSearch()._suggestionController.remoteTimeout = 5000; + + // Type an X in the search input. This is only a smoke test. See + // browser_searchSuggestionUI.js for comprehensive content search suggestion + // UI tests. + let input = $("text"); + input.focus(); + EventUtils.synthesizeKey("x", {}); + let suggestionsPromise = promiseSearchEvents(["Suggestions"]); + + // Wait for the search suggestions to become visible and for the Suggestions + // message. + let table = getContentDocument().getElementById("searchSuggestionTable"); + info("Waiting for suggestions table to open"); + let observer = new MutationObserver(() => { + if (input.getAttribute("aria-expanded") == "true") { + observer.disconnect(); + ok(!table.hidden, "Search suggestion table unhidden"); + TestRunner.next(); + } + }); + observer.observe(input, { + attributes: true, + attributeFilter: ["aria-expanded"], + }); + yield undefined; + yield suggestionsPromise.then(TestRunner.next); + + // Empty the search input, causing the suggestions to be hidden. + EventUtils.synthesizeKey("a", { accelKey: true }); + EventUtils.synthesizeKey("VK_DELETE", {}); + ok(table.hidden, "Search suggestion table hidden"); + // Done. Revert the current engine and remove the new engines. Services.search.currentEngine = oldCurrentEngine; yield promiseSearchEvents(["CurrentEngine"]).then(TestRunner.next); @@ -264,6 +309,11 @@ function checkCurrentEngine(basename, has1xLogo, has2xLogo) { ok(/^url\("blob:/.test(logo.style.backgroundImage), "Logo URI"); //" } + if (logo.hidden) { + executeSoon(TestRunner.next); + return; + } + // "selected" attributes of engines in the panel let panel = searchPanel(); promisePanelShown(panel).then(() => { @@ -283,7 +333,7 @@ function checkCurrentEngine(basename, has1xLogo, has2xLogo) { } TestRunner.next(); }); - panel.openPopup(logoImg()); + panel.openPopup(logo); } function promisePanelShown(panel) { From 4f9ae2640f74b5be965ddfb738774380f621c30e Mon Sep 17 00:00:00 2001 From: Eric Edens Date: Fri, 1 Aug 2014 12:02:51 -0700 Subject: [PATCH 09/18] Bug 1042958 - Launcher icon for search activity. r=margaret --- .../SearchAndroidManifest_activities.xml.in | 2 ++ .../res/drawable-hdpi/search_launcher.png | Bin 0 -> 7588 bytes .../res/drawable-mdpi/search_launcher.png | Bin 0 -> 5446 bytes .../res/drawable-xhdpi/search_launcher.png | Bin 0 -> 10210 bytes .../res/drawable-xxhdpi/search_launcher.png | Bin 0 -> 16073 bytes .../res/drawable-xxxhdpi/search_launcher.png | Bin 0 -> 23130 bytes 6 files changed, 2 insertions(+) create mode 100644 mobile/android/search/res/drawable-hdpi/search_launcher.png create mode 100644 mobile/android/search/res/drawable-mdpi/search_launcher.png create mode 100644 mobile/android/search/res/drawable-xhdpi/search_launcher.png create mode 100644 mobile/android/search/res/drawable-xxhdpi/search_launcher.png create mode 100644 mobile/android/search/res/drawable-xxxhdpi/search_launcher.png diff --git a/mobile/android/search/manifests/SearchAndroidManifest_activities.xml.in b/mobile/android/search/manifests/SearchAndroidManifest_activities.xml.in index 672e6353b9d6..3d247e4207f2 100644 --- a/mobile/android/search/manifests/SearchAndroidManifest_activities.xml.in +++ b/mobile/android/search/manifests/SearchAndroidManifest_activities.xml.in @@ -1,5 +1,6 @@ @@ -37,6 +38,7 @@ diff --git a/mobile/android/search/res/drawable-hdpi/search_launcher.png b/mobile/android/search/res/drawable-hdpi/search_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5895a54fd721e75d8db4ae84e806f05f87bc969 GIT binary patch literal 7588 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 literal 0 HcmV?d00001 diff --git a/mobile/android/search/res/drawable-mdpi/search_launcher.png b/mobile/android/search/res/drawable-mdpi/search_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..a8c874b14236dd55ac11b72aa02e331989b2e572 GIT binary patch literal 5446 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 literal 0 HcmV?d00001 diff --git a/mobile/android/search/res/drawable-xxxhdpi/search_launcher.png b/mobile/android/search/res/drawable-xxxhdpi/search_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..007296cda752d37aaad72591c8666387652e2cfa GIT binary patch literal 23130 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 M>M*rR70a;y1EsUR?EnA( literal 0 HcmV?d00001 From 4bffe7ee81736cec77c234f757645b86904679bd Mon Sep 17 00:00:00 2001 From: Eric Edens Date: Thu, 31 Jul 2014 16:14:12 -0700 Subject: [PATCH 10/18] Bug 992963 - Enable search activity in Nightly builds. r=margaret --- mobile/android/confvars.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mobile/android/confvars.sh b/mobile/android/confvars.sh index f95aadf1d450..359b10aeed56 100644 --- a/mobile/android/confvars.sh +++ b/mobile/android/confvars.sh @@ -79,8 +79,12 @@ MOZ_NATIVE_DEVICES= # Mark as WebGL conformant MOZ_WEBGL_CONFORMANT=1 -# Don't enable the Search Activity. -# MOZ_ANDROID_SEARCH_ACTIVITY=1 +# Enable the Search Activity in nightly. +if test "$NIGHTLY_BUILD"; then + MOZ_ANDROID_SEARCH_ACTIVITY=1 +else + MOZ_ANDROID_SEARCH_ACTIVITY= +fi # Don't enable the Mozilla Location Service stumbler. # MOZ_ANDROID_MLS_STUMBLER=1 From ad82f817e0649da6e75c29e2aa6aa7d3812c03b2 Mon Sep 17 00:00:00 2001 From: Steven MacLeod Date: Fri, 1 Aug 2014 16:28:07 -0400 Subject: [PATCH 11/18] Bug 1027181 - Call click() instead of syntehsizing mouse events in customizableui test to ensure subview opens. r=mconley --HG-- extra : rebase_source : 17b71f4b8a15cbbbe51adac514e4038afa1f2ca6 --- .../test/browser_946320_tabs_from_other_computers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/browser/components/customizableui/test/browser_946320_tabs_from_other_computers.js b/browser/components/customizableui/test/browser_946320_tabs_from_other_computers.js index 5bf97b994b87..350508e516c9 100644 --- a/browser/components/customizableui/test/browser_946320_tabs_from_other_computers.js +++ b/browser/components/customizableui/test/browser_946320_tabs_from_other_computers.js @@ -19,7 +19,7 @@ add_task(function() { let historyButton = document.getElementById("history-panelmenu"); let historySubview = document.getElementById("PanelUI-history"); let subviewShownPromise = subviewShown(historySubview); - EventUtils.synthesizeMouseAtCenter(historyButton, {}); + historyButton.click(); yield subviewShownPromise; let tabsFromOtherComputers = document.getElementById("sync-tabs-menuitem2"); @@ -34,7 +34,7 @@ add_task(function() { yield PanelUI.show({type: "command"}); subviewShownPromise = subviewShown(historySubview); - EventUtils.synthesizeMouseAtCenter(historyButton, {}); + historyButton.click(); yield subviewShownPromise; is(tabsFromOtherComputers.hidden, false, "The Tabs From Other Computers menuitem should be shown when sync is enabled."); From 2ae80bb460b8b0427914f97c406410725de6cb69 Mon Sep 17 00:00:00 2001 From: Richard Newman Date: Fri, 1 Aug 2014 15:07:00 -0700 Subject: [PATCH 12/18] Bug 1046369 - Add architecture to logged library load errors. r=blassey --- .../android/base/mozglue/GeckoLoader.java.in | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/mobile/android/base/mozglue/GeckoLoader.java.in b/mobile/android/base/mozglue/GeckoLoader.java.in index f38fea61c02e..26a58cea5221 100644 --- a/mobile/android/base/mozglue/GeckoLoader.java.in +++ b/mobile/android/base/mozglue/GeckoLoader.java.in @@ -21,8 +21,9 @@ import java.util.Locale; public final class GeckoLoader { private static final String LOGTAG = "GeckoLoader"; - // This matches AppConstants, but we're built earlier. + // These match AppConstants, but we're built earlier. private static final String ANDROID_PACKAGE_NAME = "@ANDROID_PACKAGE_NAME@"; + private static final String MOZ_APP_ABI = "@MOZ_APP_ABI@"; private static volatile Intent sIntent; private static File sCacheFile; @@ -264,6 +265,8 @@ public final class GeckoLoader { final StringBuilder message = new StringBuilder("LOAD "); message.append(lib); + // These might differ. If so, we know why the library won't load! + message.append(": ABI: " + MOZ_APP_ABI + ", " + android.os.Build.CPU_ABI); message.append(": Data: " + context.getApplicationInfo().dataDir); try { final boolean appLibExists = new File("/data/app-lib/" + ANDROID_PACKAGE_NAME + "/lib" + lib + ".so").exists(); @@ -336,9 +339,17 @@ public final class GeckoLoader { // Attempt 2: use nativeLibraryDir, which should also work. final String libDir = context.getApplicationInfo().nativeLibraryDir; - if (attemptLoad(libDir + "/lib" + lib + ".so")) { - // Success! - return null; + final String libPath = libDir + "/lib" + lib + ".so"; + + // Does it even exist? + if (new File(libPath).exists()) { + if (attemptLoad(libPath)) { + // Success! + return null; + } + Log.wtf(LOGTAG, "Library exists but couldn't load!"); + } else { + Log.wtf(LOGTAG, "Library doesn't exist when it should."); } // We failed. Return the original cause. From 7c619701284cbdde904b794b346042c731a6a7e3 Mon Sep 17 00:00:00 2001 From: Andrei Oprea Date: Fri, 1 Aug 2014 16:36:39 -0700 Subject: [PATCH 13/18] Bug 1000127 - Implement new standalone UI link clicker, r=dmose --- .../components/loop/content/conversation.html | 1 + .../loop/content/js/conversation.js | 23 +-- .../loop/content/js/conversation.jsx | 23 +-- browser/components/loop/content/js/panel.js | 66 +++---- .../loop/content/shared/css/common.css | 37 +++- .../loop/content/shared/img/firefox-logo.png | Bin 0 -> 58280 bytes .../loop/content/shared/img/mozilla-logo.png | Bin 0 -> 3142 bytes .../loop/content/shared/js/utils.js | 35 ++++ browser/components/loop/jar.mn | 1 + .../loop/standalone/content/css/webapp.css | 115 +++++++++++ .../loop/standalone/content/index.html | 20 +- .../standalone/content/js/standaloneClient.js | 21 ++ .../loop/standalone/content/js/webapp.js | 187 +++++++++++++----- .../loop/standalone/content/js/webapp.jsx | 187 +++++++++++++----- .../loop/standalone/content/l10n/data.ini | 12 ++ .../loop/test/desktop-local/index.html | 1 + .../loop/test/standalone/index.html | 1 + .../test/standalone/standalone_client_test.js | 59 ++++++ .../loop/test/standalone/webapp_test.js | 92 ++++++--- 19 files changed, 680 insertions(+), 201 deletions(-) create mode 100644 browser/components/loop/content/shared/img/firefox-logo.png create mode 100644 browser/components/loop/content/shared/img/mozilla-logo.png create mode 100644 browser/components/loop/content/shared/js/utils.js diff --git a/browser/components/loop/content/conversation.html b/browser/components/loop/content/conversation.html index 903a70d1ed7d..ad67d184168a 100644 --- a/browser/components/loop/content/conversation.html +++ b/browser/components/loop/content/conversation.html @@ -31,6 +31,7 @@ + diff --git a/browser/components/loop/content/js/conversation.js b/browser/components/loop/content/js/conversation.js index 196014b45f61..858f75d88039 100644 --- a/browser/components/loop/content/js/conversation.js +++ b/browser/components/loop/content/js/conversation.js @@ -48,26 +48,6 @@ loop.conversation = (function(OT, mozL10n) { } }, - /** - * Used for adding different styles to the panel - * @returns {String} Corresponds to the client platform - * */ - _getTargetPlatform: function() { - var platform="unknown_platform"; - - if (navigator.platform.indexOf("Win") !== -1) { - platform = "windows"; - } - if (navigator.platform.indexOf("Mac") !== -1) { - platform = "mac"; - } - if (navigator.platform.indexOf("Linux") !== -1) { - platform = "linux"; - } - - return platform; - }, - _handleAccept: function() { this.props.model.trigger("accept"); }, @@ -97,7 +77,8 @@ loop.conversation = (function(OT, mozL10n) { var btnClassAccept = "btn btn-success btn-accept"; var btnClassBlock = "btn btn-error btn-block"; var btnClassDecline = "btn btn-error btn-decline"; - var conversationPanelClass = "incoming-call " + this._getTargetPlatform(); + var conversationPanelClass = "incoming-call " + + loop.shared.utils.getTargetPlatform(); var cx = React.addons.classSet; var declineDropdownMenuClasses = cx({ "native-dropdown-menu": true, diff --git a/browser/components/loop/content/js/conversation.jsx b/browser/components/loop/content/js/conversation.jsx index 1ba92f4df631..873cb3738f10 100644 --- a/browser/components/loop/content/js/conversation.jsx +++ b/browser/components/loop/content/js/conversation.jsx @@ -48,26 +48,6 @@ loop.conversation = (function(OT, mozL10n) { } }, - /** - * Used for adding different styles to the panel - * @returns {String} Corresponds to the client platform - * */ - _getTargetPlatform: function() { - var platform="unknown_platform"; - - if (navigator.platform.indexOf("Win") !== -1) { - platform = "windows"; - } - if (navigator.platform.indexOf("Mac") !== -1) { - platform = "mac"; - } - if (navigator.platform.indexOf("Linux") !== -1) { - platform = "linux"; - } - - return platform; - }, - _handleAccept: function() { this.props.model.trigger("accept"); }, @@ -97,7 +77,8 @@ loop.conversation = (function(OT, mozL10n) { var btnClassAccept = "btn btn-success btn-accept"; var btnClassBlock = "btn btn-error btn-block"; var btnClassDecline = "btn btn-error btn-decline"; - var conversationPanelClass = "incoming-call " + this._getTargetPlatform(); + var conversationPanelClass = "incoming-call " + + loop.shared.utils.getTargetPlatform(); var cx = React.addons.classSet; var declineDropdownMenuClasses = cx({ "native-dropdown-menu": true, diff --git a/browser/components/loop/content/js/panel.js b/browser/components/loop/content/js/panel.js index 007e894cb6e4..48df19b14096 100644 --- a/browser/components/loop/content/js/panel.js +++ b/browser/components/loop/content/js/panel.js @@ -77,22 +77,22 @@ loop.panel = (function(_, mozL10n) { __("display_name_available_status"); return ( - React.DOM.div( {className:"footer component-spacer"}, - React.DOM.div( {className:"do-not-disturb"}, - React.DOM.p( {className:"dnd-status", onClick:this.showDropdownMenu}, - React.DOM.span(null, availabilityText), - React.DOM.i( {className:availabilityStatus}) - ), - React.DOM.ul( {className:availabilityDropdown, - onMouseLeave:this.hideDropdownMenu}, - React.DOM.li( {onClick:this.changeAvailability("available"), - className:"dnd-menu-item dnd-make-available"}, - React.DOM.i( {className:"status status-available"}), + React.DOM.div({className: "footer component-spacer"}, + React.DOM.div({className: "do-not-disturb"}, + React.DOM.p({className: "dnd-status", onClick: this.showDropdownMenu}, + React.DOM.span(null, availabilityText), + React.DOM.i({className: availabilityStatus}) + ), + React.DOM.ul({className: availabilityDropdown, + onMouseLeave: this.hideDropdownMenu}, + React.DOM.li({onClick: this.changeAvailability("available"), + className: "dnd-menu-item dnd-make-available"}, + React.DOM.i({className: "status status-available"}), React.DOM.span(null, __("display_name_available_status")) - ), - React.DOM.li( {onClick:this.changeAvailability("do-not-disturb"), - className:"dnd-menu-item dnd-make-unavailable"}, - React.DOM.i( {className:"status status-dnd"}), + ), + React.DOM.li({onClick: this.changeAvailability("do-not-disturb"), + className: "dnd-menu-item dnd-make-unavailable"}, + React.DOM.i({className: "status status-dnd"}), React.DOM.span(null, __("display_name_dnd_status")) ) ) @@ -115,10 +115,10 @@ loop.panel = (function(_, mozL10n) { if (this.state.seenToS == "unseen") { navigator.mozLoop.setLoopCharPref('seenToS', 'seen'); - return React.DOM.p( {className:"terms-service", - dangerouslySetInnerHTML:{__html: tosHTML}}); + return React.DOM.p({className: "terms-service", + dangerouslySetInnerHTML: {__html: tosHTML}}); } else { - return React.DOM.div(null ); + return React.DOM.div(null); } } }); @@ -130,11 +130,11 @@ loop.panel = (function(_, mozL10n) { render: function() { return ( - React.DOM.div( {className:"component-spacer share generate-url"}, - React.DOM.div( {className:"description"}, - React.DOM.p( {className:"description-content"}, this.props.summary) - ), - React.DOM.div( {className:"action"}, + React.DOM.div({className: "component-spacer share generate-url"}, + React.DOM.div({className: "description"}, + React.DOM.p({className: "description-content"}, this.props.summary) + ), + React.DOM.div({className: "action"}, this.props.children ) ) @@ -201,10 +201,10 @@ loop.panel = (function(_, mozL10n) { // from the react lib. var cx = React.addons.classSet; return ( - PanelLayout( {summary:__("share_link_header_text")}, - React.DOM.div( {className:"invite"}, - React.DOM.input( {type:"url", value:this.state.callUrl, readOnly:"true", - className:cx({'pending': this.state.pending})} ) + PanelLayout({summary: __("share_link_header_text")}, + React.DOM.div({className: "invite"}, + React.DOM.input({type: "url", value: this.state.callUrl, readOnly: "true", + className: cx({'pending': this.state.pending})}) ) ) ); @@ -223,10 +223,10 @@ loop.panel = (function(_, mozL10n) { render: function() { return ( React.DOM.div(null, - CallUrlResult( {client:this.props.client, - notifier:this.props.notifier} ), - ToSView(null ), - AvailabilityDropdown(null ) + CallUrlResult({client: this.props.client, + notifier: this.props.notifier}), + ToSView(null), + AvailabilityDropdown(null) ) ); } @@ -293,8 +293,8 @@ loop.panel = (function(_, mozL10n) { var client = new loop.Client({ baseServerUrl: navigator.mozLoop.serverUrl }); - this.loadReactComponent(PanelView( {client:client, - notifier:this._notifier} )); + this.loadReactComponent(PanelView({client: client, + notifier: this._notifier})); } }); diff --git a/browser/components/loop/content/shared/css/common.css b/browser/components/loop/content/shared/css/common.css index a4b29eb7e1f1..7b14bc602556 100644 --- a/browser/components/loop/content/shared/css/common.css +++ b/browser/components/loop/content/shared/css/common.css @@ -96,7 +96,20 @@ h1, h2, h3 { } .btn-large { - padding: .4em 1.6em; + /* Dimensions from spec + * https://people.mozilla.org/~dhenein/labs/loop-link-spec/#call-start */ + padding: .5em; + font-size: 18px; + height: auto; +} + +/* + * Left / Right padding elements + * used to center components + * */ +.flex-padding-1 { + display: flex; + flex: 1; } .btn-info { @@ -209,6 +222,8 @@ h1, h2, h3 { .button-group { display: flex; width: 100%; + align-content: space-between; + justify-content: center; } .button-group .btn { @@ -271,6 +286,26 @@ h1, h2, h3 { opacity: 0; } +.btn-large .icon { + display: inline-block; + width: 20px; + height: 20px; + background-size: 20px; + background-repeat: no-repeat; + vertical-align: top; + margin-left: 10px; +} + +.icon-video { + background-image: url("../img/video-inverse-14x14.png"); +} + +@media (min-resolution: 2dppx) { + .icon-video { + background-image: url("../img/video-inverse-14x14@2x.png"); + } +} + /* * Platform specific styles * The UI should match the user OS diff --git a/browser/components/loop/content/shared/img/firefox-logo.png b/browser/components/loop/content/shared/img/firefox-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ea8387ff19a0533c4b54ca9a3bb04c0997fef6ff GIT binary patch literal 58280 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

XC;)L?q^KS@6c}bifDPA8+81dc-RML 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 gvOvlL7mWq}2aqF_=hBA-!T - + - - -

-

Loop

-
- -
+
@@ -39,6 +33,7 @@ + @@ -46,11 +41,10 @@ diff --git a/browser/components/loop/standalone/content/js/standaloneClient.js b/browser/components/loop/standalone/content/js/standaloneClient.js index fe86cd8c72fe..e5f3746c7380 100644 --- a/browser/components/loop/standalone/content/js/standaloneClient.js +++ b/browser/components/loop/standalone/content/js/standaloneClient.js @@ -75,6 +75,27 @@ loop.StandaloneClient = (function($) { cb(err); }, + /** + * Makes a request for url creation date for standalone UI + * + * @param {String} loopToken The loopToken representing the call + * @param {Function} cb Callback(err, callUrlInfo) + * + **/ + requestCallUrlInfo: function(loopToken, cb) { + if (!loopToken) { + throw new Error("Missing required parameter loopToken"); + } + if (!cb) { + throw new Error("Missing required callback function"); + } + + $.get(this.settings.baseServerUrl + "/calls/" + loopToken) + .done(function(callUrlInfo) { + cb(null, callUrlInfo); + }).fail(this._failureHandler.bind(this, cb)); + }, + /** * Posts a call request to the server for a call represented by the * loopToken. Will return the session data for the call. diff --git a/browser/components/loop/standalone/content/js/webapp.js b/browser/components/loop/standalone/content/js/webapp.js index 1f957f3df927..c6bab1e99ccd 100644 --- a/browser/components/loop/standalone/content/js/webapp.js +++ b/browser/components/loop/standalone/content/js/webapp.js @@ -82,23 +82,56 @@ loop.webapp = (function($, _, OT, webL10n) { } }); + var ConversationHeader = React.createClass({displayName: 'ConversationHeader', + render: function() { + var cx = React.addons.classSet; + var conversationUrl = location.href; + + var urlCreationDateClasses = cx({ + "light-color-font": true, + "call-url-date": true, /* Used as a handler in the tests */ + /*hidden until date is available*/ + "hide": !this.props.urlCreationDateString.length + }); + + var callUrlCreationDateString = __("call_url_creation_date_label", { + "call_url_creation_date": this.props.urlCreationDateString + }); + + return ( + /* jshint ignore:start */ + React.DOM.header({className: "container-box"}, + React.DOM.h1({className: "light-weight-font"}, + React.DOM.strong(null, __("brandShortname")), " ", __("clientShortname") + ), + React.DOM.div({className: "loop-logo", title: "Firefox WebRTC! logo"}), + React.DOM.h3({className: "call-url"}, + conversationUrl + ), + React.DOM.h4({className: urlCreationDateClasses}, + callUrlCreationDateString + ) + ) + /* jshint ignore:end */ + ); + } + }); + + var ConversationFooter = React.createClass({displayName: 'ConversationFooter', + render: function() { + return ( + React.DOM.div({className: "footer container-box"}, + React.DOM.div({title: "Mozilla Logo", className: "footer-logo"}) + ) + ); + } + }); + /** * Conversation launcher view. A ConversationModel is associated and attached * as a `model` property. */ - var ConversationFormView = sharedViews.BaseView.extend({ - template: _.template([ - '
', - '

', - ' ', - '

', - '
' - ].join("")), - - events: { - "submit": "initiate" - }, - + var ConversationFormView = React.createClass({displayName: 'ConversationFormView', /** * Constructor. * @@ -106,55 +139,115 @@ loop.webapp = (function($, _, OT, webL10n) { * - {loop.shared.model.ConversationModel} model Conversation model. * - {loop.shared.views.NotificationListView} notifier Notifier component. * - * @param {Object} options Options object. */ - initialize: function(options) { - options = options || {}; - if (!options.model) { - throw new Error("missing required model"); - } - this.model = options.model; + getInitialState: function() { + return { + urlCreationDateString: '', + disableCallButton: false + }; + }, - if (!options.notifier) { - throw new Error("missing required notifier"); - } - this.notifier = options.notifier; + propTypes: { + model: React.PropTypes.instanceOf(sharedModels.ConversationModel) + .isRequired, + // XXX Check more tightly here when we start injecting window.loop.* + notifier: React.PropTypes.object.isRequired, + client: React.PropTypes.object.isRequired + }, - this.listenTo(this.model, "session:error", this._onSessionError); + componentDidMount: function() { + this.props.model.listenTo(this.props.model, "session:error", + this._onSessionError); + this.props.client.requestCallUrlInfo(this.props.model.get("loopToken"), + this._setConversationTimestamp); + // XXX DOM element does not exist before React view gets instantiated + // We should turn the notifier into a react component + this.props.notifier.$el = $("#messages"); }, _onSessionError: function(error) { console.error(error); - this.notifier.errorL10n("unable_retrieve_call_info"); - }, - - /** - * Disables this form to prevent multiple submissions. - * - * @see https://bugzilla.mozilla.org/show_bug.cgi?id=991126 - */ - disableForm: function() { - this.$("button").attr("disabled", "disabled"); + this.props.notifier.errorL10n("unable_retrieve_call_info"); }, /** * Initiates the call. - * - * @param {SubmitEvent} event */ - initiate: function(event) { - event.preventDefault(); - this.model.initiate({ + _initiate: function() { + this.props.model.initiate({ client: new loop.StandaloneClient({ baseServerUrl: baseServerUrl }), outgoing: true, // For now, we assume both audio and video as there is no // other option to select. - callType: "audio-video" + callType: "audio-video", + loopServer: loop.config.serverUrl }); - this.disableForm(); + + this.setState({disableCallButton: true}); + }, + + _setConversationTimestamp: function(err, callUrlInfo) { + if (err) { + this.props.notifier.errorL10n("unable_retrieve_call_info"); + } else { + var date = (new Date(callUrlInfo.urlCreationDate * 1000)); + var options = {year: "numeric", month: "long", day: "numeric"}; + var timestamp = date.toLocaleDateString(navigator.language, options); + + this.setState({urlCreationDateString: timestamp}); + } + }, + + render: function() { + var tos_link_name = __("terms_of_use_link_text"); + var privacy_notice_name = __("privacy_notice_link_text"); + + var tosHTML = __("legal_text_and_links", { + "terms_of_use_url": "" + tos_link_name + "", + "privacy_notice_url": "" + privacy_notice_name + "" + }); + + var callButtonClasses = "btn btn-success btn-large " + + loop.shared.utils.getTargetPlatform(); + + return ( + /* jshint ignore:start */ + React.DOM.div({className: "container"}, + React.DOM.div({className: "container-box"}, + + ConversationHeader({ + urlCreationDateString: this.state.urlCreationDateString}), + + React.DOM.p({className: "large-font light-weight-font"}, + __("initiate_call_button_label") + ), + + React.DOM.div({id: "messages"}), + + React.DOM.div({className: "button-group"}, + React.DOM.div({className: "flex-padding-1"}), + React.DOM.button({ref: "submitButton", onClick: this._initiate, + className: callButtonClasses, + disabled: this.state.disableCallButton}, + __("initiate_call_button"), + React.DOM.i({className: "icon icon-video"}) + ), + React.DOM.div({className: "flex-padding-1"}) + ), + + React.DOM.p({className: "terms-service", + dangerouslySetInnerHTML: {__html: tosHTML}}) + ), + + ConversationFooter(null) + ) + /* jshint ignore:end */ + ); } }); @@ -250,9 +343,12 @@ loop.webapp = (function($, _, OT, webL10n) { this._conversation.endSession(); } this._conversation.set("loopToken", loopToken); - this.loadView(new ConversationFormView({ + this.loadReactComponent(ConversationFormView({ model: this._conversation, - notifier: this._notifier + notifier: this._notifier, + client: new loop.StandaloneClient({ + baseServerUrl: loop.config.serverUrl + }) })); }, @@ -308,6 +404,9 @@ loop.webapp = (function($, _, OT, webL10n) { } else if (!OT.checkSystemRequirements()) { router.navigate("unsupportedBrowser", {trigger: true}); } + // Set the 'lang' and 'dir' attributes to when the page is translated + document.documentElement.lang = document.webL10n.getLanguage(); + document.documentElement.dir = document.webL10n.getDirection(); } return { diff --git a/browser/components/loop/standalone/content/js/webapp.jsx b/browser/components/loop/standalone/content/js/webapp.jsx index 8d71253c192e..d976934437b1 100644 --- a/browser/components/loop/standalone/content/js/webapp.jsx +++ b/browser/components/loop/standalone/content/js/webapp.jsx @@ -82,23 +82,56 @@ loop.webapp = (function($, _, OT, webL10n) { } }); + var ConversationHeader = React.createClass({ + render: function() { + var cx = React.addons.classSet; + var conversationUrl = location.href; + + var urlCreationDateClasses = cx({ + "light-color-font": true, + "call-url-date": true, /* Used as a handler in the tests */ + /*hidden until date is available*/ + "hide": !this.props.urlCreationDateString.length + }); + + var callUrlCreationDateString = __("call_url_creation_date_label", { + "call_url_creation_date": this.props.urlCreationDateString + }); + + return ( + /* jshint ignore:start */ +
+

+ {__("brandShortname")} {__("clientShortname")} +

+
+

+ {conversationUrl} +

+

+ {callUrlCreationDateString} +

+
+ /* jshint ignore:end */ + ); + } + }); + + var ConversationFooter = React.createClass({ + render: function() { + return ( +
+
+
+ ); + } + }); + /** * Conversation launcher view. A ConversationModel is associated and attached * as a `model` property. */ - var ConversationFormView = sharedViews.BaseView.extend({ - template: _.template([ - '
', - '

', - ' ', - '

', - '
' - ].join("")), - - events: { - "submit": "initiate" - }, - + var ConversationFormView = React.createClass({ /** * Constructor. * @@ -106,55 +139,115 @@ loop.webapp = (function($, _, OT, webL10n) { * - {loop.shared.model.ConversationModel} model Conversation model. * - {loop.shared.views.NotificationListView} notifier Notifier component. * - * @param {Object} options Options object. */ - initialize: function(options) { - options = options || {}; - if (!options.model) { - throw new Error("missing required model"); - } - this.model = options.model; + getInitialState: function() { + return { + urlCreationDateString: '', + disableCallButton: false + }; + }, - if (!options.notifier) { - throw new Error("missing required notifier"); - } - this.notifier = options.notifier; + propTypes: { + model: React.PropTypes.instanceOf(sharedModels.ConversationModel) + .isRequired, + // XXX Check more tightly here when we start injecting window.loop.* + notifier: React.PropTypes.object.isRequired, + client: React.PropTypes.object.isRequired + }, - this.listenTo(this.model, "session:error", this._onSessionError); + componentDidMount: function() { + this.props.model.listenTo(this.props.model, "session:error", + this._onSessionError); + this.props.client.requestCallUrlInfo(this.props.model.get("loopToken"), + this._setConversationTimestamp); + // XXX DOM element does not exist before React view gets instantiated + // We should turn the notifier into a react component + this.props.notifier.$el = $("#messages"); }, _onSessionError: function(error) { console.error(error); - this.notifier.errorL10n("unable_retrieve_call_info"); - }, - - /** - * Disables this form to prevent multiple submissions. - * - * @see https://bugzilla.mozilla.org/show_bug.cgi?id=991126 - */ - disableForm: function() { - this.$("button").attr("disabled", "disabled"); + this.props.notifier.errorL10n("unable_retrieve_call_info"); }, /** * Initiates the call. - * - * @param {SubmitEvent} event */ - initiate: function(event) { - event.preventDefault(); - this.model.initiate({ + _initiate: function() { + this.props.model.initiate({ client: new loop.StandaloneClient({ baseServerUrl: baseServerUrl }), outgoing: true, // For now, we assume both audio and video as there is no // other option to select. - callType: "audio-video" + callType: "audio-video", + loopServer: loop.config.serverUrl }); - this.disableForm(); + + this.setState({disableCallButton: true}); + }, + + _setConversationTimestamp: function(err, callUrlInfo) { + if (err) { + this.props.notifier.errorL10n("unable_retrieve_call_info"); + } else { + var date = (new Date(callUrlInfo.urlCreationDate * 1000)); + var options = {year: "numeric", month: "long", day: "numeric"}; + var timestamp = date.toLocaleDateString(navigator.language, options); + + this.setState({urlCreationDateString: timestamp}); + } + }, + + render: function() { + var tos_link_name = __("terms_of_use_link_text"); + var privacy_notice_name = __("privacy_notice_link_text"); + + var tosHTML = __("legal_text_and_links", { + "terms_of_use_url": "" + tos_link_name + "", + "privacy_notice_url": "" + privacy_notice_name + "" + }); + + var callButtonClasses = "btn btn-success btn-large " + + loop.shared.utils.getTargetPlatform(); + + return ( + /* jshint ignore:start */ +
+
+ + + +

+ {__("initiate_call_button_label")} +

+ +
+ +
+
+ +
+
+ +

+
+ + +
+ /* jshint ignore:end */ + ); } }); @@ -250,9 +343,12 @@ loop.webapp = (function($, _, OT, webL10n) { this._conversation.endSession(); } this._conversation.set("loopToken", loopToken); - this.loadView(new ConversationFormView({ + this.loadReactComponent(ConversationFormView({ model: this._conversation, - notifier: this._notifier + notifier: this._notifier, + client: new loop.StandaloneClient({ + baseServerUrl: loop.config.serverUrl + }) })); }, @@ -308,6 +404,9 @@ loop.webapp = (function($, _, OT, webL10n) { } else if (!OT.checkSystemRequirements()) { router.navigate("unsupportedBrowser", {trigger: true}); } + // Set the 'lang' and 'dir' attributes to when the page is translated + document.documentElement.lang = document.webL10n.getLanguage(); + document.documentElement.dir = document.webL10n.getDirection(); } return { diff --git a/browser/components/loop/standalone/content/l10n/data.ini b/browser/components/loop/standalone/content/l10n/data.ini index fa3f729581fb..1f1acb9b9d9d 100644 --- a/browser/components/loop/standalone/content/l10n/data.ini +++ b/browser/components/loop/standalone/content/l10n/data.ini @@ -23,6 +23,18 @@ call_url_unavailable_notification_heading=Oops! call_url_unavailable_notification_message=This URL is unavailable. promote_firefox_hello_heading=Download Firefox to make free audio and video calls! get_firefox_button=Get Firefox +call_url_unavailable_notification=This URL is unavailable. +initiate_call_button_label=Click Call to start a video chat +initiate_call_button=Call +## LOCALIZATION NOTE (legal_text_and_links): In this item, don't translate the +## part between {{..}} +legal_text_and_links=By using this product you agree to the {{terms_of_use_url}} and {{privacy_notice_url}} +terms_of_use_link_text=Terms of use +privacy_notice_link_text=Privacy notice +brandShortname=Firefox +clientShortname=WebRTC! +## LOCALIZATION NOTE (call_url_creation_date_label): Example output: (from May 26, 2014) +call_url_creation_date_label=(from {{call_url_creation_date}}) [fr] call_has_ended=L'appel est terminé. diff --git a/browser/components/loop/test/desktop-local/index.html b/browser/components/loop/test/desktop-local/index.html index 04119df6613b..249fc82e409e 100644 --- a/browser/components/loop/test/desktop-local/index.html +++ b/browser/components/loop/test/desktop-local/index.html @@ -32,6 +32,7 @@ + diff --git a/browser/components/loop/test/standalone/index.html b/browser/components/loop/test/standalone/index.html index cd1e82f33608..79e5fc3eb819 100644 --- a/browser/components/loop/test/standalone/index.html +++ b/browser/components/loop/test/standalone/index.html @@ -31,6 +31,7 @@ mocha.setup('bdd'); + diff --git a/browser/components/loop/test/standalone/standalone_client_test.js b/browser/components/loop/test/standalone/standalone_client_test.js index cabc88f3dc67..843c46cc73fa 100644 --- a/browser/components/loop/test/standalone/standalone_client_test.js +++ b/browser/components/loop/test/standalone/standalone_client_test.js @@ -40,6 +40,65 @@ describe("loop.StandaloneClient", function() { }); }); + describe("#requestCallUrlInfo", function() { + var client, fakeServerErrorDescription; + + beforeEach(function() { + client = new loop.StandaloneClient( + {baseServerUrl: "http://fake.api"} + ); + }); + + describe("should make the requests to the server", function() { + + it("should throw if loopToken is missing", function() { + expect(client.requestCallUrlInfo).to + .throw(/Missing required parameter loopToken/); + }); + + it("should make a GET request for the call url creation date", function() { + client.requestCallUrlInfo("fakeCallUrlToken", function() {}); + + expect(requests).to.have.length.of(1); + expect(requests[0].url) + .to.eql("http://fake.api/calls/fakeCallUrlToken"); + expect(requests[0].method).to.eql("GET"); + }); + + it("should call the callback with (null, serverResponse)", function() { + var successCallback = sandbox.spy(function() {}); + var serverResponse = { + calleeFriendlyName: "Andrei", + urlCreationDate: 0 + }; + + client.requestCallUrlInfo("fakeCallUrlToken", successCallback); + requests[0].respond(200, {"Content-Type": "application/json"}, + JSON.stringify(serverResponse)); + + sinon.assert.calledWithExactly(successCallback, + null, + serverResponse); + }); + + it("should log the error if the requests fails", function() { + sinon.stub(console, "error"); + var serverResponse = {error: true}; + var error = JSON.stringify(serverResponse); + + client.requestCallUrlInfo("fakeCallUrlToken", sandbox.stub()); + requests[0].respond(404, {"Content-Type": "application/json"}, + error); + + sinon.assert.calledOnce(console.error); + sinon.assert.calledWithExactly(console.error, "Server error", + "HTTP 404 Not Found", serverResponse); + }); + }) + }); + + + describe("requestCallInfo", function() { var client, fakeServerErrorDescription; diff --git a/browser/components/loop/test/standalone/webapp_test.js b/browser/components/loop/test/standalone/webapp_test.js index fa5ae49b167b..c1f51167076e 100644 --- a/browser/components/loop/test/standalone/webapp_test.js +++ b/browser/components/loop/test/standalone/webapp_test.js @@ -76,13 +76,13 @@ describe("loop.webapp", function() { sdk: {}, pendingCallTimeout: 1000 }); + sandbox.stub(loop.webapp.WebappRouter.prototype, "loadReactComponent"); router = new loop.webapp.WebappRouter({ helper: {}, conversation: conversation, notifier: notifier }); sandbox.stub(router, "loadView"); - sandbox.stub(router, "loadReactComponent"); sandbox.stub(router, "navigate"); }); @@ -165,9 +165,12 @@ describe("loop.webapp", function() { it("should load the ConversationFormView", function() { router.initiate("fakeToken"); - sinon.assert.calledOnce(router.loadView); - sinon.assert.calledWith(router.loadView, - sinon.match.instanceOf(loop.webapp.ConversationFormView)); + sinon.assert.calledOnce(router.loadReactComponent); + sinon.assert.calledWithExactly(router.loadReactComponent, + sinon.match(function(value) { + return React.addons.TestUtils.isComponentOfType( + value, loop.webapp.ConversationFormView); + })); }); // https://bugzilla.mozilla.org/show_bug.cgi?id=991118 @@ -295,47 +298,68 @@ describe("loop.webapp", function() { }); describe("#initiate", function() { - var conversation, initiate, view, fakeSubmitEvent; + var conversation, initiate, view, fakeSubmitEvent, requestCallUrlInfo; beforeEach(function() { conversation = new sharedModels.ConversationModel({}, { sdk: {}, pendingCallTimeout: 1000 }); - view = new loop.webapp.ConversationFormView({ - model: conversation, - notifier: notifier - }); + fakeSubmitEvent = {preventDefault: sinon.spy()}; initiate = sinon.stub(conversation, "initiate"); + + var standaloneClientStub = { + requestCallUrlInfo: function(token, cb) { + cb(null, {urlCreationDate: 0}); + }, + settings: {baseServerUrl: loop.webapp.baseServerUrl} + } + + view = React.addons.TestUtils.renderIntoDocument( + loop.webapp.ConversationFormView({ + model: conversation, + notifier: notifier, + client: standaloneClientStub + }) + ); }); it("should start the conversation establishment process", function() { - conversation.set("loopToken", "fake"); + var button = view.getDOMNode().querySelector("button"); + React.addons.TestUtils.Simulate.click(button); - view.initiate(fakeSubmitEvent); - - sinon.assert.calledOnce(fakeSubmitEvent.preventDefault); sinon.assert.calledOnce(initiate); sinon.assert.calledWith(initiate, sinon.match(function (value) { return !!value.outgoing && - (value.client instanceof loop.StandaloneClient) && - value.client.settings.baseServerUrl === loop.webapp.baseServerUrl; - }, "{client: , outgoing: true}")); + (value.client.settings.baseServerUrl === loop.webapp.baseServerUrl) + }, "outgoing: true && correct baseServerUrl")); }); it("should disable current form once session is initiated", function() { - sandbox.stub(view, "disableForm"); conversation.set("loopToken", "fake"); - view.initiate(fakeSubmitEvent); + var button = view.getDOMNode().querySelector("button"); + React.addons.TestUtils.Simulate.click(button); - sinon.assert.calledOnce(view.disableForm); + expect(button.disabled).to.eql(true); }); + + it("should set state.urlCreationDateString to a locale date string", + function() { + // wrap in a jquery object because text is broken up + // into several span elements + var date = new Date(0); + var options = {year: "numeric", month: "long", day: "numeric"}; + var timestamp = date.toLocaleDateString(navigator.language, options); + + expect(view.state.urlCreationDateString).to.eql(timestamp); + }); + }); describe("Events", function() { - var conversation, view; + var conversation, view, StandaloneClient, requestCallUrlInfo; beforeEach(function() { conversation = new sharedModels.ConversationModel({ @@ -344,10 +368,30 @@ describe("loop.webapp", function() { sdk: {}, pendingCallTimeout: 1000 }); - view = new loop.webapp.ConversationFormView({ - model: conversation, - notifier: notifier - }); + + sandbox.spy(conversation, "listenTo"); + requestCallUrlInfo = sandbox.stub(); + + view = React.addons.TestUtils.renderIntoDocument( + loop.webapp.ConversationFormView({ + model: conversation, + notifier: notifier, + client: {requestCallUrlInfo: requestCallUrlInfo} + }) + ); + }); + + it("should call requestCallUrlInfo", function() { + sinon.assert.calledOnce(requestCallUrlInfo); + sinon.assert.calledWithExactly(requestCallUrlInfo, + sinon.match.string, + sinon.match.func); + }); + + it("should listen for session:error events", function() { + sinon.assert.calledOnce(conversation.listenTo); + sinon.assert.calledWithExactly(conversation.listenTo, conversation, + "session:error", sinon.match.func); }); it("should trigger a notication when a session:error model event is " + From 1bb2d4325247f4a0bf50adad3284541d23a7bdba Mon Sep 17 00:00:00 2001 From: Tim Taubert Date: Wed, 30 Jul 2014 11:59:56 +0200 Subject: [PATCH 14/18] Bug 1041788 - Don't enter _beginRemoveTab() when a .permitUnload() call is pending r=dao --- browser/base/content/tabbrowser.xml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/browser/base/content/tabbrowser.xml b/browser/base/content/tabbrowser.xml index 3d19ca70a580..80b5d0450bf0 100644 --- a/browser/base/content/tabbrowser.xml +++ b/browser/base/content/tabbrowser.xml @@ -1913,6 +1913,7 @@ Date: Sat, 2 Aug 2014 11:16:23 +0200 Subject: [PATCH 15/18] Bug 952224 - Remove _ensureInitialized(), along with any calls made to it, now that synchronous start up fallback for session store has been removed r=smacleod --- .../components/sessionstore/nsSessionStartup.js | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/browser/components/sessionstore/nsSessionStartup.js b/browser/components/sessionstore/nsSessionStartup.js index cb45c4ba5b2a..3d6247b8492a 100644 --- a/browser/components/sessionstore/nsSessionStartup.js +++ b/browser/components/sessionstore/nsSessionStartup.js @@ -262,18 +262,15 @@ SessionStartup.prototype = { * Get the session state as a jsval */ get state() { - this._ensureInitialized(); return this._initialState; }, /** * Determines whether there is a pending session restore. Should only be * called after initialization has completed. - * @throws Error if initialization is not complete yet. * @returns bool */ doRestore: function sss_doRestore() { - this._ensureInitialized(); return this._willRestore(); }, @@ -324,7 +321,6 @@ SessionStartup.prototype = { * Get the type of pending session store, if any. */ get sessionType() { - this._ensureInitialized(); return this._sessionType; }, @@ -332,19 +328,9 @@ SessionStartup.prototype = { * Get whether the previous session crashed. */ get previousSessionCrashed() { - this._ensureInitialized(); return this._previousSessionCrashed; }, - // Ensure that initialization is complete. If initialization is not complete - // yet, something is attempting to use the old synchronous initialization, - // throw an error. - _ensureInitialized: function sss__ensureInitialized() { - if (!this._initialized) { - throw new Error("Session Store is not initialized."); - } - }, - /* ........ QueryInterface .............. */ QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference, From 89dedf4d2832f2d98186caaa55b8f3a999529042 Mon Sep 17 00:00:00 2001 From: Brad Lassey Date: Mon, 21 Jul 2014 14:57:28 -0400 Subject: [PATCH 16/18] bug 1041700 - add browserWindow and scrollWithPAge to media constraints r=jesup,jib,khuey --- .../webrtc/MediaEngineTabVideoSource.cpp | 67 ++++++++++++++----- .../media/webrtc/MediaEngineTabVideoSource.h | 2 + content/media/webrtc/MediaTrackConstraints.h | 7 +- dom/media/MediaManager.cpp | 17 +++++ dom/webidl/MediaTrackConstraintSet.webidl | 6 +- 5 files changed, 79 insertions(+), 20 deletions(-) diff --git a/content/media/webrtc/MediaEngineTabVideoSource.cpp b/content/media/webrtc/MediaEngineTabVideoSource.cpp index aac04ac578e1..7bd5cd995c34 100644 --- a/content/media/webrtc/MediaEngineTabVideoSource.cpp +++ b/content/media/webrtc/MediaEngineTabVideoSource.cpp @@ -1,4 +1,5 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ @@ -32,7 +33,7 @@ using dom::ConstrainLongRange; NS_IMPL_ISUPPORTS(MediaEngineTabVideoSource, nsIDOMEventListener, nsITimerCallback) MediaEngineTabVideoSource::MediaEngineTabVideoSource() -: mMonitor("MediaEngineTabVideoSource") +: mMonitor("MediaEngineTabVideoSource"), mTabSource(nullptr) { } @@ -42,7 +43,9 @@ MediaEngineTabVideoSource::StartRunnable::Run() mVideoSource->Draw(); mVideoSource->mTimer = do_CreateInstance(NS_TIMER_CONTRACTID); mVideoSource->mTimer->InitWithCallback(mVideoSource, mVideoSource->mTimePerFrame, nsITimer:: TYPE_REPEATING_SLACK); - mVideoSource->mTabSource->NotifyStreamStart(mVideoSource->mWindow); + if (mVideoSource->mTabSource) { + mVideoSource->mTabSource->NotifyStreamStart(mVideoSource->mWindow); + } return NS_OK; } @@ -55,7 +58,9 @@ MediaEngineTabVideoSource::StopRunnable::Run() mVideoSource->mTimer->Cancel(); mVideoSource->mTimer = nullptr; } - mVideoSource->mTabSource->NotifyStreamStop(mVideoSource->mWindow); + if (mVideoSource->mTabSource) { + mVideoSource->mTabSource->NotifyStreamStop(mVideoSource->mWindow); + } return NS_OK; } @@ -76,18 +81,25 @@ nsresult MediaEngineTabVideoSource::InitRunnable::Run() { mVideoSource->mData = (unsigned char*)malloc(mVideoSource->mBufW * mVideoSource->mBufH * 4); + if (mVideoSource->mWindowId != -1) { + nsCOMPtr window = nsGlobalWindow::GetOuterWindowWithId(mVideoSource->mWindowId); + if (window) { + mVideoSource->mWindow = window; + } + } + if (!mVideoSource->mWindow) { + nsresult rv; + mVideoSource->mTabSource = do_GetService(NS_TABSOURCESERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); - nsresult rv; - mVideoSource->mTabSource = do_GetService(NS_TABSOURCESERVICE_CONTRACTID, &rv); - NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr win; + rv = mVideoSource->mTabSource->GetTabToStream(getter_AddRefs(win)); + NS_ENSURE_SUCCESS(rv, rv); + if (!win) + return NS_OK; - nsCOMPtr win; - rv = mVideoSource->mTabSource->GetTabToStream(getter_AddRefs(win)); - NS_ENSURE_SUCCESS(rv, rv); - if (!win) - return NS_OK; - - mVideoSource->mWindow = win; + mVideoSource->mWindow = win; + } nsCOMPtr start(new StartRunnable(mVideoSource)); start->Run(); return NS_OK; @@ -113,13 +125,26 @@ MediaEngineTabVideoSource::Allocate(const VideoTrackConstraintsN& aConstraints, ConstrainLongRange cWidth(aConstraints.mRequired.mWidth); ConstrainLongRange cHeight(aConstraints.mRequired.mHeight); + mWindowId = aConstraints.mBrowserWindow.WasPassed() ? aConstraints.mBrowserWindow.Value() : -1; + bool haveScrollWithPage = aConstraints.mScrollWithPage.WasPassed(); + mScrollWithPage = haveScrollWithPage ? aConstraints.mScrollWithPage.Value() : true; + if (aConstraints.mAdvanced.WasPassed()) { const auto& advanced = aConstraints.mAdvanced.Value(); for (uint32_t i = 0; i < advanced.Length(); i++) { if (cWidth.mMax >= advanced[i].mWidth.mMin && cWidth.mMin <= advanced[i].mWidth.mMax && - cHeight.mMax >= advanced[i].mHeight.mMin && cHeight.mMin <= advanced[i].mHeight.mMax) { - cWidth.mMin = std::max(cWidth.mMin, advanced[i].mWidth.mMin); - cHeight.mMin = std::max(cHeight.mMin, advanced[i].mHeight.mMin); + cHeight.mMax >= advanced[i].mHeight.mMin && cHeight.mMin <= advanced[i].mHeight.mMax) { + cWidth.mMin = std::max(cWidth.mMin, advanced[i].mWidth.mMin); + cHeight.mMin = std::max(cHeight.mMin, advanced[i].mHeight.mMin); + } + + if (mWindowId == -1 && advanced[i].mBrowserWindow.WasPassed()) { + mWindowId = advanced[i].mBrowserWindow.Value(); + } + + if (!haveScrollWithPage && advanced[i].mScrollWithPage.WasPassed()) { + mScrollWithPage = advanced[i].mScrollWithPage.Value(); + haveScrollWithPage = true; } } } @@ -140,7 +165,6 @@ MediaEngineTabVideoSource::Allocate(const VideoTrackConstraintsN& aConstraints, } mTimePerFrame = aPrefs.mFPS ? 1000 / aPrefs.mFPS : aPrefs.mFPS; - return NS_OK; } @@ -227,6 +251,13 @@ MediaEngineTabVideoSource::Draw() { rect->GetWidth(&width); rect->GetHeight(&height); + if (mScrollWithPage) { + nsPoint point; + utils->GetScrollXY(false, &point.x, &point.y); + left += point.x; + top += point.y; + } + if (width == 0 || height == 0) { return; } diff --git a/content/media/webrtc/MediaEngineTabVideoSource.h b/content/media/webrtc/MediaEngineTabVideoSource.h index 7407ac9fc5f9..32c2c8e7cd52 100644 --- a/content/media/webrtc/MediaEngineTabVideoSource.h +++ b/content/media/webrtc/MediaEngineTabVideoSource.h @@ -63,6 +63,8 @@ protected: private: int mBufW; int mBufH; + int64_t mWindowId; + bool mScrollWithPage; int mTimePerFrame; ScopedFreePtr mData; nsCOMPtr mWindow; diff --git a/content/media/webrtc/MediaTrackConstraints.h b/content/media/webrtc/MediaTrackConstraints.h index e27073168a01..6b8e21dd49df 100644 --- a/content/media/webrtc/MediaTrackConstraints.h +++ b/content/media/webrtc/MediaTrackConstraints.h @@ -100,7 +100,12 @@ struct VideoTrackConstraintsN : Triage(Kind::Width).mWidth = mWidth; Triage(Kind::Height).mHeight = mHeight; Triage(Kind::FrameRate).mFrameRate = mFrameRate; - + if (mBrowserWindow.WasPassed()) { + Triage(Kind::BrowserWindow).mBrowserWindow.Construct(mBrowserWindow.Value()); + } + if (mScrollWithPage.WasPassed()) { + Triage(Kind::ScrollWithPage).mScrollWithPage.Construct(mScrollWithPage.Value()); + } // treat MediaSource special because it's always required mRequired.mMediaSource = mMediaSource; } diff --git a/dom/media/MediaManager.cpp b/dom/media/MediaManager.cpp index abe5fbb22671..9472f76cf594 100644 --- a/dom/media/MediaManager.cpp +++ b/dom/media/MediaManager.cpp @@ -1530,6 +1530,23 @@ MediaManager::GetUserMedia(bool aPrivileged, } #endif + if (c.mVideo.IsMediaTrackConstraints() && !aPrivileged) { + auto& tc = c.mVideo.GetAsMediaTrackConstraints(); + // only allow privileged content to set the window id + if (tc.mBrowserWindow.WasPassed()) { + tc.mBrowserWindow.Construct(-1); + } + + if (tc.mAdvanced.WasPassed()) { + uint32_t length = tc.mAdvanced.Value().Length(); + for (uint32_t i = 0; i < length; i++) { + if (tc.mAdvanced.Value()[i].mBrowserWindow.WasPassed()) { + tc.mAdvanced.Value()[i].mBrowserWindow.Construct(-1); + } + } + } + } + // Pass callbacks and MediaStreamListener along to GetUserMediaRunnable. nsRefPtr runnable; if (c.mFake) { diff --git a/dom/webidl/MediaTrackConstraintSet.webidl b/dom/webidl/MediaTrackConstraintSet.webidl index ccebf26b69d5..58e9820a4dc8 100644 --- a/dom/webidl/MediaTrackConstraintSet.webidl +++ b/dom/webidl/MediaTrackConstraintSet.webidl @@ -13,7 +13,9 @@ enum SupportedVideoConstraints { "width", "height", "frameRate", - "mediaSource" + "mediaSource", + "browserWindow", + "scrollWithPage" }; enum SupportedAudioConstraints { @@ -27,6 +29,8 @@ dictionary MediaTrackConstraintSet { ConstrainDoubleRange frameRate; ConstrainVideoFacingMode facingMode; ConstrainMediaSource mediaSource = "camera"; + long long browserWindow; + boolean scrollWithPage; }; // TODO: Bug 995352 can't nest unions From 986953d6aac14610a63f91b9268256d669244756 Mon Sep 17 00:00:00 2001 From: Brad Lassey Date: Sat, 2 Aug 2014 13:42:08 -0400 Subject: [PATCH 17/18] bug 1041700 - use browserWindow and scrollWithPAge media constraints for tab mirroring r=mfinkle --- mobile/android/modules/TabMirror.jsm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mobile/android/modules/TabMirror.jsm b/mobile/android/modules/TabMirror.jsm index e49ede97fb0e..d77ca42bd035 100644 --- a/mobile/android/modules/TabMirror.jsm +++ b/mobile/android/modules/TabMirror.jsm @@ -192,6 +192,8 @@ let TabMirror = function(deviceId, window) { let constraints = { video: { mediaSource: "browser", + browserWindow: windowId, + scrollWithPage: true, advanced: [ { width: { min: videoWidth, max: videoWidth }, height: { min: videoHeight, max: videoHeight } From 2e9871598181899d738c3bc917094b6d7f2b891b Mon Sep 17 00:00:00 2001 From: Brad Lassey Date: Sat, 2 Aug 2014 13:42:08 -0400 Subject: [PATCH 18/18] bug 1037424 - follow up to allow browser from privileged content r=jesup --- dom/media/MediaManager.cpp | 8 ++++++-- modules/libpref/src/init/all.js | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/dom/media/MediaManager.cpp b/dom/media/MediaManager.cpp index 9472f76cf594..3fb6240a01c2 100644 --- a/dom/media/MediaManager.cpp +++ b/dom/media/MediaManager.cpp @@ -1565,12 +1565,16 @@ MediaManager::GetUserMedia(bool aPrivileged, auto& tc = c.mVideo.GetAsMediaTrackConstraints(); // deny screensharing request if support is disabled if (tc.mMediaSource != dom::MediaSourceEnum::Camera) { - if (!Preferences::GetBool("media.getusermedia.screensharing.enabled", false)) { + if (tc.mMediaSource == dom::MediaSourceEnum::Browser) { + if (!Preferences::GetBool("media.getusermedia.browser.enabled", false)) { + return runnable->Denied(NS_LITERAL_STRING("PERMISSION_DENIED")); + } + } else if (!Preferences::GetBool("media.getusermedia.screensharing.enabled", false)) { return runnable->Denied(NS_LITERAL_STRING("PERMISSION_DENIED")); } /* Deny screensharing if the requesting document is not from a host on the whitelist. */ - if (!HostHasPermission(*docURI)) { + if (!aPrivileged && !HostHasPermission(*docURI)) { return runnable->Denied(NS_LITERAL_STRING("PERMISSION_DENIED")); } } diff --git a/modules/libpref/src/init/all.js b/modules/libpref/src/init/all.js index 97c43165617d..250b2af4acdc 100644 --- a/modules/libpref/src/init/all.js +++ b/modules/libpref/src/init/all.js @@ -311,6 +311,7 @@ pref("media.navigator.video.h264.max_br", 0); pref("media.navigator.video.h264.max_mbps", 0); pref("media.peerconnection.video.h264_enabled", false); pref("media.getusermedia.aec", 1); +pref("media.getusermedia.browser.enabled", true); #endif pref("media.peerconnection.video.min_bitrate", 200); pref("media.peerconnection.video.start_bitrate", 300);