author | Carsten "Tomcat" Book <cbook@mozilla.com> |
Mon, 22 Feb 2016 14:35:44 +0100 | |
changeset 285024 | 789a12291942763bc1e3a89f97e0b82dc1c9d00b |
parent 285017 | 241581e67d7535834591ae9ec031e5f0f37f6210 (current diff) |
parent 285023 | 675ef3c3fb457fff86b31ee0cc8b74d7fb5066d7 (diff) |
child 285039 | 3a188fabb9163317609fc7362bd7df037d4796ce |
push id | 30021 |
push user | cbook@mozilla.com |
push date | Mon, 22 Feb 2016 13:37:39 +0000 |
treeherder | mozilla-central@789a12291942 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | merge |
milestone | 47.0a1 |
first release with | nightly linux32
789a12291942
/
47.0a1
/
20160223030304
/
files
nightly linux64
789a12291942
/
47.0a1
/
20160223030304
/
files
nightly mac
789a12291942
/
47.0a1
/
20160223030304
/
files
nightly win32
789a12291942
/
47.0a1
/
20160223030304
/
files
nightly win64
789a12291942
/
47.0a1
/
20160223030304
/
files
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
releases | nightly linux32
47.0a1
/
20160223030304
/
pushlog to previous
nightly linux64
47.0a1
/
20160223030304
/
pushlog to previous
nightly mac
47.0a1
/
20160223030304
/
pushlog to previous
nightly win32
47.0a1
/
20160223030304
/
pushlog to previous
nightly win64
47.0a1
/
20160223030304
/
pushlog to previous
|
--- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1313,26 +1313,33 @@ pref("services.sync.prefs.sync.privacy.d pref("services.sync.prefs.sync.privacy.sanitize.sanitizeOnShutdown", true); pref("services.sync.prefs.sync.privacy.trackingprotection.enabled", true); pref("services.sync.prefs.sync.privacy.trackingprotection.pbmode.enabled", true); pref("services.sync.prefs.sync.security.OCSP.enabled", true); pref("services.sync.prefs.sync.security.OCSP.require", true); pref("services.sync.prefs.sync.security.default_personal_cert", true); pref("services.sync.prefs.sync.security.tls.version.min", true); pref("services.sync.prefs.sync.security.tls.version.max", true); +pref("services.sync.prefs.sync.services.sync.syncedTabs.showRemoteIcons", true); pref("services.sync.prefs.sync.signon.rememberSignons", true); pref("services.sync.prefs.sync.spellchecker.dictionary", true); pref("services.sync.prefs.sync.xpinstall.whitelist.required", true); #ifdef NIGHTLY_BUILD pref("services.sync.syncedTabsUIRefresh", true); #else pref("services.sync.syncedTabsUIRefresh", false); #endif +// A preference that controls whether we should show the icon for a remote tab. +// This pref has no UI but exists because some people may be concerned that +// fetching these icons to show remote tabs may leak information about that +// user's tabs and bookmarks. Note this pref is also synced. +pref("services.sync.syncedTabs.showRemoteIcons", true); + // Developer edition preferences #ifdef MOZ_DEV_EDITION sticky_pref("lightweightThemes.selectedThemeID", "firefox-devedition@mozilla.org"); #else sticky_pref("lightweightThemes.selectedThemeID", ""); #endif // Whether the character encoding menu is under the main Firefox button. This
--- a/browser/base/content/test/general/browser.ini +++ b/browser/base/content/test/general/browser.ini @@ -14,16 +14,17 @@ support-files = browser_bug970746.xhtml browser_fxa_oauth.html browser_fxa_oauth_with_keys.html browser_fxa_web_channel.html browser_registerProtocolHandler_notification.html browser_star_hsts.sjs browser_tab_dragdrop2_frame1.xul browser_web_channel.html + browser_web_channel_iframe.html bug592338.html bug792517-2.html bug792517.html bug792517.sjs bug839103.css contextmenu_common.js ctxmenu-image.png discovery.html
--- a/browser/base/content/test/general/browser_web_channel.html +++ b/browser/base/content/test/general/browser_web_channel.html @@ -1,29 +1,46 @@ <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>web_channel_test</title> </head> <body> <script> + var IFRAME_SRC_ROOT = "http://mochi.test:8888/browser/browser/base/content/test/general/browser_web_channel_iframe.html"; + window.onload = function() { var testName = window.location.search.replace(/^\?/, ""); switch(testName) { case "generic": test_generic(); break; case "twoway": test_twoWay(); break; case "multichannel": test_multichannel(); break; + case "iframe": + test_iframe(); + break; + case "iframe_pre_redirect": + test_iframe_pre_redirect(); + break; + case "unsolicited": + test_unsolicited(); + break; + case "bubbles": + test_bubbles(); + break; + default: + throw new Error(`INVALID TEST NAME ${testName}`); + break; } }; function test_generic() { var event = new window.CustomEvent("WebChannelMessageToChrome", { detail: { id: "generic", message: { @@ -79,11 +96,72 @@ id: "multichannel", message: {}, } }); window.dispatchEvent(event1); window.dispatchEvent(event2); } + + function test_iframe() { + // Note that this message is the response to the message sent + // by the iframe! This is bad, as this page is *not* trusted. + window.addEventListener("WebChannelMessageToContent", function(e) { + // the test parent will fail if the echo message is received. + echoEventToChannel(e, "echo"); + }); + + // only attach the iframe for the iframe test to avoid + // interfering with other tests. + var iframe = document.createElement("iframe"); + iframe.setAttribute("src", IFRAME_SRC_ROOT + "?iframe"); + document.body.appendChild(iframe); + } + + function test_iframe_pre_redirect() { + var iframe = document.createElement("iframe"); + iframe.setAttribute("src", IFRAME_SRC_ROOT + "?iframe_pre_redirect"); + document.body.appendChild(iframe); + } + + function test_unsolicited() { + // echo any unsolicted events back to chrome. + window.addEventListener("WebChannelMessageToContent", function(e) { + echoEventToChannel(e, "echo"); + }, true); + } + + function test_bubbles() { + var event = new window.CustomEvent("WebChannelMessageToChrome", { + detail: { + id: "not_a_window", + message: { + command: "start" + } + } + }); + + var nonWindowTarget = document.getElementById("not_a_window"); + + nonWindowTarget.addEventListener("WebChannelMessageToContent", function(e) { + echoEventToChannel(e, "not_a_window"); + }, true); + + + nonWindowTarget.dispatchEvent(event); + } + + function echoEventToChannel(e, channelId) { + var echoedEvent = new window.CustomEvent("WebChannelMessageToChrome", { + detail: { + id: channelId, + message: e.detail.message, + } + }); + + e.target.dispatchEvent(echoedEvent); + } </script> + +<div id="not_a_window"></div> </body> </html>
--- a/browser/base/content/test/general/browser_web_channel.js +++ b/browser/base/content/test/general/browser_web_channel.js @@ -5,16 +5,19 @@ 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"; +const HTTP_MISMATCH_PATH = "http://example.org"; +const HTTP_IFRAME_PATH = "http://mochi.test:8888"; +const HTTP_REDIRECTED_IFRAME_PATH = "http://example.org"; // Keep this synced with /mobile/android/tests/browser/robocop/testWebChannel.js // as much as possible. (We only have that since we can't run browser chrome // tests on Android. Yet?) var gTests = [ { desc: "WebChannel generic message", run: function* () { @@ -56,32 +59,280 @@ var gTests = [ } }); tab = gBrowser.addTab(HTTP_PATH + HTTP_ENDPOINT + "?twoway"); }); } }, { + desc: "WebChannel two way communication in an iframe", + run: function* () { + let parentChannel = new WebChannel("echo", Services.io.newURI(HTTP_PATH, null, null)); + let iframeChannel = new WebChannel("twoway", Services.io.newURI(HTTP_IFRAME_PATH, null, null)); + let promiseTestDone = new Promise(function (resolve, reject) { + parentChannel.listen(function (id, message, sender) { + reject(new Error("WebChannel message incorrectly sent to parent")); + }); + + iframeChannel.listen(function (id, message, sender) { + is(id, "twoway"); + ok(message.command); + + if (message.command === "one") { + iframeChannel.send({ data: { nested: true } }, sender); + } + + if (message.command === "two") { + is(message.detail.data.nested, true); + resolve(); + } + }); + }); + yield BrowserTestUtils.withNewTab({ + gBrowser: gBrowser, + url: HTTP_PATH + HTTP_ENDPOINT + "?iframe" + }, function* () { + yield promiseTestDone; + parentChannel.stopListening(); + iframeChannel.stopListening(); + }); + } + }, + { + desc: "WebChannel response to a redirected iframe", + run: function* () { + /** + * This test checks that WebChannel responses are only sent + * to an iframe if the iframe has not redirected to another origin. + * Test flow: + * 1. create a page, embed an iframe on origin A. + * 2. the iframe sends a message `redirecting`, then redirects to + * origin B. + * 3. the iframe at origin B is set up to echo any messages back to the + * test parent. + * 4. the test parent receives the `redirecting` message from origin A. + * the test parent creates a new channel with origin B. + * 5. when origin B is ready, it sends a `loaded` message to the test + * parent, letting the test parent know origin B is ready to echo + * messages. + * 5. the test parent tries to send a response to origin A. If the + * WebChannel does not perform a valid origin check, the response + * will be received by origin B. If the WebChannel does perform + * a valid origin check, the response will not be sent. + * 6. the test parent sends a `done` message to origin B, which origin + * B echoes back. If the response to origin A is not echoed but + * the message to origin B is, then hooray, the test passes. + */ + + let preRedirectChannel = new WebChannel("pre_redirect", Services.io.newURI(HTTP_IFRAME_PATH, null, null)); + let postRedirectChannel = new WebChannel("post_redirect", Services.io.newURI(HTTP_REDIRECTED_IFRAME_PATH, null, null)); + + let promiseTestDone = new Promise(function (resolve, reject) { + preRedirectChannel.listen(function (id, message, preRedirectSender) { + if (message.command === "redirecting") { + + postRedirectChannel.listen(function (id, message, postRedirectSender) { + is(id, "post_redirect"); + isnot(message.command, "no_response_expected"); + + if (message.command === "loaded") { + // The message should not be received on the preRedirectChannel + // because the target window has redirected. + preRedirectChannel.send({ command: "no_response_expected" }, preRedirectSender); + postRedirectChannel.send({ command: "done" }, postRedirectSender); + } else if (message.command === "done") { + resolve(); + } else { + reject(new Error(`Unexpected command ${message.command}`)); + } + }); + } else { + reject(new Error(`Unexpected command ${message.command}`)); + } + }); + }); + + yield BrowserTestUtils.withNewTab({ + gBrowser: gBrowser, + url: HTTP_PATH + HTTP_ENDPOINT + "?iframe_pre_redirect" + }, function* () { + yield promiseTestDone; + preRedirectChannel.stopListening(); + postRedirectChannel.stopListening(); + }); + } + }, + { 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"); }); } - } + }, + { + desc: "WebChannel unsolicited send, using system principal", + run: function* () { + let channel = new WebChannel("echo", Services.io.newURI(HTTP_PATH, null, null)); + + // an unsolicted message is sent from Chrome->Content which is then + // echoed back. If the echo is received here, then the content + // received the message. + let messagePromise = new Promise(function (resolve, reject) { + channel.listen(function (id, message, sender) { + is(id, "echo"); + is(message.command, "unsolicited"); + + resolve() + }); + }); + + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: HTTP_PATH + HTTP_ENDPOINT + "?unsolicited" + }, function* (targetBrowser) { + channel.send({ command: "unsolicited" }, { + browser: targetBrowser, + principal: Services.scriptSecurityManager.getSystemPrincipal() + }); + yield messagePromise; + channel.stopListening(); + }); + } + }, + { + desc: "WebChannel unsolicited send, using target origin's principal", + run: function* () { + let targetURI = Services.io.newURI(HTTP_PATH, null, null); + let channel = new WebChannel("echo", targetURI); + + // an unsolicted message is sent from Chrome->Content which is then + // echoed back. If the echo is received here, then the content + // received the message. + let messagePromise = new Promise(function (resolve, reject) { + channel.listen(function (id, message, sender) { + is(id, "echo"); + is(message.command, "unsolicited"); + + resolve(); + }); + }); + + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: HTTP_PATH + HTTP_ENDPOINT + "?unsolicited" + }, function* (targetBrowser) { + + channel.send({ command: "unsolicited" }, { + browser: targetBrowser, + principal: Services.scriptSecurityManager.getNoAppCodebasePrincipal(targetURI) + }); + + yield messagePromise; + channel.stopListening(); + }); + } + }, + { + desc: "WebChannel unsolicited send with principal mismatch", + run: function* () { + let targetURI = Services.io.newURI(HTTP_PATH, null, null); + let channel = new WebChannel("echo", targetURI); + + // two unsolicited messages are sent from Chrome->Content. The first, + // `unsolicited_no_response_expected` is sent to the wrong principal + // and should not be echoed back. The second, `done`, is sent to the + // correct principal and should be echoed back. + let messagePromise = new Promise(function (resolve, reject) { + channel.listen(function (id, message, sender) { + is(id, "echo"); + + if (message.command === "done") { + resolve(); + } else { + reject(new Error(`Unexpected command ${message.command}`)); + } + }); + }); + + yield BrowserTestUtils.withNewTab({ + gBrowser: gBrowser, + url: HTTP_PATH + HTTP_ENDPOINT + "?unsolicited" + }, function* (targetBrowser) { + + let mismatchURI = Services.io.newURI(HTTP_MISMATCH_PATH, null, null); + let mismatchPrincipal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(mismatchURI); + + // send a message to the wrong principal. It should not be delivered + // to content, and should not be echoed back. + channel.send({ command: "unsolicited_no_response_expected" }, { + browser: targetBrowser, + principal: mismatchPrincipal + }); + + let targetPrincipal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(targetURI); + + // send the `done` message to the correct principal. It + // should be echoed back. + channel.send({ command: "done" }, { + browser: targetBrowser, + principal: targetPrincipal + }); + + yield messagePromise; + channel.stopListening(); + }); + } + }, + { + desc: "WebChannel non-window target", + run: function* () { + /** + * This test ensures messages can be received from and responses + * sent to non-window elements. + * + * First wait for the non-window element to send a "start" message. + * Then send the non-window element a "done" message. + * The non-window element will echo the "done" message back, if it + * receives the message. + * Listen for the response. If received, good to go! + */ + let channel = new WebChannel("not_a_window", Services.io.newURI(HTTP_PATH, null, null)); + + let testDonePromise = new Promise(function (resolve, reject) { + channel.listen(function (id, message, sender) { + if (message.command === "start") { + channel.send({ command: "done" }, sender); + } else if (message.command === "done") { + resolve(); + } else { + reject(new Error(`Unexpected command ${message.command}`)); + } + }); + }); + + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: HTTP_PATH + HTTP_ENDPOINT + "?bubbles" + }, function* () { + yield testDonePromise; + channel.stopListening(); + }); + } + }, ]; // gTests function test() { waitForExplicitFinish(); Task.spawn(function () { for (let test of gTests) { info("Running: " + test.desc);
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/general/browser_web_channel_iframe.html @@ -0,0 +1,97 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>web_channel_test (iframe)</title> +</head> +<body> +<script> + var REDIRECTED_IFRAME_SRC_ROOT = "http://example.org/browser/browser/base/content/test/general/browser_web_channel_iframe.html"; + + window.onload = function() { + var testName = window.location.search.replace(/^\?/, ""); + switch(testName) { + case "iframe": + test_iframe(); + break; + case "iframe_pre_redirect": + test_iframe_pre_redirect(); + break; + case "iframe_post_redirect": + test_iframe_post_redirect(); + break; + default: + throw new Error(`INVALID TEST NAME ${testName}`); + break; + } + }; + + function test_iframe() { + var firstMessage = new window.CustomEvent("WebChannelMessageToChrome", { + detail: { + id: "twoway", + message: { + command: "one", + }, + } + }); + + window.addEventListener("WebChannelMessageToContent", function(e) { + var secondMessage = new window.CustomEvent("WebChannelMessageToChrome", { + detail: { + id: "twoway", + message: { + command: "two", + detail: e.detail.message, + }, + }, + }); + + if (!e.detail.message.error) { + window.dispatchEvent(secondMessage); + } + }, true); + + window.dispatchEvent(firstMessage); + } + + + function test_iframe_pre_redirect() { + var firstMessage = new window.CustomEvent("WebChannelMessageToChrome", { + detail: { + id: "pre_redirect", + message: { + command: "redirecting", + }, + }, + }); + window.dispatchEvent(firstMessage); + document.location = REDIRECTED_IFRAME_SRC_ROOT + "?iframe_post_redirect"; + } + + function test_iframe_post_redirect() { + window.addEventListener("WebChannelMessageToContent", function(e) { + var echoMessage = new window.CustomEvent("WebChannelMessageToChrome", { + detail: { + id: "post_redirect", + message: e.detail.message, + }, + }); + + window.dispatchEvent(echoMessage); + }, true); + + // Let the test parent know the page has loaded and is ready to echo events + var loadedMessage = new window.CustomEvent("WebChannelMessageToChrome", { + detail: { + id: "post_redirect", + message: { + command: "loaded", + }, + }, + }); + window.dispatchEvent(loadedMessage); + } +</script> +</body> +</html>
copy from devtools/client/webconsole/webconsole.js copy to devtools/client/webconsole/jsterm.js --- a/devtools/client/webconsole/webconsole.js +++ b/devtools/client/webconsole/jsterm.js @@ -15,2846 +15,39 @@ const Debugger = require("Debugger"); loader.lazyServiceGetter(this, "clipboardHelper", "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper"); loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm"); loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter"); loader.lazyRequireGetter(this, "AutocompletePopup", "devtools/client/shared/autocomplete-popup", true); loader.lazyRequireGetter(this, "ToolSidebar", "devtools/client/framework/sidebar", true); -loader.lazyRequireGetter(this, "ConsoleOutput", "devtools/client/webconsole/console-output", true); loader.lazyRequireGetter(this, "Messages", "devtools/client/webconsole/console-output", true); loader.lazyRequireGetter(this, "asyncStorage", "devtools/shared/async-storage"); loader.lazyRequireGetter(this, "EnvironmentClient", "devtools/shared/client/main", true); loader.lazyRequireGetter(this, "ObjectClient", "devtools/shared/client/main", true); -loader.lazyRequireGetter(this, "system", "devtools/shared/system"); -loader.lazyRequireGetter(this, "Timers", "sdk/timers"); loader.lazyImporter(this, "VariablesView", "resource://devtools/client/shared/widgets/VariablesView.jsm"); loader.lazyImporter(this, "VariablesViewController", "resource://devtools/client/shared/widgets/VariablesViewController.jsm"); loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true); -loader.lazyImporter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm"); const STRINGS_URI = "chrome://devtools/locale/webconsole.properties"; var l10n = new WebConsoleUtils.L10n(STRINGS_URI); -const XHTML_NS = "http://www.w3.org/1999/xhtml"; - -const MIXED_CONTENT_LEARN_MORE = "https://developer.mozilla.org/docs/Security/MixedContent"; - -const TRACKING_PROTECTION_LEARN_MORE = "https://developer.mozilla.org/Firefox/Privacy/Tracking_Protection"; - -const INSECURE_PASSWORDS_LEARN_MORE = "https://developer.mozilla.org/docs/Security/InsecurePasswords"; - -const PUBLIC_KEY_PINS_LEARN_MORE = "https://developer.mozilla.org/docs/Web/Security/Public_Key_Pinning"; - -const STRICT_TRANSPORT_SECURITY_LEARN_MORE = "https://developer.mozilla.org/docs/Security/HTTP_Strict_Transport_Security"; - -const WEAK_SIGNATURE_ALGORITHM_LEARN_MORE = "https://developer.mozilla.org/docs/Security/Weak_Signature_Algorithm"; - -const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers"; - -const VARIABLES_VIEW_URL = "chrome://devtools/content/shared/widgets/VariablesView.xul"; - -const IGNORED_SOURCE_URLS = ["debugger eval code"]; - -// The amount of time in milliseconds that we wait before performing a live -// search. -const SEARCH_DELAY = 200; - -// The number of lines that are displayed in the console output by default, for -// each category. The user can change this number by adjusting the hidden -// "devtools.hud.loglimit.{network,cssparser,exception,console}" preferences. -const DEFAULT_LOG_LIMIT = 1000; - -// The various categories of messages. We start numbering at zero so we can -// use these as indexes into the MESSAGE_PREFERENCE_KEYS matrix below. -const CATEGORY_NETWORK = 0; -const CATEGORY_CSS = 1; -const CATEGORY_JS = 2; -const CATEGORY_WEBDEV = 3; -// always on -const CATEGORY_INPUT = 4; -// always on -const CATEGORY_OUTPUT = 5; -const CATEGORY_SECURITY = 6; -const CATEGORY_SERVER = 7; - -// The possible message severities. As before, we start at zero so we can use -// these as indexes into MESSAGE_PREFERENCE_KEYS. -const SEVERITY_ERROR = 0; -const SEVERITY_WARNING = 1; -const SEVERITY_INFO = 2; -const SEVERITY_LOG = 3; - -// The fragment of a CSS class name that identifies each category. -const CATEGORY_CLASS_FRAGMENTS = [ - "network", - "cssparser", - "exception", - "console", - "input", - "output", - "security", - "server", -]; - -// The fragment of a CSS class name that identifies each severity. -const SEVERITY_CLASS_FRAGMENTS = [ - "error", - "warn", - "info", - "log", -]; - -// The preference keys to use for each category/severity combination, indexed -// first by category (rows) and then by severity (columns) in the following -// order: -// -// [ Error, Warning, Info, Log ] -// -// Most of these rather idiosyncratic names are historical and predate the -// division of message type into "category" and "severity". -const MESSAGE_PREFERENCE_KEYS = [ - // Network - [ "network", "netwarn", "netxhr", "networkinfo", ], - // CSS - [ "csserror", "cssparser", null, "csslog", ], - // JS - [ "exception", "jswarn", null, "jslog", ], - // Web Developer - [ "error", "warn", "info", "log", ], - // Input - [ null, null, null, null, ], - // Output - [ null, null, null, null, ], - // Security - [ "secerror", "secwarn", null, null, ], - // Server Logging - [ "servererror", "serverwarn", "serverinfo", "serverlog", ], -]; - -// A mapping from the console API log event levels to the Web Console -// severities. -const LEVELS = { - error: SEVERITY_ERROR, - exception: SEVERITY_ERROR, - assert: SEVERITY_ERROR, - warn: SEVERITY_WARNING, - info: SEVERITY_INFO, - log: SEVERITY_LOG, - trace: SEVERITY_LOG, - table: SEVERITY_LOG, - debug: SEVERITY_LOG, - dir: SEVERITY_LOG, - dirxml: SEVERITY_LOG, - group: SEVERITY_LOG, - groupCollapsed: SEVERITY_LOG, - groupEnd: SEVERITY_LOG, - time: SEVERITY_LOG, - timeEnd: SEVERITY_LOG, - count: SEVERITY_LOG -}; - -// This array contains the prefKey for the workers and it must keep them in the -// same order as CONSOLE_WORKER_IDS -const WORKERTYPES_PREFKEYS = - [ "sharedworkers", "serviceworkers", "windowlessworkers" ]; - -// The lowest HTTP response code (inclusive) that is considered an error. -const MIN_HTTP_ERROR_CODE = 400; -// The highest HTTP response code (inclusive) that is considered an error. -const MAX_HTTP_ERROR_CODE = 599; - // Constants used for defining the direction of JSTerm input history navigation. const HISTORY_BACK = -1; const HISTORY_FORWARD = 1; -// The indent of a console group in pixels. -const GROUP_INDENT = 12; - -// The number of messages to display in a single display update. If we display -// too many messages at once we slow down the Firefox UI too much. -const MESSAGES_IN_INTERVAL = DEFAULT_LOG_LIMIT; - -// The delay (in milliseconds) between display updates - tells how often we -// should *try* to push new messages to screen. This value is optimistic, -// updates won't always happen. Keep this low so the Web Console output feels -// live. -const OUTPUT_INTERVAL = 20; - -// The maximum amount of time (in milliseconds) that can be spent doing cleanup -// inside of the flush output callback. If things don't get cleaned up in this -// time, then it will start again the next time it is called. -const MAX_CLEANUP_TIME = 10; - -// When the output queue has more than MESSAGES_IN_INTERVAL items we throttle -// output updates to this number of milliseconds. So during a lot of output we -// update every N milliseconds given here. -const THROTTLE_UPDATES = 1000; - -// The preference prefix for all of the Web Console filters. -const FILTER_PREFS_PREFIX = "devtools.webconsole.filter."; - -// The minimum font size. -const MIN_FONT_SIZE = 10; - -const PREF_CONNECTION_TIMEOUT = "devtools.debugger.remote-timeout"; -const PREF_PERSISTLOG = "devtools.webconsole.persistlog"; -const PREF_MESSAGE_TIMESTAMP = "devtools.webconsole.timestampMessages"; -const PREF_AUTO_MULTILINE = "devtools.webconsole.autoMultiline"; -const PREF_INPUT_HISTORY_COUNT = "devtools.webconsole.inputHistoryCount"; - -/** - * A WebConsoleFrame instance is an interactive console initialized *per target* - * that displays console log data as well as provides an interactive terminal to - * manipulate the target's document content. - * - * The WebConsoleFrame is responsible for the actual Web Console UI - * implementation. - * - * @constructor - * @param object webConsoleOwner - * The WebConsole owner object. - */ -function WebConsoleFrame(webConsoleOwner) { - this.owner = webConsoleOwner; - this.hudId = this.owner.hudId; - this.window = this.owner.iframeWindow; - - this._repeatNodes = {}; - this._outputQueue = []; - this._itemDestroyQueue = []; - this._pruneCategoriesQueue = {}; - this.filterPrefs = {}; - - this.output = new ConsoleOutput(this); - - this._toggleFilter = this._toggleFilter.bind(this); - this.resize = this.resize.bind(this); - this._onPanelSelected = this._onPanelSelected.bind(this); - this._flushMessageQueue = this._flushMessageQueue.bind(this); - this._onToolboxPrefChanged = this._onToolboxPrefChanged.bind(this); - this._onUpdateListeners = this._onUpdateListeners.bind(this); - - this._outputTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - this._outputTimerInitialized = false; - - EventEmitter.decorate(this); -} -exports.WebConsoleFrame = WebConsoleFrame; - -WebConsoleFrame.prototype = { - /** - * The WebConsole instance that owns this frame. - * @see hudservice.js::WebConsole - * @type object - */ - owner: null, - - /** - * Proxy between the Web Console and the remote Web Console instance. This - * object holds methods used for connecting, listening and disconnecting from - * the remote server, using the remote debugging protocol. - * - * @see WebConsoleConnectionProxy - * @type object - */ - proxy: null, - - /** - * Getter for the xul:popupset that holds any popups we open. - * @type nsIDOMElement - */ - get popupset() { - return this.owner.mainPopupSet; - }, - - /** - * Holds the initialization promise object. - * @private - * @type object - */ - _initDefer: null, - - /** - * Last time when we displayed any message in the output. - * - * @private - * @type number - * Timestamp in milliseconds since the Unix epoch. - */ - _lastOutputFlush: 0, - - /** - * Message nodes are stored here in a queue for later display. - * - * @private - * @type array - */ - _outputQueue: null, - - /** - * Keep track of the categories we need to prune from time to time. - * - * @private - * @type array - */ - _pruneCategoriesQueue: null, - - /** - * Function invoked whenever the output queue is emptied. This is used by some - * tests. - * - * @private - * @type function - */ - _flushCallback: null, - - /** - * Timer used for flushing the messages output queue. - * - * @private - * @type nsITimer - */ - _outputTimer: null, - _outputTimerInitialized: null, - - /** - * Store for tracking repeated nodes. - * @private - * @type object - */ - _repeatNodes: null, - - /** - * Preferences for filtering messages by type. - * @see this._initDefaultFilterPrefs() - * @type object - */ - filterPrefs: null, - - /** - * Prefix used for filter preferences. - * @private - * @type string - */ - _filterPrefsPrefix: FILTER_PREFS_PREFIX, - - /** - * The nesting depth of the currently active console group. - */ - groupDepth: 0, - - /** - * The current target location. - * @type string - */ - contentLocation: "", - - /** - * The JSTerm object that manage the console's input. - * @see JSTerm - * @type object - */ - jsterm: null, - - /** - * The element that holds all of the messages we display. - * @type nsIDOMElement - */ - outputNode: null, - - /** - * The ConsoleOutput instance that manages all output. - * @type object - */ - output: null, - - /** - * The input element that allows the user to filter messages by string. - * @type nsIDOMElement - */ - filterBox: null, - - /** - * Getter for the debugger WebConsoleClient. - * @type object - */ - get webConsoleClient() { - return this.proxy ? this.proxy.webConsoleClient : null; - }, - - _destroyer: null, - - _saveRequestAndResponseBodies: true, - - // Chevron width at the starting of Web Console's input box. - _chevronWidth: 0, - // Width of the monospace characters in Web Console's input box. - _inputCharWidth: 0, - - /** - * Setter for saving of network request and response bodies. - * - * @param boolean value - * The new value you want to set. - */ - setSaveRequestAndResponseBodies: function(value) { - if (!this.webConsoleClient) { - // Don't continue if the webconsole disconnected. - return promise.resolve(null); - } - - let deferred = promise.defer(); - let newValue = !!value; - let toSet = { - "NetworkMonitor.saveRequestAndResponseBodies": newValue, - }; - - // Make sure the web console client connection is established first. - this.webConsoleClient.setPreferences(toSet, response => { - if (!response.error) { - this._saveRequestAndResponseBodies = newValue; - deferred.resolve(response); - } else { - deferred.reject(response.error); - } - }); - - return deferred.promise; - }, - - /** - * Getter for the persistent logging preference. - * @type boolean - */ - get persistLog() { - // For the browser console, we receive tab navigation - // when the original top level window we attached to is closed, - // but we don't want to reset console history and just switch to - // the next available window. - return this.owner._browserConsole || - Services.prefs.getBoolPref(PREF_PERSISTLOG); - }, - - /** - * Initialize the WebConsoleFrame instance. - * @return object - * A promise object that resolves once the frame is ready to use. - */ - init: function() { - this._initUI(); - let connectionInited = this._initConnection(); - - // Don't reject if the history fails to load for some reason. - // This would be fine, the panel will just start with empty history. - let allReady = this.jsterm.historyLoaded.catch(() => {}).then(() => { - return connectionInited; - }); - - // This notification is only used in tests. Don't chain it onto - // the returned promise because the console panel needs to be attached - // to the toolbox before the web-console-created event is receieved. - let notifyObservers = () => { - let id = WebConsoleUtils.supportsString(this.hudId); - Services.obs.notifyObservers(id, "web-console-created", null); - }; - allReady.then(notifyObservers, notifyObservers); - - return allReady; - }, - - /** - * Connect to the server using the remote debugging protocol. - * - * @private - * @return object - * A promise object that is resolved/reject based on the connection - * result. - */ - _initConnection: function() { - if (this._initDefer) { - return this._initDefer.promise; - } - - this._initDefer = promise.defer(); - this.proxy = new WebConsoleConnectionProxy(this, this.owner.target); - - this.proxy.connect().then(() => { - // on success - this._initDefer.resolve(this); - }, (reason) => { - // on failure - let node = this.createMessageNode(CATEGORY_JS, SEVERITY_ERROR, - reason.error + ": " + reason.message); - this.outputMessage(CATEGORY_JS, node, [reason]); - this._initDefer.reject(reason); - }); - - return this._initDefer.promise; - }, - - /** - * Find the Web Console UI elements and setup event listeners as needed. - * @private - */ - _initUI: function() { - this.document = this.window.document; - this.rootElement = this.document.documentElement; - - this._initDefaultFilterPrefs(); - - // Register the controller to handle "select all" properly. - this._commandController = new CommandController(this); - this.window.controllers.insertControllerAt(0, this._commandController); - - this._contextMenuHandler = new ConsoleContextMenu(this); - - let doc = this.document; - - if (system.constants.platform === "macosx") { - doc.querySelector("#key_clearOSX").removeAttribute("disabled"); - } else { - doc.querySelector("#key_clear").removeAttribute("disabled"); - } - - this.filterBox = doc.querySelector(".hud-filter-box"); - this.outputNode = doc.getElementById("output-container"); - this.outputWrapper = doc.getElementById("output-wrapper"); - - this.completeNode = doc.querySelector(".jsterm-complete-node"); - this.inputNode = doc.querySelector(".jsterm-input-node"); - - this._setFilterTextBoxEvents(); - this._initFilterButtons(); - - let fontSize = this.owner._browserConsole ? - Services.prefs.getIntPref("devtools.webconsole.fontSize") : - 0; - - if (fontSize != 0) { - fontSize = Math.max(MIN_FONT_SIZE, fontSize); - - this.outputNode.style.fontSize = fontSize + "px"; - this.completeNode.style.fontSize = fontSize + "px"; - this.inputNode.style.fontSize = fontSize + "px"; - } - - if (this.owner._browserConsole) { - for (let id of ["Enlarge", "Reduce", "Reset"]) { - this.document.getElementById("cmd_fullZoom" + id) - .removeAttribute("disabled"); - } - } - - // Update the character width and height needed for the popup offset - // calculations. - this._updateCharSize(); - - let clearButton = - doc.getElementsByClassName("webconsole-clear-console-button")[0]; - clearButton.addEventListener("command", () => { - this.owner._onClearButton(); - this.jsterm.clearOutput(true); - }); - - this.jsterm = new JSTerm(this); - this.jsterm.init(); - - this.resize(); - this.window.addEventListener("resize", this.resize, true); - this.jsterm.on("sidebar-opened", this.resize); - this.jsterm.on("sidebar-closed", this.resize); - - let toolbox = gDevTools.getToolbox(this.owner.target); - if (toolbox) { - toolbox.on("webconsole-selected", this._onPanelSelected); - } - - /* - * Focus input line whenever the output area is clicked. - * Reusing _addMEssageLinkCallback since it correctly filters - * drag and select events. - */ - this._addFocusCallback(this.outputNode, (evt) => { - if ((evt.target.nodeName.toLowerCase() != "a") && - (evt.target.parentNode.nodeName.toLowerCase() != "a")) { - this.jsterm.focus(); - } - }); - - // Toggle the timestamp on preference change - gDevTools.on("pref-changed", this._onToolboxPrefChanged); - this._onToolboxPrefChanged("pref-changed", { - pref: PREF_MESSAGE_TIMESTAMP, - newValue: Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP), - }); - - // focus input node - this.jsterm.focus(); - }, - - /** - * Resizes the output node to fit the output wrapped. - * We need this because it makes the layout a lot faster than - * using -moz-box-flex and 100% width. See Bug 1237368. - */ - resize: function() { - this.outputNode.style.width = this.outputWrapper.clientWidth + "px"; - }, - - /** - * Sets the focus to JavaScript input field when the web console tab is - * selected or when there is a split console present. - * @private - */ - _onPanelSelected: function() { - this.jsterm.focus(); - }, - - /** - * Initialize the default filter preferences. - * @private - */ - _initDefaultFilterPrefs: function() { - let prefs = ["network", "networkinfo", "csserror", "cssparser", "csslog", - "exception", "jswarn", "jslog", "error", "info", "warn", "log", - "secerror", "secwarn", "netwarn", "netxhr", "sharedworkers", - "serviceworkers", "windowlessworkers", "servererror", - "serverwarn", "serverinfo", "serverlog"]; - - for (let pref of prefs) { - this.filterPrefs[pref] = Services.prefs.getBoolPref( - this._filterPrefsPrefix + pref); - } - }, - - /** - * Attach / detach reflow listeners depending on the checked status - * of the `CSS > Log` menuitem. - * - * @param function [callback=null] - * Optional function to invoke when the listener has been - * added/removed. - */ - _updateReflowActivityListener: function(callback) { - if (this.webConsoleClient) { - let pref = this._filterPrefsPrefix + "csslog"; - if (Services.prefs.getBoolPref(pref)) { - this.webConsoleClient.startListeners(["ReflowActivity"], callback); - } else { - this.webConsoleClient.stopListeners(["ReflowActivity"], callback); - } - } - }, - - /** - * Attach / detach server logging listener depending on the filter - * preferences. If the user isn't interested in the server logs at - * all the listener is not registered. - * - * @param function [callback=null] - * Optional function to invoke when the listener has been - * added/removed. - */ - _updateServerLoggingListener: function(callback) { - if (!this.webConsoleClient) { - return; - } - - let startListener = false; - let prefs = ["servererror", "serverwarn", "serverinfo", "serverlog"]; - for (let i = 0; i < prefs.length; i++) { - if (this.filterPrefs[prefs[i]]) { - startListener = true; - break; - } - } - - if (startListener) { - this.webConsoleClient.startListeners(["ServerLogging"], callback); - } else { - this.webConsoleClient.stopListeners(["ServerLogging"], callback); - } - }, - - /** - * Sets the events for the filter input field. - * @private - */ - _setFilterTextBoxEvents: function() { - let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - let timerEvent = this.adjustVisibilityOnSearchStringChange.bind(this); - - let onChange = function _onChange() { - // To improve responsiveness, we let the user finish typing before we - // perform the search. - timer.cancel(); - timer.initWithCallback(timerEvent, SEARCH_DELAY, - Ci.nsITimer.TYPE_ONE_SHOT); - }; - - this.filterBox.addEventListener("command", onChange, false); - this.filterBox.addEventListener("input", onChange, false); - }, - - /** - * Creates one of the filter buttons on the toolbar. - * - * @private - * @param nsIDOMNode aParent - * The node to which the filter button should be appended. - * @param object aDescriptor - * A descriptor that contains info about the button. Contains "name", - * "category", and "prefKey" properties, and optionally a "severities" - * property. - */ - _initFilterButtons: function() { - let categories = this.document - .querySelectorAll(".webconsole-filter-button[category]"); - Array.forEach(categories, function(button) { - button.addEventListener("contextmenu", () => { - button.open = true; - }, false); - button.addEventListener("click", this._toggleFilter, false); - - let someChecked = false; - let severities = button.querySelectorAll("menuitem[prefKey]"); - Array.forEach(severities, function(menuItem) { - menuItem.addEventListener("command", this._toggleFilter, false); - - let prefKey = menuItem.getAttribute("prefKey"); - let checked = this.filterPrefs[prefKey]; - menuItem.setAttribute("checked", checked); - someChecked = someChecked || checked; - }, this); - - button.setAttribute("checked", someChecked); - button.setAttribute("aria-pressed", someChecked); - }, this); - - if (!this.owner._browserConsole) { - // The Browser Console displays nsIConsoleMessages which are messages that - // end up in the JS category, but they are not errors or warnings, they - // are just log messages. The Web Console does not show such messages. - let jslog = this.document.querySelector("menuitem[prefKey=jslog]"); - jslog.hidden = true; - } - - if (Services.appinfo.OS == "Darwin") { - let net = this.document.querySelector("toolbarbutton[category=net]"); - let accesskey = net.getAttribute("accesskeyMacOSX"); - net.setAttribute("accesskey", accesskey); - - let logging = - this.document.querySelector("toolbarbutton[category=logging]"); - logging.removeAttribute("accesskey"); - - let serverLogging = - this.document.querySelector("toolbarbutton[category=server]"); - serverLogging.removeAttribute("accesskey"); - } - }, - - /** - * Increase, decrease or reset the font size. - * - * @param string size - * The size of the font change. Accepted values are "+" and "-". - * An unmatched size assumes a font reset. - */ - changeFontSize: function(size) { - let fontSize = this.window - .getComputedStyle(this.outputNode, null) - .getPropertyValue("font-size").replace("px", ""); - - if (this.outputNode.style.fontSize) { - fontSize = this.outputNode.style.fontSize.replace("px", ""); - } - - if (size == "+" || size == "-") { - fontSize = parseInt(fontSize, 10); - - if (size == "+") { - fontSize += 1; - } else { - fontSize -= 1; - } - - if (fontSize < MIN_FONT_SIZE) { - fontSize = MIN_FONT_SIZE; - } - - Services.prefs.setIntPref("devtools.webconsole.fontSize", fontSize); - fontSize = fontSize + "px"; - - this.completeNode.style.fontSize = fontSize; - this.inputNode.style.fontSize = fontSize; - this.outputNode.style.fontSize = fontSize; - } else { - this.completeNode.style.fontSize = ""; - this.inputNode.style.fontSize = ""; - this.outputNode.style.fontSize = ""; - Services.prefs.clearUserPref("devtools.webconsole.fontSize"); - } - this._updateCharSize(); - }, - - /** - * Calculates the width and height of a single character of the input box. - * This will be used in opening the popup at the correct offset. - * - * @private - */ - _updateCharSize: function() { - let doc = this.document; - let tempLabel = doc.createElementNS(XHTML_NS, "span"); - let style = tempLabel.style; - style.position = "fixed"; - style.padding = "0"; - style.margin = "0"; - style.width = "auto"; - style.color = "transparent"; - WebConsoleUtils.copyTextStyles(this.inputNode, tempLabel); - tempLabel.textContent = "x"; - doc.documentElement.appendChild(tempLabel); - this._inputCharWidth = tempLabel.offsetWidth; - tempLabel.parentNode.removeChild(tempLabel); - // Calculate the width of the chevron placed at the beginning of the input - // box. Remove 4 more pixels to accomodate the padding of the popup. - this._chevronWidth = +doc.defaultView.getComputedStyle(this.inputNode) - .paddingLeft.replace(/[^0-9.]/g, "") - 4; - }, +const XHTML_NS = "http://www.w3.org/1999/xhtml"; - /** - * The event handler that is called whenever a user switches a filter on or - * off. - * - * @private - * @param nsIDOMEvent event - * The event that triggered the filter change. - */ - _toggleFilter: function(event) { - let target = event.target; - let tagName = target.tagName; - // Prevent toggle if generated from a contextmenu event (right click) - let isRightClick = (event.button === 2); - if (tagName != event.currentTarget.tagName || isRightClick) { - return; - } - - switch (tagName) { - case "toolbarbutton": { - let originalTarget = event.originalTarget; - let classes = originalTarget.classList; - - if (originalTarget.localName !== "toolbarbutton") { - // Oddly enough, the click event is sent to the menu button when - // selecting a menu item with the mouse. Detect this case and bail - // out. - break; - } - - if (!classes.contains("toolbarbutton-menubutton-button") && - originalTarget.getAttribute("type") === "menu-button") { - // This is a filter button with a drop-down. The user clicked the - // drop-down, so do nothing. (The menu will automatically appear - // without our intervention.) - break; - } - - // Toggle on the targeted filter button, and if the user alt clicked, - // toggle off all other filter buttons and their associated filters. - let state = target.getAttribute("checked") !== "true"; - if (event.getModifierState("Alt")) { - let buttons = this.document - .querySelectorAll(".webconsole-filter-button"); - Array.forEach(buttons, (button) => { - if (button !== target) { - button.setAttribute("checked", false); - button.setAttribute("aria-pressed", false); - this._setMenuState(button, false); - } - }); - state = true; - } - target.setAttribute("checked", state); - target.setAttribute("aria-pressed", state); - - // This is a filter button with a drop-down, and the user clicked the - // main part of the button. Go through all the severities and toggle - // their associated filters. - this._setMenuState(target, state); - - // CSS reflow logging can decrease web page performance. - // Make sure the option is always unchecked when the CSS filter button - // is selected. See bug 971798. - if (target.getAttribute("category") == "css" && state) { - let csslogMenuItem = target.querySelector("menuitem[prefKey=csslog]"); - csslogMenuItem.setAttribute("checked", false); - this.setFilterState("csslog", false); - } - - break; - } - - case "menuitem": { - let state = target.getAttribute("checked") !== "true"; - target.setAttribute("checked", state); - - let prefKey = target.getAttribute("prefKey"); - this.setFilterState(prefKey, state); - - // Adjust the state of the button appropriately. - let menuPopup = target.parentNode; - - let someChecked = false; - let menuItem = menuPopup.firstChild; - while (menuItem) { - if (menuItem.hasAttribute("prefKey") && - menuItem.getAttribute("checked") === "true") { - someChecked = true; - break; - } - menuItem = menuItem.nextSibling; - } - let toolbarButton = menuPopup.parentNode; - toolbarButton.setAttribute("checked", someChecked); - toolbarButton.setAttribute("aria-pressed", someChecked); - break; - } - } - }, - - /** - * Set the menu attributes for a specific toggle button. - * - * @private - * @param XULElement target - * Button with drop down items to be toggled. - * @param boolean state - * True if the menu item is being toggled on, and false otherwise. - */ - _setMenuState: function(target, state) { - let menuItems = target.querySelectorAll("menuitem"); - Array.forEach(menuItems, (item) => { - item.setAttribute("checked", state); - let prefKey = item.getAttribute("prefKey"); - this.setFilterState(prefKey, state); - }); - }, - - /** - * Set the filter state for a specific toggle button. - * - * @param string toggleType - * @param boolean state - * @returns void - */ - setFilterState: function(toggleType, state) { - this.filterPrefs[toggleType] = state; - this.adjustVisibilityForMessageType(toggleType, state); - - Services.prefs.setBoolPref(this._filterPrefsPrefix + toggleType, state); - - if (this._updateListenersTimeout) { - Timers.clearTimeout(this._updateListenersTimeout); - } - - this._updateListenersTimeout = Timers.setTimeout( - this._onUpdateListeners, 200); - }, - - /** - * Get the filter state for a specific toggle button. - * - * @param string toggleType - * @returns boolean - */ - getFilterState: function(toggleType) { - return this.filterPrefs[toggleType]; - }, - - /** - * Called when a logging filter changes. Allows to stop/start - * listeners according to the current filter state. - */ - _onUpdateListeners: function() { - this._updateReflowActivityListener(); - this._updateServerLoggingListener(); - }, - - /** - * Check that the passed string matches the filter arguments. - * - * @param String str - * to search for filter words in. - * @param String filter - * is a string containing all of the words to filter on. - * @returns boolean - */ - stringMatchesFilters: function(str, filter) { - if (!filter || !str) { - return true; - } - - let searchStr = str.toLowerCase(); - let filterStrings = filter.toLowerCase().split(/\s+/); - return !filterStrings.some(function(f) { - return searchStr.indexOf(f) == -1; - }); - }, - - /** - * Turns the display of log nodes on and off appropriately to reflect the - * adjustment of the message type filter named by @prefKey. - * - * @param string prefKey - * The preference key for the message type being filtered: one of the - * values in the MESSAGE_PREFERENCE_KEYS table. - * @param boolean state - * True if the filter named by @messageType is being turned on; false - * otherwise. - * @returns void - */ - adjustVisibilityForMessageType: function(prefKey, state) { - let outputNode = this.outputNode; - let doc = this.document; - - // Look for message nodes (".message") with the given preference key - // (filter="error", filter="cssparser", etc.) and add or remove the - // "filtered-by-type" class, which turns on or off the display. - - let attribute = WORKERTYPES_PREFKEYS.indexOf(prefKey) == -1 - ? "filter" : "workerType"; - - let xpath = ".//*[contains(@class, 'message') and " + - "@" + attribute + "='" + prefKey + "']"; - let result = doc.evaluate(xpath, outputNode, null, - Ci.nsIDOMXPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); - for (let i = 0; i < result.snapshotLength; i++) { - let node = result.snapshotItem(i); - if (state) { - node.classList.remove("filtered-by-type"); - } else { - node.classList.add("filtered-by-type"); - } - } - }, - - /** - * Turns the display of log nodes on and off appropriately to reflect the - * adjustment of the search string. - */ - adjustVisibilityOnSearchStringChange: function() { - let nodes = this.outputNode.getElementsByClassName("message"); - let searchString = this.filterBox.value; - - for (let i = 0, n = nodes.length; i < n; ++i) { - let node = nodes[i]; - - // hide nodes that match the strings - let text = node.textContent; - - // if the text matches the words in aSearchString... - if (this.stringMatchesFilters(text, searchString)) { - node.classList.remove("filtered-by-string"); - } else { - node.classList.add("filtered-by-string"); - } - } - }, - - /** - * Applies the user's filters to a newly-created message node via CSS - * classes. - * - * @param nsIDOMNode node - * The newly-created message node. - * @return boolean - * True if the message was filtered or false otherwise. - */ - filterMessageNode: function(node) { - let isFiltered = false; - - // Filter by the message type. - let prefKey = MESSAGE_PREFERENCE_KEYS[node.category][node.severity]; - if (prefKey && !this.getFilterState(prefKey)) { - // The node is filtered by type. - node.classList.add("filtered-by-type"); - isFiltered = true; - } - - // Filter by worker type - if ("workerType" in node && !this.getFilterState(node.workerType)) { - node.classList.add("filtered-by-type"); - isFiltered = true; - } - - // Filter on the search string. - let search = this.filterBox.value; - let text = node.clipboardText; - - // if string matches the filter text - if (!this.stringMatchesFilters(text, search)) { - node.classList.add("filtered-by-string"); - isFiltered = true; - } - - if (isFiltered && node.classList.contains("inlined-variables-view")) { - node.classList.add("hidden-message"); - } - - return isFiltered; - }, - - /** - * Merge the attributes of repeated nodes. - * - * @param nsIDOMNode original - * The Original Node. The one being merged into. - */ - mergeFilteredMessageNode: function(original) { - let repeatNode = original.getElementsByClassName("message-repeats")[0]; - if (!repeatNode) { - // no repeat node, return early. - return; - } - - let occurrences = parseInt(repeatNode.getAttribute("value"), 10) + 1; - repeatNode.setAttribute("value", occurrences); - repeatNode.textContent = occurrences; - let str = l10n.getStr("messageRepeats.tooltip2"); - repeatNode.title = PluralForm.get(occurrences, str) - .replace("#1", occurrences); - }, - - /** - * Filter the message node from the output if it is a repeat. - * - * @private - * @param nsIDOMNode node - * The message node to be filtered or not. - * @returns nsIDOMNode|null - * Returns the duplicate node if the message was filtered, null - * otherwise. - */ - _filterRepeatedMessage: function(node) { - let repeatNode = node.getElementsByClassName("message-repeats")[0]; - if (!repeatNode) { - return null; - } - - let uid = repeatNode._uid; - let dupeNode = null; - - if (node.category == CATEGORY_CSS || - node.category == CATEGORY_SECURITY) { - dupeNode = this._repeatNodes[uid]; - if (!dupeNode) { - this._repeatNodes[uid] = node; - } - } else if ((node.category == CATEGORY_WEBDEV || - node.category == CATEGORY_JS) && - node.category != CATEGORY_NETWORK && - !node.classList.contains("inlined-variables-view")) { - let lastMessage = this.outputNode.lastChild; - if (!lastMessage) { - return null; - } - - let lastRepeatNode = - lastMessage.getElementsByClassName("message-repeats")[0]; - if (lastRepeatNode && lastRepeatNode._uid == uid) { - dupeNode = lastMessage; - } - } - - if (dupeNode) { - this.mergeFilteredMessageNode(dupeNode); - return dupeNode; - } - - return null; - }, - - /** - * Display cached messages that may have been collected before the UI is - * displayed. - * - * @param array remoteMessages - * Array of cached messages coming from the remote Web Console - * content instance. - */ - displayCachedMessages: function(remoteMessages) { - if (!remoteMessages.length) { - return; - } - - remoteMessages.forEach(function(message) { - switch (message._type) { - case "PageError": { - let category = Utils.categoryForScriptError(message); - this.outputMessage(category, this.reportPageError, - [category, message]); - break; - } - case "LogMessage": - this.handleLogMessage(message); - break; - case "ConsoleAPI": - this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage, - [message]); - break; - case "NetworkEvent": - this.outputMessage(CATEGORY_NETWORK, this.logNetEvent, [message]); - break; - } - }, this); - }, - - /** - * Logs a message to the Web Console that originates from the Web Console - * server. - * - * @param object message - * The message received from the server. - * @return nsIDOMElement|null - * The message element to display in the Web Console output. - */ - logConsoleAPIMessage: function(message) { - let body = null; - let clipboardText = null; - let sourceURL = message.filename; - let sourceLine = message.lineNumber; - let level = message.level; - let args = message.arguments; - let objectActors = new Set(); - let node = null; - - // Gather the actor IDs. - args.forEach((value) => { - if (WebConsoleUtils.isActorGrip(value)) { - objectActors.add(value.actor); - } - }); - - switch (level) { - case "log": - case "info": - case "warn": - case "error": - case "exception": - case "assert": - case "debug": { - let msg = new Messages.ConsoleGeneric(message); - node = msg.init(this.output).render().element; - break; - } - case "table": { - let msg = new Messages.ConsoleTable(message); - node = msg.init(this.output).render().element; - break; - } - case "trace": { - let msg = new Messages.ConsoleTrace(message); - node = msg.init(this.output).render().element; - break; - } - case "dir": { - body = { arguments: args }; - let clipboardArray = []; - args.forEach((value) => { - clipboardArray.push(VariablesView.getString(value)); - }); - clipboardText = clipboardArray.join(" "); - break; - } - case "dirxml": { - // We just alias console.dirxml() with console.log(). - message.level = "log"; - return this.logConsoleAPIMessage(message); - } - case "group": - case "groupCollapsed": - clipboardText = body = message.groupName; - this.groupDepth++; - break; - - case "groupEnd": - if (this.groupDepth > 0) { - this.groupDepth--; - } - break; - - case "time": { - let timer = message.timer; - if (!timer) { - return null; - } - if (timer.error) { - Cu.reportError(l10n.getStr(timer.error)); - return null; - } - body = l10n.getFormatStr("timerStarted", [timer.name]); - clipboardText = body; - break; - } - - case "timeEnd": { - let timer = message.timer; - if (!timer) { - return null; - } - let duration = Math.round(timer.duration * 100) / 100; - body = l10n.getFormatStr("timeEnd", [timer.name, duration]); - clipboardText = body; - break; - } - - case "count": { - let counter = message.counter; - if (!counter) { - return null; - } - if (counter.error) { - Cu.reportError(l10n.getStr(counter.error)); - return null; - } - let msg = new Messages.ConsoleGeneric(message); - node = msg.init(this.output).render().element; - break; - } - - case "timeStamp": { - // console.timeStamp() doesn't need to display anything. - return null; - } - - default: - Cu.reportError("Unknown Console API log level: " + level); - return null; - } - - // Release object actors for arguments coming from console API methods that - // we ignore their arguments. - switch (level) { - case "group": - case "groupCollapsed": - case "groupEnd": - case "time": - case "timeEnd": - case "count": - for (let actor of objectActors) { - this._releaseObject(actor); - } - objectActors.clear(); - } - - if (level == "groupEnd") { - // no need to continue - return null; - } - - if (!node) { - node = this.createMessageNode(CATEGORY_WEBDEV, LEVELS[level], body, - sourceURL, sourceLine, clipboardText, - level, message.timeStamp); - if (message.private) { - node.setAttribute("private", true); - } - } - - if (objectActors.size > 0) { - node._objectActors = objectActors; - - if (!node._messageObject) { - let repeatNode = node.getElementsByClassName("message-repeats")[0]; - repeatNode._uid += [...objectActors].join("-"); - } - } - - let workerTypeID = CONSOLE_WORKER_IDS.indexOf(message.workerType); - if (workerTypeID != -1) { - node.workerType = WORKERTYPES_PREFKEYS[workerTypeID]; - node.setAttribute("workerType", WORKERTYPES_PREFKEYS[workerTypeID]); - } - - return node; - }, - - /** - * Handle ConsoleAPICall objects received from the server. This method outputs - * the window.console API call. - * - * @param object message - * The console API message received from the server. - */ - handleConsoleAPICall: function(message) { - this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage, [message]); - }, - - /** - * Reports an error in the page source, either JavaScript or CSS. - * - * @param nsIScriptError scriptError - * The error message to report. - * @return nsIDOMElement|undefined - * The message element to display in the Web Console output. - */ - reportPageError: function(category, scriptError) { - // Warnings and legacy strict errors become warnings; other types become - // errors. - let severity = "error"; - if (scriptError.warning || scriptError.strict) { - severity = "warning"; - } else if (scriptError.info) { - severity = "log"; - } - - switch (category) { - case CATEGORY_CSS: - category = "css"; - break; - case CATEGORY_SECURITY: - category = "security"; - break; - default: - category = "js"; - break; - } - - let objectActors = new Set(); - - // Gather the actor IDs. - for (let prop of ["errorMessage", "lineText"]) { - let grip = scriptError[prop]; - if (WebConsoleUtils.isActorGrip(grip)) { - objectActors.add(grip.actor); - } - } - - let errorMessage = scriptError.errorMessage; - if (errorMessage.type && errorMessage.type == "longString") { - errorMessage = errorMessage.initial; - } - - let displayOrigin = scriptError.sourceName; - - // TLS errors are related to the connection and not the resource; therefore - // it makes sense to only display the protcol, host and port (prePath). - // This also means messages are grouped for a single origin. - if (scriptError.category && scriptError.category == "SHA-1 Signature") { - let sourceURI = Services.io.newURI(scriptError.sourceName, null, null) - .QueryInterface(Ci.nsIURL); - displayOrigin = sourceURI.prePath; - } - - // Create a new message - let msg = new Messages.Simple(errorMessage, { - location: { - url: displayOrigin, - line: scriptError.lineNumber, - column: scriptError.columnNumber - }, - stack: scriptError.stacktrace, - category: category, - severity: severity, - timestamp: scriptError.timeStamp, - private: scriptError.private, - filterDuplicates: true - }); - - let node = msg.init(this.output).render().element; - - // Select the body of the message node that is displayed in the console - let msgBody = node.getElementsByClassName("message-body")[0]; - // Add the more info link node to messages that belong to certain categories - this.addMoreInfoLink(msgBody, scriptError); - - if (objectActors.size > 0) { - node._objectActors = objectActors; - } - - return node; - }, - - /** - * Handle PageError objects received from the server. This method outputs the - * given error. - * - * @param nsIScriptError pageError - * The error received from the server. - */ - handlePageError: function(pageError) { - let category = Utils.categoryForScriptError(pageError); - this.outputMessage(category, this.reportPageError, [category, pageError]); - }, - - /** - * Handle log messages received from the server. This method outputs the given - * message. - * - * @param object packet - * The message packet received from the server. - */ - handleLogMessage: function(packet) { - if (packet.message) { - this.outputMessage(CATEGORY_JS, this._reportLogMessage, [packet]); - } - }, +const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers"; - /** - * Display log messages received from the server. - * - * @private - * @param object packet - * The message packet received from the server. - * @return nsIDOMElement - * The message element to render for the given log message. - */ - _reportLogMessage: function(packet) { - let msg = packet.message; - if (msg.type && msg.type == "longString") { - msg = msg.initial; - } - let node = this.createMessageNode(CATEGORY_JS, SEVERITY_LOG, msg, null, - null, null, null, packet.timeStamp); - if (WebConsoleUtils.isActorGrip(packet.message)) { - node._objectActors = new Set([packet.message.actor]); - } - return node; - }, - - /** - * Log network event. - * - * @param object networkInfo - * The network request information to log. - * @return nsIDOMElement|null - * The message element to display in the Web Console output. - */ - logNetEvent: function(networkInfo) { - let actorId = networkInfo.actor; - let request = networkInfo.request; - let clipboardText = request.method + " " + request.url; - let severity = SEVERITY_LOG; - if (networkInfo.isXHR) { - clipboardText = request.method + " XHR " + request.url; - severity = SEVERITY_INFO; - } - let mixedRequest = - WebConsoleUtils.isMixedHTTPSRequest(request.url, this.contentLocation); - if (mixedRequest) { - severity = SEVERITY_WARNING; - } - - let methodNode = this.document.createElementNS(XHTML_NS, "span"); - methodNode.className = "method"; - methodNode.textContent = request.method + " "; - - let messageNode = this.createMessageNode(CATEGORY_NETWORK, severity, - methodNode, null, null, - clipboardText, null, - networkInfo.timeStamp); - if (networkInfo.private) { - messageNode.setAttribute("private", true); - } - messageNode._connectionId = actorId; - messageNode.url = request.url; - - let body = methodNode.parentNode; - body.setAttribute("aria-haspopup", true); - - if (networkInfo.isXHR) { - let xhrNode = this.document.createElementNS(XHTML_NS, "span"); - xhrNode.className = "xhr"; - xhrNode.textContent = l10n.getStr("webConsoleXhrIndicator"); - body.appendChild(xhrNode); - body.appendChild(this.document.createTextNode(" ")); - } - - let displayUrl = request.url; - let pos = displayUrl.indexOf("?"); - if (pos > -1) { - displayUrl = displayUrl.substr(0, pos); - } - - let urlNode = this.document.createElementNS(XHTML_NS, "a"); - urlNode.className = "url"; - urlNode.setAttribute("title", request.url); - urlNode.href = request.url; - urlNode.textContent = displayUrl; - urlNode.draggable = false; - body.appendChild(urlNode); - body.appendChild(this.document.createTextNode(" ")); - - if (mixedRequest) { - messageNode.classList.add("mixed-content"); - this.makeMixedContentNode(body); - } - - let statusNode = this.document.createElementNS(XHTML_NS, "a"); - statusNode.className = "status"; - body.appendChild(statusNode); - - let onClick = () => this.openNetworkPanel(networkInfo.actor); - - this._addMessageLinkCallback(urlNode, onClick); - this._addMessageLinkCallback(statusNode, onClick); - - networkInfo.node = messageNode; - - this._updateNetMessage(actorId); - - return messageNode; - }, - - /** - * Create a mixed content warning Node. - * - * @param linkNode - * Parent to the requested urlNode. - */ - makeMixedContentNode: function(linkNode) { - let mixedContentWarning = - "[" + l10n.getStr("webConsoleMixedContentWarning") + "]"; - - // Mixed content warning message links to a Learn More page - let mixedContentWarningNode = this.document.createElementNS(XHTML_NS, "a"); - mixedContentWarningNode.title = MIXED_CONTENT_LEARN_MORE; - mixedContentWarningNode.href = MIXED_CONTENT_LEARN_MORE; - mixedContentWarningNode.className = "learn-more-link"; - mixedContentWarningNode.textContent = mixedContentWarning; - mixedContentWarningNode.draggable = false; - - linkNode.appendChild(mixedContentWarningNode); - - this._addMessageLinkCallback(mixedContentWarningNode, (event) => { - event.stopPropagation(); - this.owner.openLink(MIXED_CONTENT_LEARN_MORE); - }); - }, - - /** - * Adds a more info link node to messages based on the nsIScriptError object - * that we need to report to the console - * - * @param node - * The node to which we will be adding the more info link node - * @param scriptError - * The script error object that we are reporting to the console - */ - addMoreInfoLink: function(node, scriptError) { - let url; - switch (scriptError.category) { - case "Insecure Password Field": - url = INSECURE_PASSWORDS_LEARN_MORE; - break; - case "Mixed Content Message": - case "Mixed Content Blocker": - url = MIXED_CONTENT_LEARN_MORE; - break; - case "Invalid HPKP Headers": - url = PUBLIC_KEY_PINS_LEARN_MORE; - break; - case "Invalid HSTS Headers": - url = STRICT_TRANSPORT_SECURITY_LEARN_MORE; - break; - case "SHA-1 Signature": - url = WEAK_SIGNATURE_ALGORITHM_LEARN_MORE; - break; - case "Tracking Protection": - url = TRACKING_PROTECTION_LEARN_MORE; - break; - default: - // Unknown category. Return without adding more info node. - return; - } - - this.addLearnMoreWarningNode(node, url); - }, - - /* - * Appends a clickable warning node to the node passed - * as a parameter to the function. When a user clicks on the appended - * warning node, the browser navigates to the provided url. - * - * @param node - * The node to which we will be adding a clickable warning node. - * @param url - * The url which points to the page where the user can learn more - * about security issues associated with the specific message that's - * being logged. - */ - addLearnMoreWarningNode: function(node, url) { - let moreInfoLabel = "[" + l10n.getStr("webConsoleMoreInfoLabel") + "]"; - - let warningNode = this.document.createElementNS(XHTML_NS, "a"); - warningNode.title = url; - warningNode.href = url; - warningNode.draggable = false; - warningNode.textContent = moreInfoLabel; - warningNode.className = "learn-more-link"; - - this._addMessageLinkCallback(warningNode, (event) => { - event.stopPropagation(); - this.owner.openLink(url); - }); - - node.appendChild(warningNode); - }, - - /** - * Log file activity. - * - * @param string fileURI - * The file URI that was loaded. - * @return nsIDOMElement|undefined - * The message element to display in the Web Console output. - */ - logFileActivity: function(fileURI) { - let urlNode = this.document.createElementNS(XHTML_NS, "a"); - urlNode.setAttribute("title", fileURI); - urlNode.className = "url"; - urlNode.textContent = fileURI; - urlNode.draggable = false; - urlNode.href = fileURI; - - let outputNode = this.createMessageNode(CATEGORY_NETWORK, SEVERITY_LOG, - urlNode, null, null, fileURI); - - this._addMessageLinkCallback(urlNode, () => { - this.owner.viewSource(fileURI); - }); - - return outputNode; - }, - - /** - * Handle the file activity messages coming from the remote Web Console. - * - * @param string fileURI - * The file URI that was requested. - */ - handleFileActivity: function(fileURI) { - this.outputMessage(CATEGORY_NETWORK, this.logFileActivity, [fileURI]); - }, - - /** - * Handle the reflow activity messages coming from the remote Web Console. - * - * @param object msg - * An object holding information about a reflow batch. - */ - logReflowActivity: function(message) { - let {start, end, sourceURL, sourceLine} = message; - let duration = Math.round((end - start) * 100) / 100; - let node = this.document.createElementNS(XHTML_NS, "span"); - if (sourceURL) { - node.textContent = - l10n.getFormatStr("reflow.messageWithLink", [duration]); - let a = this.document.createElementNS(XHTML_NS, "a"); - a.href = "#"; - a.draggable = "false"; - let filename = WebConsoleUtils.abbreviateSourceURL(sourceURL); - let functionName = message.functionName || - l10n.getStr("stacktrace.anonymousFunction"); - a.textContent = l10n.getFormatStr("reflow.messageLinkText", - [functionName, filename, sourceLine]); - this._addMessageLinkCallback(a, () => { - this.owner.viewSourceInDebugger(sourceURL, sourceLine); - }); - node.appendChild(a); - } else { - node.textContent = - l10n.getFormatStr("reflow.messageWithNoLink", [duration]); - } - return this.createMessageNode(CATEGORY_CSS, SEVERITY_LOG, node); - }, - - handleReflowActivity: function(message) { - this.outputMessage(CATEGORY_CSS, this.logReflowActivity, [message]); - }, - - /** - * Inform user that the window.console API has been replaced by a script - * in a content page. - */ - logWarningAboutReplacedAPI: function() { - let node = this.createMessageNode(CATEGORY_JS, SEVERITY_WARNING, - l10n.getStr("ConsoleAPIDisabled")); - this.outputMessage(CATEGORY_JS, node); - }, - - /** - * Handle the network events coming from the remote Web Console. - * - * @param object networkInfo - * The network request information. - */ - handleNetworkEvent: function(networkInfo) { - this.outputMessage(CATEGORY_NETWORK, this.logNetEvent, [networkInfo]); - }, - - /** - * Handle network event updates coming from the server. - * - * @param object networkInfo - * The network request information. - * @param object packet - * Update details. - */ - handleNetworkEventUpdate: function(networkInfo, packet) { - if (networkInfo.node && this._updateNetMessage(packet.from)) { - this.emit("new-messages", new Set([{ - update: true, - node: networkInfo.node, - response: packet, - }])); - } - - // For unit tests we pass the HTTP activity object to the test callback, - // once requests complete. - if (this.owner.lastFinishedRequestCallback && - networkInfo.updates.indexOf("responseContent") > -1 && - networkInfo.updates.indexOf("eventTimings") > -1) { - this.owner.lastFinishedRequestCallback(networkInfo, this); - } - }, - - /** - * Update an output message to reflect the latest state of a network request, - * given a network event actor ID. - * - * @private - * @param string actorId - * The network event actor ID for which you want to update the message. - * @return boolean - * |true| if the message node was updated, or |false| otherwise. - */ - _updateNetMessage: function(actorId) { - let networkInfo = this.webConsoleClient.getNetworkRequest(actorId); - if (!networkInfo || !networkInfo.node) { - return false; - } - - let messageNode = networkInfo.node; - let updates = networkInfo.updates; - let hasEventTimings = updates.indexOf("eventTimings") > -1; - let hasResponseStart = updates.indexOf("responseStart") > -1; - let request = networkInfo.request; - let methodText = (networkInfo.isXHR) ? - request.method + " XHR" : request.method; - let response = networkInfo.response; - let updated = false; - - if (hasEventTimings || hasResponseStart) { - let status = []; - if (response.httpVersion && response.status) { - status = [response.httpVersion, response.status, response.statusText]; - } - if (hasEventTimings) { - status.push(l10n.getFormatStr("NetworkPanel.durationMS", - [networkInfo.totalTime])); - } - let statusText = "[" + status.join(" ") + "]"; - - let statusNode = messageNode.getElementsByClassName("status")[0]; - statusNode.textContent = statusText; - - messageNode.clipboardText = [methodText, request.url, statusText] - .join(" "); - - if (hasResponseStart && response.status >= MIN_HTTP_ERROR_CODE && - response.status <= MAX_HTTP_ERROR_CODE) { - this.setMessageType(messageNode, CATEGORY_NETWORK, SEVERITY_ERROR); - } - - updated = true; - } - - if (messageNode._netPanel) { - messageNode._netPanel.update(); - } - - return updated; - }, - - /** - * Opens the network monitor and highlights the specified request. - * - * @param string requestId - * The actor ID of the network request. - */ - openNetworkPanel: function(requestId) { - let toolbox = gDevTools.getToolbox(this.owner.target); - // The browser console doesn't have a toolbox. - if (!toolbox) { - return; - } - return toolbox.selectTool("netmonitor").then(panel => { - return panel.panelWin.NetMonitorController.inspectRequest(requestId); - }); - }, - - /** - * Handler for page location changes. - * - * @param string uri - * New page location. - * @param string title - * New page title. - */ - onLocationChange: function(uri, title) { - this.contentLocation = uri; - if (this.owner.onLocationChange) { - this.owner.onLocationChange(uri, title); - } - }, - - /** - * Handler for the tabNavigated notification. - * - * @param string event - * Event name. - * @param object packet - * Notification packet received from the server. - */ - handleTabNavigated: function(event, packet) { - if (event == "will-navigate") { - if (this.persistLog) { - let marker = new Messages.NavigationMarker(packet, Date.now()); - this.output.addMessage(marker); - } else { - this.jsterm.clearOutput(); - } - } - - if (packet.url) { - this.onLocationChange(packet.url, packet.title); - } - - if (event == "navigate" && !packet.nativeConsoleAPI) { - this.logWarningAboutReplacedAPI(); - } - }, - - /** - * Output a message node. This filters a node appropriately, then sends it to - * the output, regrouping and pruning output as necessary. - * - * Note: this call is async - the given message node may not be displayed when - * you call this method. - * - * @param integer category - * The category of the message you want to output. See the CATEGORY_* - * constants. - * @param function|nsIDOMElement methodOrNode - * The method that creates the message element to send to the output or - * the actual element. If a method is given it will be bound to the HUD - * object and the arguments will be |args|. - * @param array [args] - * If a method is given to output the message element then the method - * will be invoked with the list of arguments given here. The last - * object in this array should be the packet received from the - * back end. - */ - outputMessage: function(category, methodOrNode, args) { - if (!this._outputQueue.length) { - // If the queue is empty we consider that now was the last output flush. - // This avoid an immediate output flush when the timer executes. - this._lastOutputFlush = Date.now(); - } - - this._outputQueue.push([category, methodOrNode, args]); - - this._initOutputTimer(); - }, - - /** - * Try to flush the output message queue. This takes the messages in the - * output queue and displays them. Outputting stops at MESSAGES_IN_INTERVAL. - * Further output is queued to happen later - see OUTPUT_INTERVAL. - * - * @private - */ - _flushMessageQueue: function() { - this._outputTimerInitialized = false; - if (!this._outputTimer) { - return; - } - - let startTime = Date.now(); - let timeSinceFlush = startTime - this._lastOutputFlush; - let shouldThrottle = this._outputQueue.length > MESSAGES_IN_INTERVAL && - timeSinceFlush < THROTTLE_UPDATES; - - // Determine how many messages we can display now. - let toDisplay = Math.min(this._outputQueue.length, MESSAGES_IN_INTERVAL); - - // If there aren't any messages to display (because of throttling or an - // empty queue), then take care of some cleanup. Destroy items that were - // pruned from the outputQueue before being displayed. - if (shouldThrottle || toDisplay < 1) { - while (this._itemDestroyQueue.length) { - if ((Date.now() - startTime) > MAX_CLEANUP_TIME) { - break; - } - this._destroyItem(this._itemDestroyQueue.pop()); - } - - this._initOutputTimer(); - return; - } - - // Try to prune the message queue. - let shouldPrune = false; - if (this._outputQueue.length > toDisplay && this._pruneOutputQueue()) { - toDisplay = Math.min(this._outputQueue.length, toDisplay); - shouldPrune = true; - } - - let batch = this._outputQueue.splice(0, toDisplay); - let outputNode = this.outputNode; - let lastVisibleNode = null; - let scrollNode = this.outputWrapper; - let hudIdSupportsString = WebConsoleUtils.supportsString(this.hudId); - - // We won't bother to try to restore scroll position if this is showing - // a lot of messages at once (and there are still items in the queue). - // It is going to purge whatever you were looking at anyway. - let scrolledToBottom = - shouldPrune || Utils.isOutputScrolledToBottom(outputNode, scrollNode); - - // Output the current batch of messages. - let messages = new Set(); - for (let i = 0; i < batch.length; i++) { - let item = batch[i]; - let result = this._outputMessageFromQueue(hudIdSupportsString, item); - if (result) { - messages.add({ - node: result.isRepeated ? result.isRepeated : result.node, - response: result.message, - update: !!result.isRepeated, - }); - - if (result.visible && result.node == this.outputNode.lastChild) { - lastVisibleNode = result.node; - } - } - } - - let oldScrollHeight = 0; - let removedNodes = 0; - - // Prune messages from the DOM, but only if needed. - if (shouldPrune || !this._outputQueue.length) { - // Only bother measuring the scrollHeight if not scrolled to bottom, - // since the oldScrollHeight will not be used if it is. - if (!scrolledToBottom) { - oldScrollHeight = scrollNode.scrollHeight; - } - - let categories = Object.keys(this._pruneCategoriesQueue); - categories.forEach(function _pruneOutput(category) { - removedNodes += this.pruneOutputIfNecessary(category); - }, this); - this._pruneCategoriesQueue = {}; - } - - let isInputOutput = lastVisibleNode && - (lastVisibleNode.category == CATEGORY_INPUT || - lastVisibleNode.category == CATEGORY_OUTPUT); - - // Scroll to the new node if it is not filtered, and if the output node is - // scrolled at the bottom or if the new node is a jsterm input/output - // message. - if (lastVisibleNode && (scrolledToBottom || isInputOutput)) { - Utils.scrollToVisible(lastVisibleNode); - } else if (!scrolledToBottom && removedNodes > 0 && - oldScrollHeight != scrollNode.scrollHeight) { - // If there were pruned messages and if scroll is not at the bottom, then - // we need to adjust the scroll location. - scrollNode.scrollTop -= oldScrollHeight - scrollNode.scrollHeight; - } - - if (messages.size) { - this.emit("new-messages", messages); - } - - // If the output queue is empty, then run _flushCallback. - if (this._outputQueue.length === 0 && this._flushCallback) { - if (this._flushCallback() === false) { - this._flushCallback = null; - } - } - - this._initOutputTimer(); - - this._lastOutputFlush = Date.now(); - }, - - /** - * Initialize the output timer. - * @private - */ - _initOutputTimer: function() { - let panelIsDestroyed = !this._outputTimer; - let alreadyScheduled = this._outputTimerInitialized; - let nothingToDo = !this._itemDestroyQueue.length && - !this._outputQueue.length; - - // Don't schedule a callback in the following cases: - if (panelIsDestroyed || alreadyScheduled || nothingToDo) { - return; - } - - this._outputTimerInitialized = true; - this._outputTimer.initWithCallback(this._flushMessageQueue, - OUTPUT_INTERVAL, - Ci.nsITimer.TYPE_ONE_SHOT); - }, - - /** - * Output a message from the queue. - * - * @private - * @param nsISupportsString hudIdSupportsString - * The HUD ID as an nsISupportsString. - * @param array item - * An item from the output queue - this item represents a message. - * @return object - * An object that holds the following properties: - * - node: the DOM element of the message. - * - isRepeated: the DOM element of the original message, if this is - * a repeated message, otherwise null. - * - visible: boolean that tells if the message is visible. - */ - _outputMessageFromQueue: function(hudIdSupportsString, item) { - let [, methodOrNode, args] = item; - - // The last object in the args array should be message - // object or response packet received from the server. - let message = (args && args.length) ? args[args.length - 1] : null; - - let node = typeof methodOrNode == "function" ? - methodOrNode.apply(this, args || []) : - methodOrNode; - if (!node) { - return null; - } - - let isFiltered = this.filterMessageNode(node); - - let isRepeated = this._filterRepeatedMessage(node); - - let visible = !isRepeated && !isFiltered; - if (!isRepeated) { - this.outputNode.appendChild(node); - this._pruneCategoriesQueue[node.category] = true; - - let nodeID = node.getAttribute("id"); - Services.obs.notifyObservers(hudIdSupportsString, - "web-console-message-created", nodeID); - } - - if (node._onOutput) { - node._onOutput(); - delete node._onOutput; - } - - return { - visible: visible, - node: node, - isRepeated: isRepeated, - message: message - }; - }, +const VARIABLES_VIEW_URL = "chrome://devtools/content/shared/widgets/VariablesView.xul"; - /** - * Prune the queue of messages to display. This avoids displaying messages - * that will be removed at the end of the queue anyway. - * @private - */ - _pruneOutputQueue: function() { - let nodes = {}; - - // Group the messages per category. - this._outputQueue.forEach(function(item, index) { - let [category] = item; - if (!(category in nodes)) { - nodes[category] = []; - } - nodes[category].push(index); - }, this); - - let pruned = 0; - - // Loop through the categories we found and prune if needed. - for (let category in nodes) { - let limit = Utils.logLimitForCategory(category); - let indexes = nodes[category]; - if (indexes.length > limit) { - let n = Math.max(0, indexes.length - limit); - pruned += n; - for (let i = n - 1; i >= 0; i--) { - this._itemDestroyQueue.push(this._outputQueue[indexes[i]]); - this._outputQueue.splice(indexes[i], 1); - } - } - } - - return pruned; - }, - - /** - * Destroy an item that was once in the outputQueue but isn't needed - * after all. - * - * @private - * @param array item - * The item you want to destroy. Does not remove it from the output - * queue. - */ - _destroyItem: function(item) { - // TODO: handle object releasing in a more elegant way once all console - // messages use the new API - bug 778766. - let [category, methodOrNode, args] = item; - if (typeof methodOrNode != "function" && methodOrNode._objectActors) { - for (let actor of methodOrNode._objectActors) { - this._releaseObject(actor); - } - methodOrNode._objectActors.clear(); - } - - if (methodOrNode == this.output._flushMessageQueue && - args[0]._objectActors) { - for (let arg of args) { - if (!arg._objectActors) { - continue; - } - for (let actor of arg._objectActors) { - this._releaseObject(actor); - } - arg._objectActors.clear(); - } - } - - if (category == CATEGORY_NETWORK) { - let connectionId = null; - if (methodOrNode == this.logNetEvent) { - connectionId = args[0].actor; - } else if (typeof methodOrNode != "function") { - connectionId = methodOrNode._connectionId; - } - if (connectionId && - this.webConsoleClient.hasNetworkRequest(connectionId)) { - this.webConsoleClient.removeNetworkRequest(connectionId); - this._releaseObject(connectionId); - } - } else if (category == CATEGORY_WEBDEV && - methodOrNode == this.logConsoleAPIMessage) { - args[0].arguments.forEach((value) => { - if (WebConsoleUtils.isActorGrip(value)) { - this._releaseObject(value.actor); - } - }); - } else if (category == CATEGORY_JS && - methodOrNode == this.reportPageError) { - let pageError = args[1]; - for (let prop of ["errorMessage", "lineText"]) { - let grip = pageError[prop]; - if (WebConsoleUtils.isActorGrip(grip)) { - this._releaseObject(grip.actor); - } - } - } else if (category == CATEGORY_JS && - methodOrNode == this._reportLogMessage) { - if (WebConsoleUtils.isActorGrip(args[0].message)) { - this._releaseObject(args[0].message.actor); - } - } - }, - - /** - * Ensures that the number of message nodes of type category don't exceed that - * category's line limit by removing old messages as needed. - * - * @param integer category - * The category of message nodes to prune if needed. - * @return number - * The number of removed nodes. - */ - pruneOutputIfNecessary: function(category) { - let logLimit = Utils.logLimitForCategory(category); - let messageNodes = this.outputNode.querySelectorAll(".message[category=" + - CATEGORY_CLASS_FRAGMENTS[category] + "]"); - let n = Math.max(0, messageNodes.length - logLimit); - [...messageNodes].slice(0, n).forEach(this.removeOutputMessage, this); - return n; - }, - - /** - * Remove a given message from the output. - * - * @param nsIDOMNode node - * The message node you want to remove. - */ - removeOutputMessage: function(node) { - if (node._messageObject) { - node._messageObject.destroy(); - } - - if (node._objectActors) { - for (let actor of node._objectActors) { - this._releaseObject(actor); - } - node._objectActors.clear(); - } - - if (node.category == CATEGORY_CSS || - node.category == CATEGORY_SECURITY) { - let repeatNode = node.getElementsByClassName("message-repeats")[0]; - if (repeatNode && repeatNode._uid) { - delete this._repeatNodes[repeatNode._uid]; - } - } else if (node._connectionId && - node.category == CATEGORY_NETWORK) { - this.webConsoleClient.removeNetworkRequest(node._connectionId); - this._releaseObject(node._connectionId); - } else if (node.classList.contains("inlined-variables-view")) { - let view = node._variablesView; - if (view) { - view.controller.releaseActors(); - } - node._variablesView = null; - } - - node.remove(); - }, - - /** - * Given a category and message body, creates a DOM node to represent an - * incoming message. The timestamp is automatically added. - * - * @param number category - * The category of the message: one of the CATEGORY_* constants. - * @param number severity - * The severity of the message: one of the SEVERITY_* constants; - * @param string|nsIDOMNode body - * The body of the message, either a simple string or a DOM node. - * @param string sourceURL [optional] - * The URL of the source file that emitted the error. - * @param number sourceLine [optional] - * The line number on which the error occurred. If zero or omitted, - * there is no line number associated with this message. - * @param string clipboardText [optional] - * The text that should be copied to the clipboard when this node is - * copied. If omitted, defaults to the body text. If `body` is not - * a string, then the clipboard text must be supplied. - * @param number level [optional] - * The level of the console API message. - * @param number timestamp [optional] - * The timestamp to use for this message node. If omitted, the current - * date and time is used. - * @return nsIDOMNode - * The message node: a DIV ready to be inserted into the Web Console - * output node. - */ - createMessageNode: function(category, severity, body, sourceURL, sourceLine, - clipboardText, level, timestamp) { - if (typeof body != "string" && clipboardText == null && body.innerText) { - clipboardText = body.innerText; - } - - let indentNode = this.document.createElementNS(XHTML_NS, "span"); - indentNode.className = "indent"; - - // Apply the current group by indenting appropriately. - let indent = this.groupDepth * GROUP_INDENT; - indentNode.style.width = indent + "px"; - - // Make the icon container, which is a vertical box. Its purpose is to - // ensure that the icon stays anchored at the top of the message even for - // long multi-line messages. - let iconContainer = this.document.createElementNS(XHTML_NS, "span"); - iconContainer.className = "icon"; - - // Create the message body, which contains the actual text of the message. - let bodyNode = this.document.createElementNS(XHTML_NS, "span"); - bodyNode.className = "message-body-wrapper message-body devtools-monospace"; - - // Store the body text, since it is needed later for the variables view. - let storedBody = body; - // If a string was supplied for the body, turn it into a DOM node and an - // associated clipboard string now. - clipboardText = clipboardText || - (body + (sourceURL ? " @ " + sourceURL : "") + - (sourceLine ? ":" + sourceLine : "")); - - timestamp = timestamp || Date.now(); - - // Create the containing node and append all its elements to it. - let node = this.document.createElementNS(XHTML_NS, "div"); - node.id = "console-msg-" + gSequenceId(); - node.className = "message"; - node.clipboardText = clipboardText; - node.timestamp = timestamp; - this.setMessageType(node, category, severity); - - if (body instanceof Ci.nsIDOMNode) { - bodyNode.appendChild(body); - } else { - let str = undefined; - if (level == "dir") { - str = VariablesView.getString(body.arguments[0]); - } else { - str = body; - } - - if (str !== undefined) { - body = this.document.createTextNode(str); - bodyNode.appendChild(body); - } - } - - // Add the message repeats node only when needed. - let repeatNode = null; - if (category != CATEGORY_INPUT && - category != CATEGORY_OUTPUT && - category != CATEGORY_NETWORK && - !(category == CATEGORY_CSS && severity == SEVERITY_LOG)) { - repeatNode = this.document.createElementNS(XHTML_NS, "span"); - repeatNode.setAttribute("value", "1"); - repeatNode.className = "message-repeats"; - repeatNode.textContent = 1; - repeatNode._uid = [bodyNode.textContent, category, severity, level, - sourceURL, sourceLine].join(":"); - } - - // Create the timestamp. - let timestampNode = this.document.createElementNS(XHTML_NS, "span"); - timestampNode.className = "timestamp devtools-monospace"; - - let timestampString = l10n.timestampString(timestamp); - timestampNode.textContent = timestampString + " "; - - // Create the source location (e.g. www.example.com:6) that sits on the - // right side of the message, if applicable. - let locationNode; - if (sourceURL && IGNORED_SOURCE_URLS.indexOf(sourceURL) == -1) { - locationNode = this.createLocationNode({url: sourceURL, - line: sourceLine}); - } - - node.appendChild(timestampNode); - node.appendChild(indentNode); - node.appendChild(iconContainer); - - // Display the variables view after the message node. - if (level == "dir") { - let options = { - objectActor: storedBody.arguments[0], - targetElement: bodyNode, - hideFilterInput: true, - }; - this.jsterm.openVariablesView(options).then((view) => { - node._variablesView = view; - if (node.classList.contains("hidden-message")) { - node.classList.remove("hidden-message"); - } - }); - - node.classList.add("inlined-variables-view"); - } - - node.appendChild(bodyNode); - if (repeatNode) { - node.appendChild(repeatNode); - } - if (locationNode) { - node.appendChild(locationNode); - } - node.appendChild(this.document.createTextNode("\n")); - - return node; - }, - - /** - * Creates the anchor that displays the textual location of an incoming - * message. - * - * @param object aLocation - * An object containing url, line and column number of the message - * source (destructured). - * @param string target [optional] - * Tells which tool to open the link with, on click. Supported tools: - * jsdebugger, styleeditor, scratchpad. - * @return nsIDOMNode - * The new anchor element, ready to be added to the message node. - */ - createLocationNode: function({url, line, column}, target) { - if (!url) { - url = ""; - } - let locationNode = this.document.createElementNS(XHTML_NS, "a"); - let filenameNode = this.document.createElementNS(XHTML_NS, "span"); - - // Create the text, which consists of an abbreviated version of the URL - // Scratchpad URLs should not be abbreviated. - let filename; - let fullURL; - let isScratchpad = false; - - if (/^Scratchpad\/\d+$/.test(url)) { - filename = url; - fullURL = url; - isScratchpad = true; - } else { - fullURL = url.split(" -> ").pop(); - filename = WebConsoleUtils.abbreviateSourceURL(fullURL); - } - - filenameNode.className = "filename"; - filenameNode.textContent = - " " + (filename || l10n.getStr("unknownLocation")); - locationNode.appendChild(filenameNode); - - locationNode.href = isScratchpad || !fullURL ? "#" : fullURL; - locationNode.draggable = false; - if (target) { - locationNode.target = target; - } - locationNode.setAttribute("title", url); - locationNode.className = "message-location theme-link devtools-monospace"; - - // Make the location clickable. - let onClick = () => { - let nodeTarget = locationNode.target; - if (nodeTarget == "scratchpad" || isScratchpad) { - this.owner.viewSourceInScratchpad(url, line); - return; - } - - let category = locationNode.parentNode.category; - if (nodeTarget == "styleeditor" || category == CATEGORY_CSS) { - this.owner.viewSourceInStyleEditor(fullURL, line); - } else if (nodeTarget == "jsdebugger" || - category == CATEGORY_JS || category == CATEGORY_WEBDEV) { - this.owner.viewSourceInDebugger(fullURL, line); - } else { - this.owner.viewSource(fullURL, line); - } - }; - - if (fullURL) { - this._addMessageLinkCallback(locationNode, onClick); - } - - if (line) { - let lineNumberNode = this.document.createElementNS(XHTML_NS, "span"); - lineNumberNode.className = "line-number"; - lineNumberNode.textContent = - ":" + line + (column >= 0 ? ":" + column : ""); - locationNode.appendChild(lineNumberNode); - locationNode.sourceLine = line; - } - - return locationNode; - }, - - /** - * Adjusts the category and severity of the given message. - * - * @param nsIDOMNode messageNode - * The message node to alter. - * @param number category - * The category for the message; one of the CATEGORY_ constants. - * @param number severity - * The severity for the message; one of the SEVERITY_ constants. - * @return void - */ - setMessageType: function(messageNode, category, severity) { - messageNode.category = category; - messageNode.severity = severity; - messageNode.setAttribute("category", CATEGORY_CLASS_FRAGMENTS[category]); - messageNode.setAttribute("severity", SEVERITY_CLASS_FRAGMENTS[severity]); - messageNode.setAttribute("filter", - MESSAGE_PREFERENCE_KEYS[category][severity]); - }, - - /** - * Add the mouse event handlers needed to make a link. - * - * @private - * @param nsIDOMNode node - * The node for which you want to add the event handlers. - * @param function callback - * The function you want to invoke on click. - */ - _addMessageLinkCallback: function(node, callback) { - node.addEventListener("mousedown", (event) => { - this._mousedown = true; - this._startX = event.clientX; - this._startY = event.clientY; - }, false); - - node.addEventListener("click", (event) => { - let mousedown = this._mousedown; - this._mousedown = false; - - event.preventDefault(); - - // Do not allow middle/right-click or 2+ clicks. - if (event.detail != 1 || event.button != 0) { - return; - } - - // If this event started with a mousedown event and it ends at a different - // location, we consider this text selection. - if (mousedown && - (this._startX != event.clientX) && - (this._startY != event.clientY)) { - this._startX = this._startY = undefined; - return; - } - - this._startX = this._startY = undefined; - - callback.call(this, event); - }, false); - }, - - _addFocusCallback: function(node, callback) { - node.addEventListener("mousedown", (event) => { - this._mousedown = true; - this._startX = event.clientX; - this._startY = event.clientY; - }, false); - - node.addEventListener("click", (event) => { - let mousedown = this._mousedown; - this._mousedown = false; - - // Do not allow middle/right-click or 2+ clicks. - if (event.detail != 1 || event.button != 0) { - return; - } - - // If this event started with a mousedown event and it ends at a different - // location, we consider this text selection. - // Add a fuzz modifier of two pixels in any direction to account for - // sloppy clicking. - if (mousedown && - (Math.abs(event.clientX - this._startX) >= 2) && - (Math.abs(event.clientY - this._startY) >= 1)) { - this._startX = this._startY = undefined; - return; - } - - this._startX = this._startY = undefined; - - callback.call(this, event); - }, false); - }, - - /** - * Handler for the pref-changed event coming from the toolbox. - * Currently this function only handles the timestamps preferences. - * - * @private - * @param object event - * This parameter is a string that holds the event name - * pref-changed in this case. - * @param object data - * This is the pref-changed data object. - */ - _onToolboxPrefChanged: function(event, data) { - if (data.pref == PREF_MESSAGE_TIMESTAMP) { - if (data.newValue) { - this.outputNode.classList.remove("hideTimestamps"); - } else { - this.outputNode.classList.add("hideTimestamps"); - } - } - }, - - /** - * Copies the selected items to the system clipboard. - * - * @param object options - * - linkOnly: - * An optional flag to copy only URL without other meta-information. - * Default is false. - * - contextmenu: - * An optional flag to copy the last clicked item which brought - * up the context menu if nothing is selected. Default is false. - */ - copySelectedItems: function(options) { - options = options || { linkOnly: false, contextmenu: false }; - - // Gather up the selected items and concatenate their clipboard text. - let strings = []; - - let children = this.output.getSelectedMessages(); - if (!children.length && options.contextmenu) { - children = [this._contextMenuHandler.lastClickedMessage]; - } - - for (let item of children) { - // Ensure the selected item hasn't been filtered by type or string. - if (!item.classList.contains("filtered-by-type") && - !item.classList.contains("filtered-by-string")) { - if (options.linkOnly) { - strings.push(item.url); - } else { - strings.push(item.clipboardText); - } - } - } - - clipboardHelper.copyString(strings.join("\n")); - }, - - /** - * Object properties provider. This function gives you the properties of the - * remote object you want. - * - * @param string actor - * The object actor ID from which you want the properties. - * @param function callback - * Function you want invoked once the properties are received. - */ - objectPropertiesProvider: function(actor, callback) { - this.webConsoleClient.inspectObjectProperties(actor, - function(response) { - if (response.error) { - Cu.reportError("Failed to retrieve the object properties from the " + - "server. Error: " + response.error); - return; - } - callback(response.properties); - }); - }, - - /** - * Release an actor. - * - * @private - * @param string actor - * The actor ID you want to release. - */ - _releaseObject: function(actor) { - if (this.proxy) { - this.proxy.releaseActor(actor); - } - }, - - /** - * Open the selected item's URL in a new tab. - */ - openSelectedItemInTab: function() { - let item = this.output.getSelectedMessages(1)[0] || - this._contextMenuHandler.lastClickedMessage; - - if (!item || !item.url) { - return; - } - - this.owner.openLink(item.url); - }, - - /** - * Destroy the WebConsoleFrame object. Call this method to avoid memory leaks - * when the Web Console is closed. - * - * @return object - * A promise that is resolved when the WebConsoleFrame instance is - * destroyed. - */ - destroy: function() { - if (this._destroyer) { - return this._destroyer.promise; - } - - this._destroyer = promise.defer(); - - let toolbox = gDevTools.getToolbox(this.owner.target); - if (toolbox) { - toolbox.off("webconsole-selected", this._onPanelSelected); - } - - gDevTools.off("pref-changed", this._onToolboxPrefChanged); - this.window.removeEventListener("resize", this.resize, true); - - this._repeatNodes = {}; - this._outputQueue.forEach(this._destroyItem, this); - this._outputQueue = []; - this._itemDestroyQueue.forEach(this._destroyItem, this); - this._itemDestroyQueue = []; - this._pruneCategoriesQueue = {}; - this.webConsoleClient.clearNetworkRequests(); - - if (this._outputTimerInitialized) { - this._outputTimerInitialized = false; - this._outputTimer.cancel(); - } - this._outputTimer = null; - if (this.jsterm) { - this.jsterm.off("sidebar-opened", this.resize); - this.jsterm.off("sidebar-closed", this.resize); - this.jsterm.destroy(); - this.jsterm = null; - } - this.output.destroy(); - this.output = null; - - if (this._contextMenuHandler) { - this._contextMenuHandler.destroy(); - this._contextMenuHandler = null; - } - - this._commandController = null; - - let onDestroy = () => { - this._destroyer.resolve(null); - }; - - if (this.proxy) { - this.proxy.disconnect().then(onDestroy); - this.proxy = null; - } else { - onDestroy(); - } - - return this._destroyer.promise; - }, -}; - -/** - * @see VariablesView.simpleValueEvalMacro - */ -function simpleValueEvalMacro(item, currentString) { - return VariablesView.simpleValueEvalMacro(item, currentString, "_self"); -} - -/** - * @see VariablesView.overrideValueEvalMacro - */ -function overrideValueEvalMacro(item, currentString) { - return VariablesView.overrideValueEvalMacro(item, currentString, "_self"); -} - -/** - * @see VariablesView.getterOrSetterEvalMacro - */ -function getterOrSetterEvalMacro(item, currentString) { - return VariablesView.getterOrSetterEvalMacro(item, currentString, "_self"); -} +const PREF_INPUT_HISTORY_COUNT = "devtools.webconsole.inputHistoryCount"; +const PREF_AUTO_MULTILINE = "devtools.webconsole.autoMultiline"; /** * Create a JSTerminal (a JavaScript command line). This is attached to an * existing HeadsUpDisplay (a Web Console instance). This code is responsible * with handling command line input, code evaluation and result output. * * @constructor * @param object webConsoleFrame @@ -4492,788 +1685,36 @@ JSTerm.prototype = { this.inputNode.removeEventListener("keyup", this._inputEventHandler, false); this.inputNode.removeEventListener("focus", this._focusEventHandler, false); this.hud.window.removeEventListener("blur", this._blurEventHandler, false); this.hud = null; }, }; -/** - * Utils: a collection of globally used functions. - */ -var Utils = { - /** - * Scrolls a node so that it's visible in its containing element. - * - * @param nsIDOMNode node - * The node to make visible. - * @returns void - */ - scrollToVisible: function(node) { - node.scrollIntoView(false); - }, - - /** - * Check if the given output node is scrolled to the bottom. - * - * @param nsIDOMNode outputNode - * @param nsIDOMNode scrollNode - * @return boolean - * True if the output node is scrolled to the bottom, or false - * otherwise. - */ - isOutputScrolledToBottom: function(outputNode, scrollNode) { - let lastNodeHeight = outputNode.lastChild ? - outputNode.lastChild.clientHeight : 0; - return scrollNode.scrollTop + scrollNode.clientHeight >= - scrollNode.scrollHeight - lastNodeHeight / 2; - }, - - /** - * Determine the category of a given nsIScriptError. - * - * @param nsIScriptError scriptError - * The script error you want to determine the category for. - * @return CATEGORY_JS|CATEGORY_CSS|CATEGORY_SECURITY - * Depending on the script error CATEGORY_JS, CATEGORY_CSS, or - * CATEGORY_SECURITY can be returned. - */ - categoryForScriptError: function(scriptError) { - let category = scriptError.category; - - if (/^(?:CSS|Layout)\b/.test(category)) { - return CATEGORY_CSS; - } - - switch (category) { - case "Mixed Content Blocker": - case "Mixed Content Message": - case "CSP": - case "Invalid HSTS Headers": - case "Invalid HPKP Headers": - case "SHA-1 Signature": - case "Insecure Password Field": - case "SSL": - case "CORS": - case "Iframe Sandbox": - case "Tracking Protection": - case "Sub-resource Integrity": - return CATEGORY_SECURITY; - - default: - return CATEGORY_JS; - } - }, - - /** - * Retrieve the limit of messages for a specific category. - * - * @param number category - * The category of messages you want to retrieve the limit for. See the - * CATEGORY_* constants. - * @return number - * The number of messages allowed for the specific category. - */ - logLimitForCategory: function(category) { - let logLimit = DEFAULT_LOG_LIMIT; - - try { - let prefName = CATEGORY_CLASS_FRAGMENTS[category]; - logLimit = Services.prefs.getIntPref("devtools.hud.loglimit." + prefName); - logLimit = Math.max(logLimit, 1); - } catch (e) { - // Ignore any exceptions - } - - return logLimit; - }, -}; - -// //////////////////////////////////////////////////////////////////////////// -// CommandController -// //////////////////////////////////////////////////////////////////////////// - -/** - * A controller (an instance of nsIController) that makes editing actions - * behave appropriately in the context of the Web Console. - */ -function CommandController(webConsole) { - this.owner = webConsole; -} - -CommandController.prototype = { - /** - * Selects all the text in the HUD output. - */ - selectAll: function() { - this.owner.output.selectAllMessages(); - }, - - /** - * Open the URL of the selected message in a new tab. - */ - openURL: function() { - this.owner.openSelectedItemInTab(); - }, - - copyURL: function() { - this.owner.copySelectedItems({ linkOnly: true, contextmenu: true }); - }, - - /** - * Copies the last clicked message. - */ - copyLastClicked: function() { - this.owner.copySelectedItems({ linkOnly: false, contextmenu: true }); - }, - - supportsCommand: function(command) { - if (!this.owner || !this.owner.output) { - return false; - } - return this.isCommandEnabled(command); - }, - - isCommandEnabled: function(command) { - switch (command) { - case "consoleCmd_openURL": - case "consoleCmd_copyURL": { - // Only enable URL-related actions if node is Net Activity. - let selectedItem = this.owner.output.getSelectedMessages(1)[0] || - this.owner._contextMenuHandler.lastClickedMessage; - return selectedItem && "url" in selectedItem; - } - case "cmd_copy": { - // Only copy if we right-clicked the console and there's no selected - // text. With text selected, we want to fall back onto the default - // copy behavior. - return this.owner._contextMenuHandler.lastClickedMessage && - !this.owner.output.getSelectedMessages(1)[0]; - } - case "consoleCmd_clearOutput": - case "cmd_selectAll": - case "cmd_find": - return true; - case "cmd_fontSizeEnlarge": - case "cmd_fontSizeReduce": - case "cmd_fontSizeReset": - case "cmd_close": - return this.owner.owner._browserConsole; - } - return false; - }, - - doCommand: function(command) { - switch (command) { - case "consoleCmd_openURL": - this.openURL(); - break; - case "consoleCmd_copyURL": - this.copyURL(); - break; - case "consoleCmd_clearOutput": - this.owner.jsterm.clearOutput(true); - break; - case "cmd_copy": - this.copyLastClicked(); - break; - case "cmd_find": - this.owner.filterBox.focus(); - break; - case "cmd_selectAll": - this.selectAll(); - break; - case "cmd_fontSizeEnlarge": - this.owner.changeFontSize("+"); - break; - case "cmd_fontSizeReduce": - this.owner.changeFontSize("-"); - break; - case "cmd_fontSizeReset": - this.owner.changeFontSize(""); - break; - case "cmd_close": - this.owner.window.close(); - break; - } - } -}; - -// //////////////////////////////////////////////////////////////////////////// -// Web Console connection proxy -// //////////////////////////////////////////////////////////////////////////// - -/** - * The WebConsoleConnectionProxy handles the connection between the Web Console - * and the application we connect to through the remote debug protocol. - * - * @constructor - * @param object webConsoleFrame - * The WebConsoleFrame object that owns this connection proxy. - * @param RemoteTarget target - * The target that the console will connect to. - */ -function WebConsoleConnectionProxy(webConsoleFrame, target) { - this.webConsoleFrame = webConsoleFrame; - this.target = target; - - this._onPageError = this._onPageError.bind(this); - this._onLogMessage = this._onLogMessage.bind(this); - this._onConsoleAPICall = this._onConsoleAPICall.bind(this); - this._onNetworkEvent = this._onNetworkEvent.bind(this); - this._onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this); - this._onFileActivity = this._onFileActivity.bind(this); - this._onReflowActivity = this._onReflowActivity.bind(this); - this._onServerLogCall = this._onServerLogCall.bind(this); - this._onTabNavigated = this._onTabNavigated.bind(this); - this._onAttachConsole = this._onAttachConsole.bind(this); - this._onCachedMessages = this._onCachedMessages.bind(this); - this._connectionTimeout = this._connectionTimeout.bind(this); - this._onLastPrivateContextExited = - this._onLastPrivateContextExited.bind(this); -} - -WebConsoleConnectionProxy.prototype = { - /** - * The owning Web Console Frame instance. - * - * @see WebConsoleFrame - * @type object - */ - webConsoleFrame: null, - - /** - * The target that the console connects to. - * @type RemoteTarget - */ - target: null, - - /** - * The DebuggerClient object. - * - * @see DebuggerClient - * @type object - */ - client: null, - - /** - * The WebConsoleClient object. - * - * @see WebConsoleClient - * @type object - */ - webConsoleClient: null, - - /** - * Tells if the connection is established. - * @type boolean - */ - connected: false, - - /** - * Timer used for the connection. - * @private - * @type object - */ - _connectTimer: null, - - _connectDefer: null, - _disconnecter: null, - - /** - * The WebConsoleActor ID. - * - * @private - * @type string - */ - _consoleActor: null, - - /** - * Tells if the window.console object of the remote web page is the native - * object or not. - * @private - * @type boolean - */ - _hasNativeConsoleAPI: false, - - /** - * Initialize a debugger client and connect it to the debugger server. - * - * @return object - * A promise object that is resolved/rejected based on the success of - * the connection initialization. - */ - connect: function() { - if (this._connectDefer) { - return this._connectDefer.promise; - } - - this._connectDefer = promise.defer(); - - let timeout = Services.prefs.getIntPref(PREF_CONNECTION_TIMEOUT); - this._connectTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - this._connectTimer.initWithCallback(this._connectionTimeout, - timeout, Ci.nsITimer.TYPE_ONE_SHOT); - - let connPromise = this._connectDefer.promise; - connPromise.then(() => { - this._connectTimer.cancel(); - this._connectTimer = null; - }, () => { - this._connectTimer = null; - }); - - let client = this.client = this.target.client; - - if (this.target.isWorkerTarget) { - // XXXworkers: Not Console API yet inside of workers (Bug 1209353). - } else { - client.addListener("logMessage", this._onLogMessage); - client.addListener("pageError", this._onPageError); - client.addListener("consoleAPICall", this._onConsoleAPICall); - client.addListener("fileActivity", this._onFileActivity); - client.addListener("reflowActivity", this._onReflowActivity); - client.addListener("serverLogCall", this._onServerLogCall); - client.addListener("lastPrivateContextExited", - this._onLastPrivateContextExited); - } - this.target.on("will-navigate", this._onTabNavigated); - this.target.on("navigate", this._onTabNavigated); - - this._consoleActor = this.target.form.consoleActor; - if (this.target.isTabActor) { - let tab = this.target.form; - this.webConsoleFrame.onLocationChange(tab.url, tab.title); - } - this._attachConsole(); - - return connPromise; - }, - - /** - * Connection timeout handler. - * @private - */ - _connectionTimeout: function() { - let error = { - error: "timeout", - message: l10n.getStr("connectionTimeout"), - }; - - this._connectDefer.reject(error); - }, - - /** - * Attach to the Web Console actor. - * @private - */ - _attachConsole: function() { - let listeners = ["PageError", "ConsoleAPI", "NetworkActivity", - "FileActivity"]; - this.client.attachConsole(this._consoleActor, listeners, - this._onAttachConsole); - }, - - /** - * The "attachConsole" response handler. - * - * @private - * @param object response - * The JSON response object received from the server. - * @param object webConsoleClient - * The WebConsoleClient instance for the attached console, for the - * specific tab we work with. - */ - _onAttachConsole: function(response, webConsoleClient) { - if (response.error) { - Cu.reportError("attachConsole failed: " + response.error + " " + - response.message); - this._connectDefer.reject(response); - return; - } - - this.webConsoleClient = webConsoleClient; - this._hasNativeConsoleAPI = response.nativeConsoleAPI; - - // There is no way to view response bodies from the Browser Console, so do - // not waste the memory. - let saveBodies = !this.webConsoleFrame.owner._browserConsole; - this.webConsoleFrame.setSaveRequestAndResponseBodies(saveBodies); - - this.webConsoleClient.on("networkEvent", this._onNetworkEvent); - this.webConsoleClient.on("networkEventUpdate", this._onNetworkEventUpdate); - - let msgs = ["PageError", "ConsoleAPI"]; - this.webConsoleClient.getCachedMessages(msgs, this._onCachedMessages); - - this.webConsoleFrame._onUpdateListeners(); - }, - - /** - * The "cachedMessages" response handler. - * - * @private - * @param object response - * The JSON response object received from the server. - */ - _onCachedMessages: function(response) { - if (response.error) { - Cu.reportError("Web Console getCachedMessages error: " + response.error + - " " + response.message); - this._connectDefer.reject(response); - return; - } - - if (!this._connectTimer) { - // This happens if the promise is rejected (eg. a timeout), but the - // connection attempt is successful, nonetheless. - Cu.reportError("Web Console getCachedMessages error: invalid state."); - } - - let messages = - response.messages.concat(...this.webConsoleClient.getNetworkEvents()); - messages.sort((a, b) => a.timeStamp - b.timeStamp); - - this.webConsoleFrame.displayCachedMessages(messages); - - if (!this._hasNativeConsoleAPI) { - this.webConsoleFrame.logWarningAboutReplacedAPI(); - } - - this.connected = true; - this._connectDefer.resolve(this); - }, - - /** - * The "pageError" message type handler. We redirect any page errors to the UI - * for displaying. - * - * @private - * @param string type - * Message type. - * @param object packet - * The message received from the server. - */ - _onPageError: function(type, packet) { - if (this.webConsoleFrame && packet.from == this._consoleActor) { - this.webConsoleFrame.handlePageError(packet.pageError); - } - }, - - /** - * The "logMessage" message type handler. We redirect any message to the UI - * for displaying. - * - * @private - * @param string type - * Message type. - * @param object packet - * The message received from the server. - */ - _onLogMessage: function(type, packet) { - if (this.webConsoleFrame && packet.from == this._consoleActor) { - this.webConsoleFrame.handleLogMessage(packet); - } - }, - - /** - * The "consoleAPICall" message type handler. We redirect any message to - * the UI for displaying. - * - * @private - * @param string type - * Message type. - * @param object packet - * The message received from the server. - */ - _onConsoleAPICall: function(type, packet) { - if (this.webConsoleFrame && packet.from == this._consoleActor) { - this.webConsoleFrame.handleConsoleAPICall(packet.message); - } - }, - - /** - * The "networkEvent" message type handler. We redirect any message to - * the UI for displaying. - * - * @private - * @param string type - * Message type. - * @param object networkInfo - * The network request information. - */ - _onNetworkEvent: function(type, networkInfo) { - if (this.webConsoleFrame) { - this.webConsoleFrame.handleNetworkEvent(networkInfo); - } - }, - - /** - * The "networkEventUpdate" message type handler. We redirect any message to - * the UI for displaying. - * - * @private - * @param string type - * Message type. - * @param object packet - * The message received from the server. - * @param object networkInfo - * The network request information. - */ - _onNetworkEventUpdate: function(type, { packet, networkInfo }) { - if (this.webConsoleFrame) { - this.webConsoleFrame.handleNetworkEventUpdate(networkInfo, packet); - } - }, - - /** - * The "fileActivity" message type handler. We redirect any message to - * the UI for displaying. - * - * @private - * @param string type - * Message type. - * @param object packet - * The message received from the server. - */ - _onFileActivity: function(type, packet) { - if (this.webConsoleFrame && packet.from == this._consoleActor) { - this.webConsoleFrame.handleFileActivity(packet.uri); - } - }, - - _onReflowActivity: function(type, packet) { - if (this.webConsoleFrame && packet.from == this._consoleActor) { - this.webConsoleFrame.handleReflowActivity(packet); - } - }, - - /** - * The "serverLogCall" message type handler. We redirect any message to - * the UI for displaying. - * - * @private - * @param string type - * Message type. - * @param object packet - * The message received from the server. - */ - _onServerLogCall: function(type, packet) { - if (this.webConsoleFrame && packet.from == this._consoleActor) { - this.webConsoleFrame.handleConsoleAPICall(packet.message); - } - }, - - /** - * The "lastPrivateContextExited" message type handler. When this message is - * received the Web Console UI is cleared. - * - * @private - * @param string type - * Message type. - * @param object packet - * The message received from the server. - */ - _onLastPrivateContextExited: function(type, packet) { - if (this.webConsoleFrame && packet.from == this._consoleActor) { - this.webConsoleFrame.jsterm.clearPrivateMessages(); - } - }, - - /** - * The "will-navigate" and "navigate" event handlers. We redirect any message - * to the UI for displaying. - * - * @private - * @param string event - * Event type. - * @param object packet - * The message received from the server. - */ - _onTabNavigated: function(event, packet) { - if (!this.webConsoleFrame) { - return; - } - - this.webConsoleFrame.handleTabNavigated(event, packet); - }, - - /** - * Release an object actor. - * - * @param string actor - * The actor ID to send the request to. - */ - releaseActor: function(actor) { - if (this.client) { - this.client.release(actor); - } - }, - - /** - * Disconnect the Web Console from the remote server. - * - * @return object - * A promise object that is resolved when disconnect completes. - */ - disconnect: function() { - if (this._disconnecter) { - return this._disconnecter.promise; - } - - this._disconnecter = promise.defer(); - - if (!this.client) { - this._disconnecter.resolve(null); - return this._disconnecter.promise; - } - - this.client.removeListener("logMessage", this._onLogMessage); - this.client.removeListener("pageError", this._onPageError); - this.client.removeListener("consoleAPICall", this._onConsoleAPICall); - this.client.removeListener("fileActivity", this._onFileActivity); - this.client.removeListener("reflowActivity", this._onReflowActivity); - this.client.removeListener("serverLogCall", this._onServerLogCall); - this.client.removeListener("lastPrivateContextExited", - this._onLastPrivateContextExited); - this.webConsoleClient.off("networkEvent", this._onNetworkEvent); - this.webConsoleClient.off("networkEventUpdate", this._onNetworkEventUpdate); - this.target.off("will-navigate", this._onTabNavigated); - this.target.off("navigate", this._onTabNavigated); - - this.client = null; - this.webConsoleClient = null; - this.target = null; - this.connected = false; - this.webConsoleFrame = null; - this._disconnecter.resolve(null); - - return this._disconnecter.promise; - }, -}; - function gSequenceId() { return gSequenceId.n++; } gSequenceId.n = 0; - -// //////////////////////////////////////////////////////////////////////////// -// Context Menu -// //////////////////////////////////////////////////////////////////////////// +exports.gSequenceId = gSequenceId; -/* - * ConsoleContextMenu this used to handle the visibility of context menu items. - * - * @constructor - * @param object owner - * The WebConsoleFrame instance that owns this object. +/** + * @see VariablesView.simpleValueEvalMacro */ -function ConsoleContextMenu(owner) { - this.owner = owner; - this.popup = this.owner.document.getElementById("output-contextmenu"); - this.build = this.build.bind(this); - this.popup.addEventListener("popupshowing", this.build); +function simpleValueEvalMacro(item, currentString) { + return VariablesView.simpleValueEvalMacro(item, currentString, "_self"); } -ConsoleContextMenu.prototype = { - lastClickedMessage: null, - - /* - * Handle to show/hide context menu item. - */ - build: function(event) { - let metadata = this.getSelectionMetadata(event.rangeParent); - for (let element of this.popup.children) { - element.hidden = this.shouldHideMenuItem(element, metadata); - } - }, - - /* - * Get selection information from the view. - * - * @param nsIDOMElement clickElement - * The DOM element the user clicked on. - * @return object - * Selection metadata. - */ - getSelectionMetadata: function(clickElement) { - let metadata = { - selectionType: "", - selection: new Set(), - }; - let selectedItems = this.owner.output.getSelectedMessages(); - if (!selectedItems.length) { - let clickedItem = this.owner.output.getMessageForElement(clickElement); - if (clickedItem) { - this.lastClickedMessage = clickedItem; - selectedItems = [clickedItem]; - } - } - - metadata.selectionType = selectedItems.length > 1 ? "multiple" : "single"; +/** + * @see VariablesView.overrideValueEvalMacro + */ +function overrideValueEvalMacro(item, currentString) { + return VariablesView.overrideValueEvalMacro(item, currentString, "_self"); +} - let selection = metadata.selection; - for (let item of selectedItems) { - switch (item.category) { - case CATEGORY_NETWORK: - selection.add("network"); - break; - case CATEGORY_CSS: - selection.add("css"); - break; - case CATEGORY_JS: - selection.add("js"); - break; - case CATEGORY_WEBDEV: - selection.add("webdev"); - break; - case CATEGORY_SERVER: - selection.add("server"); - break; - } - } - - return metadata; - }, +/** + * @see VariablesView.getterOrSetterEvalMacro + */ +function getterOrSetterEvalMacro(item, currentString) { + return VariablesView.getterOrSetterEvalMacro(item, currentString, "_self"); +} - /* - * Determine if an item should be hidden. - * - * @param nsIDOMElement menuItem - * @param object metadata - * @return boolean - * Whether the given item should be hidden or not. - */ - shouldHideMenuItem: function(menuItem, metadata) { - let selectionType = menuItem.getAttribute("selectiontype"); - if (selectionType && !metadata.selectionType == selectionType) { - return true; - } - - let selection = menuItem.getAttribute("selection"); - if (!selection) { - return false; - } - - let shouldHide = true; - let itemData = selection.split("|"); - for (let type of metadata.selection) { - // check whether this menu item should show or not. - if (itemData.indexOf(type) !== -1) { - shouldHide = false; - break; - } - } - - return shouldHide; - }, - - /** - * Destroy the ConsoleContextMenu object instance. - */ - destroy: function() { - this.popup.removeEventListener("popupshowing", this.build); - this.popup = null; - this.owner = null; - this.lastClickedMessage = null; - }, -}; +exports.JSTerm = JSTerm;
--- a/devtools/client/webconsole/moz.build +++ b/devtools/client/webconsole/moz.build @@ -5,11 +5,12 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] DevToolsModules( 'console-commands.js', 'console-output.js', 'hudservice.js', + 'jsterm.js', 'panel.js', 'webconsole.js', )
--- a/devtools/client/webconsole/webconsole.js +++ b/devtools/client/webconsole/webconsole.js @@ -6,32 +6,32 @@ "use strict"; const {Cc, Ci, Cu} = require("chrome"); const {Utils: WebConsoleUtils, CONSOLE_WORKER_IDS} = require("devtools/shared/webconsole/utils"); const promise = require("promise"); -const Debugger = require("Debugger"); loader.lazyServiceGetter(this, "clipboardHelper", "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper"); loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm"); loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter"); loader.lazyRequireGetter(this, "AutocompletePopup", "devtools/client/shared/autocomplete-popup", true); loader.lazyRequireGetter(this, "ToolSidebar", "devtools/client/framework/sidebar", true); loader.lazyRequireGetter(this, "ConsoleOutput", "devtools/client/webconsole/console-output", true); loader.lazyRequireGetter(this, "Messages", "devtools/client/webconsole/console-output", true); -loader.lazyRequireGetter(this, "asyncStorage", "devtools/shared/async-storage"); loader.lazyRequireGetter(this, "EnvironmentClient", "devtools/shared/client/main", true); loader.lazyRequireGetter(this, "ObjectClient", "devtools/shared/client/main", true); loader.lazyRequireGetter(this, "system", "devtools/shared/system"); loader.lazyRequireGetter(this, "Timers", "sdk/timers"); +loader.lazyRequireGetter(this, "JSTerm", "devtools/client/webconsole/jsterm", true); +loader.lazyRequireGetter(this, "gSequenceId", "devtools/client/webconsole/jsterm", true); loader.lazyImporter(this, "VariablesView", "resource://devtools/client/shared/widgets/VariablesView.jsm"); loader.lazyImporter(this, "VariablesViewController", "resource://devtools/client/shared/widgets/VariablesViewController.jsm"); loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true); loader.lazyImporter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm"); const STRINGS_URI = "chrome://devtools/locale/webconsole.properties"; var l10n = new WebConsoleUtils.L10n(STRINGS_URI); @@ -44,20 +44,16 @@ const TRACKING_PROTECTION_LEARN_MORE = " const INSECURE_PASSWORDS_LEARN_MORE = "https://developer.mozilla.org/docs/Security/InsecurePasswords"; const PUBLIC_KEY_PINS_LEARN_MORE = "https://developer.mozilla.org/docs/Web/Security/Public_Key_Pinning"; const STRICT_TRANSPORT_SECURITY_LEARN_MORE = "https://developer.mozilla.org/docs/Security/HTTP_Strict_Transport_Security"; const WEAK_SIGNATURE_ALGORITHM_LEARN_MORE = "https://developer.mozilla.org/docs/Security/Weak_Signature_Algorithm"; -const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers"; - -const VARIABLES_VIEW_URL = "chrome://devtools/content/shared/widgets/VariablesView.xul"; - const IGNORED_SOURCE_URLS = ["debugger eval code"]; // The amount of time in milliseconds that we wait before performing a live // search. const SEARCH_DELAY = 200; // The number of lines that are displayed in the console output by default, for // each category. The user can change this number by adjusting the hidden @@ -158,20 +154,16 @@ const LEVELS = { const WORKERTYPES_PREFKEYS = [ "sharedworkers", "serviceworkers", "windowlessworkers" ]; // The lowest HTTP response code (inclusive) that is considered an error. const MIN_HTTP_ERROR_CODE = 400; // The highest HTTP response code (inclusive) that is considered an error. const MAX_HTTP_ERROR_CODE = 599; -// Constants used for defining the direction of JSTerm input history navigation. -const HISTORY_BACK = -1; -const HISTORY_FORWARD = 1; - // The indent of a console group in pixels. const GROUP_INDENT = 12; // The number of messages to display in a single display update. If we display // too many messages at once we slow down the Firefox UI too much. const MESSAGES_IN_INTERVAL = DEFAULT_LOG_LIMIT; // The delay (in milliseconds) between display updates - tells how often we @@ -194,18 +186,16 @@ const THROTTLE_UPDATES = 1000; const FILTER_PREFS_PREFIX = "devtools.webconsole.filter."; // The minimum font size. const MIN_FONT_SIZE = 10; const PREF_CONNECTION_TIMEOUT = "devtools.debugger.remote-timeout"; const PREF_PERSISTLOG = "devtools.webconsole.persistlog"; const PREF_MESSAGE_TIMESTAMP = "devtools.webconsole.timestampMessages"; -const PREF_AUTO_MULTILINE = "devtools.webconsole.autoMultiline"; -const PREF_INPUT_HISTORY_COUNT = "devtools.webconsole.inputHistoryCount"; /** * A WebConsoleFrame instance is an interactive console initialized *per target* * that displays console log data as well as provides an interactive terminal to * manipulate the target's document content. * * The WebConsoleFrame is responsible for the actual Web Console UI * implementation. @@ -2085,16 +2075,19 @@ WebConsoleFrame.prototype = { if (this._outputQueue.length === 0 && this._flushCallback) { if (this._flushCallback() === false) { this._flushCallback = null; } } this._initOutputTimer(); + // Resize the output area in case a vertical scrollbar has been added + this.resize(); + this._lastOutputFlush = Date.now(); }, /** * Initialize the output timer. * @private */ _initOutputTimer: function() { @@ -2826,1683 +2819,16 @@ WebConsoleFrame.prototype = { onDestroy(); } return this._destroyer.promise; }, }; /** - * @see VariablesView.simpleValueEvalMacro - */ -function simpleValueEvalMacro(item, currentString) { - return VariablesView.simpleValueEvalMacro(item, currentString, "_self"); -} - -/** - * @see VariablesView.overrideValueEvalMacro - */ -function overrideValueEvalMacro(item, currentString) { - return VariablesView.overrideValueEvalMacro(item, currentString, "_self"); -} - -/** - * @see VariablesView.getterOrSetterEvalMacro - */ -function getterOrSetterEvalMacro(item, currentString) { - return VariablesView.getterOrSetterEvalMacro(item, currentString, "_self"); -} - -/** - * Create a JSTerminal (a JavaScript command line). This is attached to an - * existing HeadsUpDisplay (a Web Console instance). This code is responsible - * with handling command line input, code evaluation and result output. - * - * @constructor - * @param object webConsoleFrame - * The WebConsoleFrame object that owns this JSTerm instance. - */ -function JSTerm(webConsoleFrame) { - this.hud = webConsoleFrame; - this.hudId = this.hud.hudId; - this.inputHistoryCount = Services.prefs.getIntPref(PREF_INPUT_HISTORY_COUNT); - - this.lastCompletion = { value: null }; - this._loadHistory(); - - this._objectActorsInVariablesViews = new Map(); - - this._keyPress = this._keyPress.bind(this); - this._inputEventHandler = this._inputEventHandler.bind(this); - this._focusEventHandler = this._focusEventHandler.bind(this); - this._onKeypressInVariablesView = this._onKeypressInVariablesView.bind(this); - this._blurEventHandler = this._blurEventHandler.bind(this); - - EventEmitter.decorate(this); -} - -JSTerm.prototype = { - SELECTED_FRAME: -1, - - /** - * Load the console history from previous sessions. - * @private - */ - _loadHistory: function() { - this.history = []; - this.historyIndex = this.historyPlaceHolder = 0; - - this.historyLoaded = asyncStorage.getItem("webConsoleHistory") - .then(value => { - if (Array.isArray(value)) { - // Since it was gotten asynchronously, there could be items already in - // the history. It's not likely but stick them onto the end anyway. - this.history = value.concat(this.history); - - // Holds the number of entries in history. This value is incremented - // in this.execute(). - this.historyIndex = this.history.length; - - // Holds the index of the history entry that the user is currently - // viewing. This is reset to this.history.length when this.execute() - // is invoked. - this.historyPlaceHolder = this.history.length; - } - }, console.error); - }, - - /** - * Clear the console history altogether. Note that this will not affect - * other consoles that are already opened (since they have their own copy), - * but it will reset the array for all newly-opened consoles. - * @returns Promise - * Resolves once the changes have been persisted. - */ - clearHistory: function() { - this.history = []; - this.historyIndex = this.historyPlaceHolder = 0; - return this.storeHistory(); - }, - - /** - * Stores the console history for future console instances. - * @returns Promise - * Resolves once the changes have been persisted. - */ - storeHistory: function() { - return asyncStorage.setItem("webConsoleHistory", this.history); - }, - - /** - * Stores the data for the last completion. - * @type object - */ - lastCompletion: null, - - /** - * Array that caches the user input suggestions received from the server. - * @private - * @type array - */ - _autocompleteCache: null, - - /** - * The input that caused the last request to the server, whose response is - * cached in the _autocompleteCache array. - * @private - * @type string - */ - _autocompleteQuery: null, - - /** - * The frameActorId used in the last autocomplete query. Whenever this changes - * the autocomplete cache must be invalidated. - * @private - * @type string - */ - _lastFrameActorId: null, - - /** - * The Web Console sidebar. - * @see this._createSidebar() - * @see Sidebar.jsm - */ - sidebar: null, - - /** - * The Variables View instance shown in the sidebar. - * @private - * @type object - */ - _variablesView: null, - - /** - * Tells if you want the variables view UI updates to be lazy or not. Tests - * disable lazy updates. - * - * @private - * @type boolean - */ - _lazyVariablesView: true, - - /** - * Holds a map between VariablesView instances and sets of ObjectActor IDs - * that have been retrieved from the server. This allows us to release the - * objects when needed. - * - * @private - * @type Map - */ - _objectActorsInVariablesViews: null, - - /** - * Last input value. - * @type string - */ - lastInputValue: "", - - /** - * Tells if the input node changed since the last focus. - * - * @private - * @type boolean - */ - _inputChanged: false, - - /** - * Tells if the autocomplete popup was navigated since the last open. - * - * @private - * @type boolean - */ - _autocompletePopupNavigated: false, - - /** - * History of code that was executed. - * @type array - */ - history: null, - autocompletePopup: null, - inputNode: null, - completeNode: null, - - /** - * Getter for the element that holds the messages we display. - * @type nsIDOMElement - */ - get outputNode() { - return this.hud.outputNode; - }, - - /** - * Getter for the debugger WebConsoleClient. - * @type object - */ - get webConsoleClient() { - return this.hud.webConsoleClient; - }, - - COMPLETE_FORWARD: 0, - COMPLETE_BACKWARD: 1, - COMPLETE_HINT_ONLY: 2, - COMPLETE_PAGEUP: 3, - COMPLETE_PAGEDOWN: 4, - - /** - * Initialize the JSTerminal UI. - */ - init: function() { - let autocompleteOptions = { - onSelect: this.onAutocompleteSelect.bind(this), - onClick: this.acceptProposedCompletion.bind(this), - panelId: "webConsole_autocompletePopup", - listBoxId: "webConsole_autocompletePopupListBox", - position: "before_start", - theme: "auto", - direction: "ltr", - autoSelect: true - }; - this.autocompletePopup = new AutocompletePopup(this.hud.document, - autocompleteOptions); - - let doc = this.hud.document; - let inputContainer = doc.querySelector(".jsterm-input-container"); - this.completeNode = doc.querySelector(".jsterm-complete-node"); - this.inputNode = doc.querySelector(".jsterm-input-node"); - - if (this.hud.owner._browserConsole && - !Services.prefs.getBoolPref("devtools.chrome.enabled")) { - inputContainer.style.display = "none"; - } else { - let okstring = l10n.getStr("selfxss.okstring"); - let msg = l10n.getFormatStr("selfxss.msg", [okstring]); - this._onPaste = WebConsoleUtils.pasteHandlerGen( - this.inputNode, doc.getElementById("webconsole-notificationbox"), - msg, okstring); - this.inputNode.addEventListener("keypress", this._keyPress, false); - this.inputNode.addEventListener("paste", this._onPaste); - this.inputNode.addEventListener("drop", this._onPaste); - this.inputNode.addEventListener("input", this._inputEventHandler, false); - this.inputNode.addEventListener("keyup", this._inputEventHandler, false); - this.inputNode.addEventListener("focus", this._focusEventHandler, false); - } - - this.hud.window.addEventListener("blur", this._blurEventHandler, false); - this.lastInputValue && this.setInputValue(this.lastInputValue); - }, - - focus: function() { - if (!this.inputNode.getAttribute("focused")) { - this.inputNode.focus(); - } - }, - - /** - * The JavaScript evaluation response handler. - * - * @private - * @param function [callback] - * Optional function to invoke when the evaluation result is added to - * the output. - * @param object response - * The message received from the server. - */ - _executeResultCallback: function(callback, response) { - if (!this.hud) { - return; - } - if (response.error) { - Cu.reportError("Evaluation error " + response.error + ": " + - response.message); - return; - } - let errorMessage = response.exceptionMessage; - let result = response.result; - let helperResult = response.helperResult; - let helperHasRawOutput = !!(helperResult || {}).rawOutput; - - if (helperResult && helperResult.type) { - switch (helperResult.type) { - case "clearOutput": - this.clearOutput(); - break; - case "clearHistory": - this.clearHistory(); - break; - case "inspectObject": - this.openVariablesView({ - label: - VariablesView.getString(helperResult.object, { concise: true }), - objectActor: helperResult.object, - }); - break; - case "error": - try { - errorMessage = l10n.getStr(helperResult.message); - } catch (ex) { - errorMessage = helperResult.message; - } - break; - case "help": - this.hud.owner.openLink(HELP_URL); - break; - case "copyValueToClipboard": - clipboardHelper.copyString(helperResult.value); - break; - } - } - - // Hide undefined results coming from JSTerm helper functions. - if (!errorMessage && result && typeof result == "object" && - result.type == "undefined" && - helperResult && !helperHasRawOutput) { - callback && callback(); - return; - } - - let msg = new Messages.JavaScriptEvalOutput(response, errorMessage); - this.hud.output.addMessage(msg); - - if (callback) { - let oldFlushCallback = this.hud._flushCallback; - this.hud._flushCallback = () => { - callback(msg.element); - if (oldFlushCallback) { - oldFlushCallback(); - this.hud._flushCallback = oldFlushCallback; - return true; - } - - return false; - }; - } - - msg._objectActors = new Set(); - - if (WebConsoleUtils.isActorGrip(response.exception)) { - msg._objectActors.add(response.exception.actor); - } - - if (WebConsoleUtils.isActorGrip(result)) { - msg._objectActors.add(result.actor); - } - }, - - /** - * Execute a string. Execution happens asynchronously in the content process. - * - * @param string [executeString] - * The string you want to execute. If this is not provided, the current - * user input is used - taken from |this.getInputValue()|. - * @param function [callback] - * Optional function to invoke when the result is displayed. - * This is deprecated - please use the promise return value instead. - * @returns Promise - * Resolves with the message once the result is displayed. - */ - execute: function(executeString, callback) { - let deferred = promise.defer(); - let resultCallback = function(msg) { - deferred.resolve(msg); - if (callback) { - callback(msg); - } - }; - - // attempt to execute the content of the inputNode - executeString = executeString || this.getInputValue(); - if (!executeString) { - return; - } - - let selectedNodeActor = null; - let inspectorSelection = this.hud.owner.getInspectorSelection(); - if (inspectorSelection && inspectorSelection.nodeFront) { - selectedNodeActor = inspectorSelection.nodeFront.actorID; - } - - let message = new Messages.Simple(executeString, { - category: "input", - severity: "log", - }); - this.hud.output.addMessage(message); - let onResult = this._executeResultCallback.bind(this, resultCallback); - - let options = { - frame: this.SELECTED_FRAME, - selectedNodeActor: selectedNodeActor, - }; - - this.requestEvaluation(executeString, options).then(onResult, onResult); - - // Append a new value in the history of executed code, or overwrite the most - // recent entry. The most recent entry may contain the last edited input - // value that was not evaluated yet. - this.history[this.historyIndex++] = executeString; - this.historyPlaceHolder = this.history.length; - - if (this.history.length > this.inputHistoryCount) { - this.history.splice(0, this.history.length - this.inputHistoryCount); - this.historyIndex = this.historyPlaceHolder = this.history.length; - } - this.storeHistory(); - WebConsoleUtils.usageCount++; - this.setInputValue(""); - this.clearCompletion(); - return deferred.promise; - }, - - /** - * Request a JavaScript string evaluation from the server. - * - * @param string str - * String to execute. - * @param object [options] - * Options for evaluation: - * - bindObjectActor: tells the ObjectActor ID for which you want to do - * the evaluation. The Debugger.Object of the OA will be bound to - * |_self| during evaluation, such that it's usable in the string you - * execute. - * - frame: tells the stackframe depth to evaluate the string in. If - * the jsdebugger is paused, you can pick the stackframe to be used for - * evaluation. Use |this.SELECTED_FRAME| to always pick the - * user-selected stackframe. - * If you do not provide a |frame| the string will be evaluated in the - * global content window. - * - selectedNodeActor: tells the NodeActor ID of the current selection - * in the Inspector, if such a selection exists. This is used by - * helper functions that can evaluate on the current selection. - * @return object - * A promise object that is resolved when the server response is - * received. - */ - requestEvaluation: function(str, options = {}) { - let deferred = promise.defer(); - - function onResult(response) { - if (!response.error) { - deferred.resolve(response); - } else { - deferred.reject(response); - } - } - - let frameActor = null; - if ("frame" in options) { - frameActor = this.getFrameActor(options.frame); - } - - let evalOptions = { - bindObjectActor: options.bindObjectActor, - frameActor: frameActor, - selectedNodeActor: options.selectedNodeActor, - selectedObjectActor: options.selectedObjectActor, - }; - - this.webConsoleClient.evaluateJSAsync(str, onResult, evalOptions); - return deferred.promise; - }, - - /** - * Retrieve the FrameActor ID given a frame depth. - * - * @param number frame - * Frame depth. - * @return string|null - * The FrameActor ID for the given frame depth. - */ - getFrameActor: function(frame) { - let state = this.hud.owner.getDebuggerFrames(); - if (!state) { - return null; - } - - let grip; - if (frame == this.SELECTED_FRAME) { - grip = state.frames[state.selected]; - } else { - grip = state.frames[frame]; - } - - return grip ? grip.actor : null; - }, - - /** - * Opens a new variables view that allows the inspection of the given object. - * - * @param object options - * Options for the variables view: - * - objectActor: grip of the ObjectActor you want to show in the - * variables view. - * - rawObject: the raw object you want to show in the variables view. - * - label: label to display in the variables view for inspected - * object. - * - hideFilterInput: optional boolean, |true| if you want to hide the - * variables view filter input. - * - targetElement: optional nsIDOMElement to append the variables view - * to. An iframe element is used as a container for the view. If this - * option is not used, then the variables view opens in the sidebar. - * - autofocus: optional boolean, |true| if you want to give focus to - * the variables view window after open, |false| otherwise. - * @return object - * A promise object that is resolved when the variables view has - * opened. The new variables view instance is given to the callbacks. - */ - openVariablesView: function(options) { - let onContainerReady = (window) => { - let container = window.document.querySelector("#variables"); - let view = this._variablesView; - if (!view || options.targetElement) { - let viewOptions = { - container: container, - hideFilterInput: options.hideFilterInput, - }; - view = this._createVariablesView(viewOptions); - if (!options.targetElement) { - this._variablesView = view; - window.addEventListener("keypress", this._onKeypressInVariablesView); - } - } - options.view = view; - this._updateVariablesView(options); - - if (!options.targetElement && options.autofocus) { - window.focus(); - } - - this.emit("variablesview-open", view, options); - return view; - }; - - let openPromise; - if (options.targetElement) { - let deferred = promise.defer(); - openPromise = deferred.promise; - let document = options.targetElement.ownerDocument; - let iframe = document.createElementNS(XHTML_NS, "iframe"); - - iframe.addEventListener("load", function onIframeLoad() { - iframe.removeEventListener("load", onIframeLoad, true); - iframe.style.visibility = "visible"; - deferred.resolve(iframe.contentWindow); - }, true); - - iframe.flex = 1; - iframe.style.visibility = "hidden"; - iframe.setAttribute("src", VARIABLES_VIEW_URL); - options.targetElement.appendChild(iframe); - } else { - if (!this.sidebar) { - this._createSidebar(); - } - openPromise = this._addVariablesViewSidebarTab(); - } - - return openPromise.then(onContainerReady); - }, - - /** - * Create the Web Console sidebar. - * - * @see devtools/framework/sidebar.js - * @private - */ - _createSidebar: function() { - let tabbox = this.hud.document.querySelector("#webconsole-sidebar"); - this.sidebar = new ToolSidebar(tabbox, this, "webconsole"); - this.sidebar.show(); - this.emit("sidebar-opened"); - }, - - /** - * Add the variables view tab to the sidebar. - * - * @private - * @return object - * A promise object for the adding of the new tab. - */ - _addVariablesViewSidebarTab: function() { - let deferred = promise.defer(); - - let onTabReady = () => { - let window = this.sidebar.getWindowForTab("variablesview"); - deferred.resolve(window); - }; - - let tabPanel = this.sidebar.getTabPanel("variablesview"); - if (tabPanel) { - if (this.sidebar.getCurrentTabID() == "variablesview") { - onTabReady(); - } else { - this.sidebar.once("variablesview-selected", onTabReady); - this.sidebar.select("variablesview"); - } - } else { - this.sidebar.once("variablesview-ready", onTabReady); - this.sidebar.addTab("variablesview", VARIABLES_VIEW_URL, true); - } - - return deferred.promise; - }, - - /** - * The keypress event handler for the Variables View sidebar. Currently this - * is used for removing the sidebar when Escape is pressed. - * - * @private - * @param nsIDOMEvent event - * The keypress DOM event object. - */ - _onKeypressInVariablesView: function(event) { - let tag = event.target.nodeName; - if (event.keyCode != Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE || event.shiftKey || - event.altKey || event.ctrlKey || event.metaKey || - ["input", "textarea", "select", "textbox"].indexOf(tag) > -1) { - return; - } - - this._sidebarDestroy(); - this.focus(); - event.stopPropagation(); - }, - - /** - * Create a variables view instance. - * - * @private - * @param object options - * Options for the new Variables View instance: - * - container: the DOM element where the variables view is inserted. - * - hideFilterInput: boolean, if true the variables filter input is - * hidden. - * @return object - * The new Variables View instance. - */ - _createVariablesView: function(options) { - let view = new VariablesView(options.container); - view.toolbox = gDevTools.getToolbox(this.hud.owner.target); - view.searchPlaceholder = l10n.getStr("propertiesFilterPlaceholder"); - view.emptyText = l10n.getStr("emptyPropertiesList"); - view.searchEnabled = !options.hideFilterInput; - view.lazyEmpty = this._lazyVariablesView; - - VariablesViewController.attach(view, { - getEnvironmentClient: grip => { - return new EnvironmentClient(this.hud.proxy.client, grip); - }, - getObjectClient: grip => { - return new ObjectClient(this.hud.proxy.client, grip); - }, - getLongStringClient: grip => { - return this.webConsoleClient.longString(grip); - }, - releaseActor: actor => { - this.hud._releaseObject(actor); - }, - simpleValueEvalMacro: simpleValueEvalMacro, - overrideValueEvalMacro: overrideValueEvalMacro, - getterOrSetterEvalMacro: getterOrSetterEvalMacro, - }); - - // Relay events from the VariablesView. - view.on("fetched", (event, type, variableObject) => { - this.emit("variablesview-fetched", variableObject); - }); - - return view; - }, - - /** - * Update the variables view. - * - * @private - * @param object options - * Options for updating the variables view: - * - view: the view you want to update. - * - objectActor: the grip of the new ObjectActor you want to show in - * the view. - * - rawObject: the new raw object you want to show. - * - label: the new label for the inspected object. - */ - _updateVariablesView: function(options) { - let view = options.view; - view.empty(); - - // We need to avoid pruning the object inspection starting point. - // That one is pruned when the console message is removed. - view.controller.releaseActors(actor => { - return view._consoleLastObjectActor != actor; - }); - - if (options.objectActor && - (!this.hud.owner._browserConsole || - Services.prefs.getBoolPref("devtools.chrome.enabled"))) { - // Make sure eval works in the correct context. - view.eval = this._variablesViewEvaluate.bind(this, options); - view.switch = this._variablesViewSwitch.bind(this, options); - view.delete = this._variablesViewDelete.bind(this, options); - } else { - view.eval = null; - view.switch = null; - view.delete = null; - } - - let { variable, expanded } = view.controller.setSingleVariable(options); - variable.evaluationMacro = simpleValueEvalMacro; - - if (options.objectActor) { - view._consoleLastObjectActor = options.objectActor.actor; - } else if (options.rawObject) { - view._consoleLastObjectActor = null; - } else { - throw new Error( - "Variables View cannot open without giving it an object display."); - } - - expanded.then(() => { - this.emit("variablesview-updated", view, options); - }); - }, - - /** - * The evaluation function used by the variables view when editing a property - * value. - * - * @private - * @param object options - * The options used for |this._updateVariablesView()|. - * @param object variableObject - * The Variable object instance for the edited property. - * @param string value - * The value the edited property was changed to. - */ - _variablesViewEvaluate: function(options, variableObject, value) { - let updater = this._updateVariablesView.bind(this, options); - let onEval = this._silentEvalCallback.bind(this, updater); - let string = variableObject.evaluationMacro(variableObject, value); - - let evalOptions = { - frame: this.SELECTED_FRAME, - bindObjectActor: options.objectActor.actor, - }; - - this.requestEvaluation(string, evalOptions).then(onEval, onEval); - }, - - /** - * The property deletion function used by the variables view when a property - * is deleted. - * - * @private - * @param object options - * The options used for |this._updateVariablesView()|. - * @param object variableObject - * The Variable object instance for the deleted property. - */ - _variablesViewDelete: function(options, variableObject) { - let onEval = this._silentEvalCallback.bind(this, null); - - let evalOptions = { - frame: this.SELECTED_FRAME, - bindObjectActor: options.objectActor.actor, - }; - - this.requestEvaluation("delete _self" + - variableObject.symbolicName, evalOptions).then(onEval, onEval); - }, - - /** - * The property rename function used by the variables view when a property - * is renamed. - * - * @private - * @param object options - * The options used for |this._updateVariablesView()|. - * @param object variableObject - * The Variable object instance for the renamed property. - * @param string newName - * The new name for the property. - */ - _variablesViewSwitch: function(options, variableObject, newName) { - let updater = this._updateVariablesView.bind(this, options); - let onEval = this._silentEvalCallback.bind(this, updater); - - let evalOptions = { - frame: this.SELECTED_FRAME, - bindObjectActor: options.objectActor.actor, - }; - - let newSymbolicName = - variableObject.ownerView.symbolicName + '["' + newName + '"]'; - if (newSymbolicName == variableObject.symbolicName) { - return; - } - - let code = "_self" + newSymbolicName + " = _self" + - variableObject.symbolicName + ";" + "delete _self" + - variableObject.symbolicName; - - this.requestEvaluation(code, evalOptions).then(onEval, onEval); - }, - - /** - * A noop callback for JavaScript evaluation. This method releases any - * result ObjectActors that come from the server for evaluation requests. This - * is used for editing, renaming and deleting properties in the variables - * view. - * - * Exceptions are displayed in the output. - * - * @private - * @param function callback - * Function to invoke once the response is received. - * @param object response - * The response packet received from the server. - */ - _silentEvalCallback: function(callback, response) { - if (response.error) { - Cu.reportError("Web Console evaluation failed. " + response.error + ":" + - response.message); - - callback && callback(response); - return; - } - - if (response.exceptionMessage) { - let message = new Messages.Simple(response.exceptionMessage, { - category: "output", - severity: "error", - timestamp: response.timestamp, - }); - this.hud.output.addMessage(message); - message._objectActors = new Set(); - if (WebConsoleUtils.isActorGrip(response.exception)) { - message._objectActors.add(response.exception.actor); - } - } - - let helper = response.helperResult || { type: null }; - let helperGrip = null; - if (helper.type == "inspectObject") { - helperGrip = helper.object; - } - - let grips = [response.result, helperGrip]; - for (let grip of grips) { - if (WebConsoleUtils.isActorGrip(grip)) { - this.hud._releaseObject(grip.actor); - } - } - - callback && callback(response); - }, - - /** - * Clear the Web Console output. - * - * This method emits the "messages-cleared" notification. - * - * @param boolean clearStorage - * True if you want to clear the console messages storage associated to - * this Web Console. - */ - clearOutput: function(clearStorage) { - let hud = this.hud; - let outputNode = hud.outputNode; - let node; - while ((node = outputNode.firstChild)) { - hud.removeOutputMessage(node); - } - - hud.groupDepth = 0; - hud._outputQueue.forEach(hud._destroyItem, hud); - hud._outputQueue = []; - this.webConsoleClient.clearNetworkRequests(); - hud._repeatNodes = {}; - - if (clearStorage) { - this.webConsoleClient.clearMessagesCache(); - } - - this._sidebarDestroy(); - - this.emit("messages-cleared"); - }, - - /** - * Remove all of the private messages from the Web Console output. - * - * This method emits the "private-messages-cleared" notification. - */ - clearPrivateMessages: function() { - let nodes = this.hud.outputNode.querySelectorAll(".message[private]"); - for (let node of nodes) { - this.hud.removeOutputMessage(node); - } - this.emit("private-messages-cleared"); - }, - - /** - * Updates the size of the input field (command line) to fit its contents. - * - * @returns void - */ - resizeInput: function() { - let inputNode = this.inputNode; - - // Reset the height so that scrollHeight will reflect the natural height of - // the contents of the input field. - inputNode.style.height = "auto"; - - // Now resize the input field to fit its contents. - let scrollHeight = inputNode.inputField.scrollHeight; - if (scrollHeight > 0) { - inputNode.style.height = scrollHeight + "px"; - } - }, - - /** - * Sets the value of the input field (command line), and resizes the field to - * fit its contents. This method is preferred over setting "inputNode.value" - * directly, because it correctly resizes the field. - * - * @param string newValue - * The new value to set. - * @returns void - */ - setInputValue: function(newValue) { - this.inputNode.value = newValue; - this.lastInputValue = newValue; - this.completeNode.value = ""; - this.resizeInput(); - this._inputChanged = true; - this.emit("set-input-value"); - }, - - /** - * Gets the value from the input field - * @returns string - */ - getInputValue: function() { - return this.inputNode.value || ""; - }, - - /** - * The inputNode "input" and "keyup" event handler. - * @private - */ - _inputEventHandler: function() { - if (this.lastInputValue != this.getInputValue()) { - this.resizeInput(); - this.complete(this.COMPLETE_HINT_ONLY); - this.lastInputValue = this.getInputValue(); - this._inputChanged = true; - } - }, - - /** - * The window "blur" event handler. - * @private - */ - _blurEventHandler: function() { - if (this.autocompletePopup) { - this.clearCompletion(); - } - }, - - /** - * The inputNode "keypress" event handler. - * - * @private - * @param nsIDOMEvent event - */ - _keyPress: function(event) { - let inputNode = this.inputNode; - let inputValue = this.getInputValue(); - let inputUpdated = false; - - if (event.ctrlKey) { - switch (event.charCode) { - case 101: - // control-e - if (Services.appinfo.OS == "WINNT") { - break; - } - let lineEndPos = inputValue.length; - if (this.hasMultilineInput()) { - // find index of closest newline >= cursor - for (let i = inputNode.selectionEnd; i < lineEndPos; i++) { - if (inputValue.charAt(i) == "\r" || - inputValue.charAt(i) == "\n") { - lineEndPos = i; - break; - } - } - } - inputNode.setSelectionRange(lineEndPos, lineEndPos); - event.preventDefault(); - this.clearCompletion(); - break; - - case 110: - // Control-N differs from down arrow: it ignores autocomplete state. - // Note that we preserve the default 'down' navigation within - // multiline text. - if (Services.appinfo.OS == "Darwin" && - this.canCaretGoNext() && - this.historyPeruse(HISTORY_FORWARD)) { - event.preventDefault(); - // Ctrl-N is also used to focus the Network category button on - // MacOSX. The preventDefault() call doesn't prevent the focus - // from moving away from the input. - this.focus(); - } - this.clearCompletion(); - break; - - case 112: - // Control-P differs from up arrow: it ignores autocomplete state. - // Note that we preserve the default 'up' navigation within - // multiline text. - if (Services.appinfo.OS == "Darwin" && - this.canCaretGoPrevious() && - this.historyPeruse(HISTORY_BACK)) { - event.preventDefault(); - // Ctrl-P may also be used to focus some category button on MacOSX. - // The preventDefault() call doesn't prevent the focus from moving - // away from the input. - this.focus(); - } - this.clearCompletion(); - break; - default: - break; - } - return; - } else if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN) { - let autoMultiline = Services.prefs.getBoolPref(PREF_AUTO_MULTILINE); - if (event.shiftKey || - (!Debugger.isCompilableUnit(inputNode.value) && autoMultiline)) { - // shift return or incomplete statement - return; - } - } - - switch (event.keyCode) { - case Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE: - if (this.autocompletePopup.isOpen) { - this.clearCompletion(); - event.preventDefault(); - event.stopPropagation(); - } else if (this.sidebar) { - this._sidebarDestroy(); - event.preventDefault(); - event.stopPropagation(); - } - break; - - case Ci.nsIDOMKeyEvent.DOM_VK_RETURN: - if (this._autocompletePopupNavigated && - this.autocompletePopup.isOpen && - this.autocompletePopup.selectedIndex > -1) { - this.acceptProposedCompletion(); - } else { - this.execute(); - this._inputChanged = false; - } - event.preventDefault(); - break; - - case Ci.nsIDOMKeyEvent.DOM_VK_UP: - if (this.autocompletePopup.isOpen) { - inputUpdated = this.complete(this.COMPLETE_BACKWARD); - if (inputUpdated) { - this._autocompletePopupNavigated = true; - } - } else if (this.canCaretGoPrevious()) { - inputUpdated = this.historyPeruse(HISTORY_BACK); - } - if (inputUpdated) { - event.preventDefault(); - } - break; - - case Ci.nsIDOMKeyEvent.DOM_VK_DOWN: - if (this.autocompletePopup.isOpen) { - inputUpdated = this.complete(this.COMPLETE_FORWARD); - if (inputUpdated) { - this._autocompletePopupNavigated = true; - } - } else if (this.canCaretGoNext()) { - inputUpdated = this.historyPeruse(HISTORY_FORWARD); - } - if (inputUpdated) { - event.preventDefault(); - } - break; - - case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP: - if (this.autocompletePopup.isOpen) { - inputUpdated = this.complete(this.COMPLETE_PAGEUP); - if (inputUpdated) { - this._autocompletePopupNavigated = true; - } - } else { - this.hud.outputWrapper.scrollTop = - Math.max(0, - this.hud.outputWrapper.scrollTop - - this.hud.outputWrapper.clientHeight - ); - } - event.preventDefault(); - break; - - case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN: - if (this.autocompletePopup.isOpen) { - inputUpdated = this.complete(this.COMPLETE_PAGEDOWN); - if (inputUpdated) { - this._autocompletePopupNavigated = true; - } - } else { - this.hud.outputWrapper.scrollTop = - Math.min(this.hud.outputWrapper.scrollHeight, - this.hud.outputWrapper.scrollTop + - this.hud.outputWrapper.clientHeight - ); - } - event.preventDefault(); - break; - - case Ci.nsIDOMKeyEvent.DOM_VK_HOME: - if (this.autocompletePopup.isOpen) { - this.autocompletePopup.selectedIndex = 0; - event.preventDefault(); - } else if (inputValue.length <= 0) { - this.hud.outputWrapper.scrollTop = 0; - event.preventDefault(); - } - break; - - case Ci.nsIDOMKeyEvent.DOM_VK_END: - if (this.autocompletePopup.isOpen) { - this.autocompletePopup.selectedIndex = - this.autocompletePopup.itemCount - 1; - event.preventDefault(); - } else if (inputValue.length <= 0) { - this.hud.outputWrapper.scrollTop = - this.hud.outputWrapper.scrollHeight; - event.preventDefault(); - } - break; - - case Ci.nsIDOMKeyEvent.DOM_VK_LEFT: - if (this.autocompletePopup.isOpen || this.lastCompletion.value) { - this.clearCompletion(); - } - break; - - case Ci.nsIDOMKeyEvent.DOM_VK_RIGHT: { - let cursorAtTheEnd = this.inputNode.selectionStart == - this.inputNode.selectionEnd && - this.inputNode.selectionStart == - inputValue.length; - let haveSuggestion = this.autocompletePopup.isOpen || - this.lastCompletion.value; - let useCompletion = cursorAtTheEnd || this._autocompletePopupNavigated; - if (haveSuggestion && useCompletion && - this.complete(this.COMPLETE_HINT_ONLY) && - this.lastCompletion.value && - this.acceptProposedCompletion()) { - event.preventDefault(); - } - if (this.autocompletePopup.isOpen) { - this.clearCompletion(); - } - break; - } - case Ci.nsIDOMKeyEvent.DOM_VK_TAB: - // Generate a completion and accept the first proposed value. - if (this.complete(this.COMPLETE_HINT_ONLY) && - this.lastCompletion && - this.acceptProposedCompletion()) { - event.preventDefault(); - } else if (this._inputChanged) { - this.updateCompleteNode(l10n.getStr("Autocomplete.blank")); - event.preventDefault(); - } - break; - default: - break; - } - }, - - /** - * The inputNode "focus" event handler. - * @private - */ - _focusEventHandler: function() { - this._inputChanged = false; - }, - - /** - * Go up/down the history stack of input values. - * - * @param number direction - * History navigation direction: HISTORY_BACK or HISTORY_FORWARD. - * - * @returns boolean - * True if the input value changed, false otherwise. - */ - historyPeruse: function(direction) { - if (!this.history.length) { - return false; - } - - // Up Arrow key - if (direction == HISTORY_BACK) { - if (this.historyPlaceHolder <= 0) { - return false; - } - let inputVal = this.history[--this.historyPlaceHolder]; - - // Save the current input value as the latest entry in history, only if - // the user is already at the last entry. - // Note: this code does not store changes to items that are already in - // history. - if (this.historyPlaceHolder + 1 == this.historyIndex) { - this.history[this.historyIndex] = this.getInputValue() || ""; - } - - this.setInputValue(inputVal); - } else if (direction == HISTORY_FORWARD) { - // Down Arrow key - if (this.historyPlaceHolder >= (this.history.length - 1)) { - return false; - } - - let inputVal = this.history[++this.historyPlaceHolder]; - this.setInputValue(inputVal); - } else { - throw new Error("Invalid argument 0"); - } - - return true; - }, - - /** - * Test for multiline input. - * - * @return boolean - * True if CR or LF found in node value; else false. - */ - hasMultilineInput: function() { - return /[\r\n]/.test(this.getInputValue()); - }, - - /** - * Check if the caret is at a location that allows selecting the previous item - * in history when the user presses the Up arrow key. - * - * @return boolean - * True if the caret is at a location that allows selecting the - * previous item in history when the user presses the Up arrow key, - * otherwise false. - */ - canCaretGoPrevious: function() { - let node = this.inputNode; - if (node.selectionStart != node.selectionEnd) { - return false; - } - - let multiline = /[\r\n]/.test(node.value); - return node.selectionStart == 0 ? true : - node.selectionStart == node.value.length && !multiline; - }, - - /** - * Check if the caret is at a location that allows selecting the next item in - * history when the user presses the Down arrow key. - * - * @return boolean - * True if the caret is at a location that allows selecting the next - * item in history when the user presses the Down arrow key, otherwise - * false. - */ - canCaretGoNext: function() { - let node = this.inputNode; - if (node.selectionStart != node.selectionEnd) { - return false; - } - - let multiline = /[\r\n]/.test(node.value); - return node.selectionStart == node.value.length ? true : - node.selectionStart == 0 && !multiline; - }, - - /** - * Completes the current typed text in the inputNode. Completion is performed - * only if the selection/cursor is at the end of the string. If no completion - * is found, the current inputNode value and cursor/selection stay. - * - * @param int type possible values are - * - this.COMPLETE_FORWARD: If there is more than one possible completion - * and the input value stayed the same compared to the last time this - * function was called, then the next completion of all possible - * completions is used. If the value changed, then the first possible - * completion is used and the selection is set from the current - * cursor position to the end of the completed text. - * If there is only one possible completion, then this completion - * value is used and the cursor is put at the end of the completion. - * - this.COMPLETE_BACKWARD: Same as this.COMPLETE_FORWARD but if the - * value stayed the same as the last time the function was called, - * then the previous completion of all possible completions is used. - * - this.COMPLETE_PAGEUP: Scroll up one page if available or select the - * first item. - * - this.COMPLETE_PAGEDOWN: Scroll down one page if available or select - * the last item. - * - this.COMPLETE_HINT_ONLY: If there is more than one possible - * completion and the input value stayed the same compared to the - * last time this function was called, then the same completion is - * used again. If there is only one possible completion, then - * the this.getInputValue() is set to this value and the selection - * is set from the current cursor position to the end of the - * completed text. - * @param function callback - * Optional function invoked when the autocomplete properties are - * updated. - * @returns boolean true if there existed a completion for the current input, - * or false otherwise. - */ - complete: function(type, callback) { - let inputNode = this.inputNode; - let inputValue = this.getInputValue(); - let frameActor = this.getFrameActor(this.SELECTED_FRAME); - - // If the inputNode has no value, then don't try to complete on it. - if (!inputValue) { - this.clearCompletion(); - callback && callback(this); - this.emit("autocomplete-updated"); - return false; - } - - // Only complete if the selection is empty. - if (inputNode.selectionStart != inputNode.selectionEnd) { - this.clearCompletion(); - callback && callback(this); - this.emit("autocomplete-updated"); - return false; - } - - // Update the completion results. - if (this.lastCompletion.value != inputValue || - frameActor != this._lastFrameActorId) { - this._updateCompletionResult(type, callback); - return false; - } - - let popup = this.autocompletePopup; - let accepted = false; - - if (type != this.COMPLETE_HINT_ONLY && popup.itemCount == 1) { - this.acceptProposedCompletion(); - accepted = true; - } else if (type == this.COMPLETE_BACKWARD) { - popup.selectPreviousItem(); - } else if (type == this.COMPLETE_FORWARD) { - popup.selectNextItem(); - } else if (type == this.COMPLETE_PAGEUP) { - popup.selectPreviousPageItem(); - } else if (type == this.COMPLETE_PAGEDOWN) { - popup.selectNextPageItem(); - } - - callback && callback(this); - this.emit("autocomplete-updated"); - return accepted || popup.itemCount > 0; - }, - - /** - * Update the completion result. This operation is performed asynchronously by - * fetching updated results from the content process. - * - * @private - * @param int type - * Completion type. See this.complete() for details. - * @param function [callback] - * Optional, function to invoke when completion results are received. - */ - _updateCompletionResult: function(type, callback) { - let frameActor = this.getFrameActor(this.SELECTED_FRAME); - if (this.lastCompletion.value == this.getInputValue() && - frameActor == this._lastFrameActorId) { - return; - } - - let requestId = gSequenceId(); - let cursor = this.inputNode.selectionStart; - let input = this.getInputValue().substring(0, cursor); - let cache = this._autocompleteCache; - - // If the current input starts with the previous input, then we already - // have a list of suggestions and we just need to filter the cached - // suggestions. When the current input ends with a non-alphanumeric - // character we ask the server again for suggestions. - - // Check if last character is non-alphanumeric - if (!/[a-zA-Z0-9]$/.test(input) || frameActor != this._lastFrameActorId) { - this._autocompleteQuery = null; - this._autocompleteCache = null; - } - - if (this._autocompleteQuery && input.startsWith(this._autocompleteQuery)) { - let filterBy = input; - // Find the last non-alphanumeric other than _ or $ if it exists. - let lastNonAlpha = input.match(/[^a-zA-Z0-9_$][a-zA-Z0-9_$]*$/); - // If input contains non-alphanumerics, use the part after the last one - // to filter the cache - if (lastNonAlpha) { - filterBy = input.substring(input.lastIndexOf(lastNonAlpha) + 1); - } - - let newList = cache.sort().filter(function(l) { - return l.startsWith(filterBy); - }); - - this.lastCompletion = { - requestId: null, - completionType: type, - value: null, - }; - - let response = { matches: newList, matchProp: filterBy }; - this._receiveAutocompleteProperties(null, callback, response); - return; - } - - this._lastFrameActorId = frameActor; - - this.lastCompletion = { - requestId: requestId, - completionType: type, - value: null, - }; - - let autocompleteCallback = - this._receiveAutocompleteProperties.bind(this, requestId, callback); - - this.webConsoleClient.autocomplete( - input, cursor, autocompleteCallback, frameActor); - }, - - /** - * Handler for the autocompletion results. This method takes - * the completion result received from the server and updates the UI - * accordingly. - * - * @param number requestId - * Request ID. - * @param function [callback=null] - * Optional, function to invoke when the completion result is received. - * @param object message - * The JSON message which holds the completion results received from - * the content process. - */ - _receiveAutocompleteProperties: function(requestId, callback, message) { - let inputNode = this.inputNode; - let inputValue = this.getInputValue(); - if (this.lastCompletion.value == inputValue || - requestId != this.lastCompletion.requestId) { - return; - } - // Cache whatever came from the server if the last char is - // alphanumeric or '.' - let cursor = inputNode.selectionStart; - let inputUntilCursor = inputValue.substring(0, cursor); - - if (requestId != null && /[a-zA-Z0-9.]$/.test(inputUntilCursor)) { - this._autocompleteCache = message.matches; - this._autocompleteQuery = inputUntilCursor; - } - - let matches = message.matches; - let lastPart = message.matchProp; - if (!matches.length) { - this.clearCompletion(); - callback && callback(this); - this.emit("autocomplete-updated"); - return; - } - - let items = matches.reverse().map(function(match) { - return { preLabel: lastPart, label: match }; - }); - - let popup = this.autocompletePopup; - popup.setItems(items); - - let completionType = this.lastCompletion.completionType; - this.lastCompletion = { - value: inputValue, - matchProp: lastPart, - }; - - if (items.length > 1 && !popup.isOpen) { - let str = this.getInputValue().substr(0, this.inputNode.selectionStart); - let offset = str.length - (str.lastIndexOf("\n") + 1) - lastPart.length; - let x = offset * this.hud._inputCharWidth; - popup.openPopup(inputNode, x + this.hud._chevronWidth); - this._autocompletePopupNavigated = false; - } else if (items.length < 2 && popup.isOpen) { - popup.hidePopup(); - this._autocompletePopupNavigated = false; - } - - if (items.length == 1) { - popup.selectedIndex = 0; - } - - this.onAutocompleteSelect(); - - if (completionType != this.COMPLETE_HINT_ONLY && popup.itemCount == 1) { - this.acceptProposedCompletion(); - } else if (completionType == this.COMPLETE_BACKWARD) { - popup.selectPreviousItem(); - } else if (completionType == this.COMPLETE_FORWARD) { - popup.selectNextItem(); - } - - callback && callback(this); - this.emit("autocomplete-updated"); - }, - - onAutocompleteSelect: function() { - // Render the suggestion only if the cursor is at the end of the input. - if (this.inputNode.selectionStart != this.getInputValue().length) { - return; - } - - let currentItem = this.autocompletePopup.selectedItem; - if (currentItem && this.lastCompletion.value) { - let suffix = - currentItem.label.substring(this.lastCompletion.matchProp.length); - this.updateCompleteNode(suffix); - } else { - this.updateCompleteNode(""); - } - }, - - /** - * Clear the current completion information and close the autocomplete popup, - * if needed. - */ - clearCompletion: function() { - this.autocompletePopup.clearItems(); - this.lastCompletion = { value: null }; - this.updateCompleteNode(""); - if (this.autocompletePopup.isOpen) { - this.autocompletePopup.hidePopup(); - this._autocompletePopupNavigated = false; - } - }, - - /** - * Accept the proposed input completion. - * - * @return boolean - * True if there was a selected completion item and the input value - * was updated, false otherwise. - */ - acceptProposedCompletion: function() { - let updated = false; - - let currentItem = this.autocompletePopup.selectedItem; - if (currentItem && this.lastCompletion.value) { - let suffix = - currentItem.label.substring(this.lastCompletion.matchProp.length); - let cursor = this.inputNode.selectionStart; - let value = this.getInputValue(); - this.setInputValue(value.substr(0, cursor) + - suffix + value.substr(cursor)); - let newCursor = cursor + suffix.length; - this.inputNode.selectionStart = this.inputNode.selectionEnd = newCursor; - updated = true; - } - - this.clearCompletion(); - - return updated; - }, - - /** - * Update the node that displays the currently selected autocomplete proposal. - * - * @param string suffix - * The proposed suffix for the inputNode value. - */ - updateCompleteNode: function(suffix) { - // completion prefix = input, with non-control chars replaced by spaces - let prefix = suffix ? this.getInputValue().replace(/[\S]/g, " ") : ""; - this.completeNode.value = prefix + suffix; - }, - - /** - * Destroy the sidebar. - * @private - */ - _sidebarDestroy: function() { - if (this._variablesView) { - this._variablesView.controller.releaseActors(); - this._variablesView = null; - } - - if (this.sidebar) { - this.sidebar.hide(); - this.sidebar.destroy(); - this.sidebar = null; - } - - this.emit("sidebar-closed"); - }, - - /** - * Destroy the JSTerm object. Call this method to avoid memory leaks. - */ - destroy: function() { - this._sidebarDestroy(); - - this.clearCompletion(); - this.clearOutput(); - - this.autocompletePopup.destroy(); - this.autocompletePopup = null; - - let popup = this.hud.owner.chromeWindow.document - .getElementById("webConsole_autocompletePopup"); - if (popup) { - popup.parentNode.removeChild(popup); - } - - if (this._onPaste) { - this.inputNode.removeEventListener("paste", this._onPaste, false); - this.inputNode.removeEventListener("drop", this._onPaste, false); - this._onPaste = null; - } - - this.inputNode.removeEventListener("keypress", this._keyPress, false); - this.inputNode.removeEventListener("input", this._inputEventHandler, false); - this.inputNode.removeEventListener("keyup", this._inputEventHandler, false); - this.inputNode.removeEventListener("focus", this._focusEventHandler, false); - this.hud.window.removeEventListener("blur", this._blurEventHandler, false); - - this.hud = null; - }, -}; - -/** * Utils: a collection of globally used functions. */ var Utils = { /** * Scrolls a node so that it's visible in its containing element. * * @param nsIDOMNode node * The node to make visible. @@ -5146,21 +3472,16 @@ WebConsoleConnectionProxy.prototype = { this.connected = false; this.webConsoleFrame = null; this._disconnecter.resolve(null); return this._disconnecter.promise; }, }; -function gSequenceId() { - return gSequenceId.n++; -} -gSequenceId.n = 0; - // //////////////////////////////////////////////////////////////////////////// // Context Menu // //////////////////////////////////////////////////////////////////////////// /* * ConsoleContextMenu this used to handle the visibility of context menu items. * * @constructor
--- a/services/sync/modules/SyncedTabs.jsm +++ b/services/sync/modules/SyncedTabs.jsm @@ -57,18 +57,21 @@ let SyncedTabsInternal = { let isMobile = Weave.Service.clientsEngine.isMobile(id); if (isMobile) { return "chrome://browser/skin/sync-mobileIcon.png"; } return "chrome://browser/skin/sync-desktopIcon.png"; }, /* Make a "tab" record. Returns a promise */ - _makeTab: Task.async(function* (client, tab, url) { - let icon = tab.icon; + _makeTab: Task.async(function* (client, tab, url, showRemoteIcons) { + let icon; + if (showRemoteIcons) { + icon = tab.icon; + } if (!icon) { try { icon = (yield PlacesUtils.promiseFaviconLinkUrl(url)).spec; } catch (ex) { /* no favicon avaiable */ } } if (!icon) { icon = PlacesUtils.favicons.defaultFavicon.spec; } @@ -103,16 +106,19 @@ let SyncedTabsInternal = { let result = []; // If Sync isn't ready, don't try and get anything. if (!weaveXPCService.ready) { log.debug("Sync isn't yet ready, so returning an empty tab list"); return result; } + // A boolean that controls whether we should show the icon from the remote tab. + const showRemoteIcons = Preferences.get("services.sync.syncedTabs.showRemoteIcons", true); + let engine = Weave.Service.engineManager.get("tabs"); let seenURLs = new Set(); let parentIndex = 0; let ntabs = 0; for (let [guid, client] in Iterator(engine.getAllClients())) { let clientRepr = yield this._makeClient(client); @@ -129,17 +135,17 @@ let SyncedTabsInternal = { // In a followup we should consider simply dropping this |seenUrls| // check and return duplicate records - it seems the user will be more // confused by tabs not showing up on a device (because it was detected // as a dupe so it only appears on a different device) than being // confused by seeing the same tab on different clients. if (!url || seenURLs.has(url)) { continue; } - let tabRepr = yield this._makeTab(client, tab, url); + let tabRepr = yield this._makeTab(client, tab, url, showRemoteIcons); if (filter && !this._tabMatchesFilter(tabRepr, filter)) { continue; } seenURLs.add(url); clientRepr.tabs.push(tabRepr); } // We return all clients, even those without tabs - the consumer should // filter it if they care.
--- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -179,17 +179,19 @@ TabStore.prototype = { // Truncate if necessary. if (urls.length > TAB_ENTRIES_LIMIT) { urls.length = TAB_ENTRIES_LIMIT; } allTabs.push({ title: current.title || "", urlHistory: urls, - icon: tabState.attributes && tabState.attributes.image || "", + icon: tabState.image || + (tabState.attributes && tabState.attributes.image) || + "", lastUsed: Math.floor((tabState.lastAccessed || 0) / 1000), }); } } return allTabs; },
--- a/services/sync/tests/unit/test_syncedtabs.js +++ b/services/sync/tests/unit/test_syncedtabs.js @@ -2,16 +2,19 @@ * vim:set ts=2 sw=2 sts=2 et: */ "use strict"; Cu.import("resource://services-sync/main.js"); Cu.import("resource://services-sync/SyncedTabs.jsm"); Cu.import("resource://gre/modules/Log.jsm"); +const faviconService = Cc["@mozilla.org/browser/favicon-service;1"] + .getService(Ci.nsIFaviconService); + Log.repository.getLogger("Sync.RemoteTabs").addAppender(new Log.DumpAppender()); // A mock "Tabs" engine which the SyncedTabs module will use instead of the real // engine. We pass a constructor that Sync creates. function MockTabsEngine() { this.clients = {}; // We'll set this dynamically } @@ -74,33 +77,58 @@ add_task(function* test_noClients() { add_task(function* test_clientWithTabs() { yield configureClients({ guid_desktop: { clientName: "My Desktop", tabs: [ { urlHistory: ["http://foo.com/"], + icon: "http://foo.com/favicon", }], }, guid_mobile: { clientName: "My Phone", tabs: [], } }); let clients = yield SyncedTabs.getTabClients(); equal(clients.length, 2); clients.sort((a, b) => { return a.name.localeCompare(b.name);}); equal(clients[0].tabs.length, 1); equal(clients[0].tabs[0].url, "http://foo.com/"); + equal(clients[0].tabs[0].icon, "http://foo.com/favicon"); // second client has no tabs. equal(clients[1].tabs.length, 0); }); +add_task(function* test_clientWithTabsIconsDisabled() { + Services.prefs.setBoolPref("services.sync.syncedTabs.showRemoteIcons", false); + yield configureClients({ + guid_desktop: { + clientName: "My Desktop", + tabs: [ + { + urlHistory: ["http://foo.com/"], + icon: "http://foo.com/favicon", + }], + }, + }); + + let clients = yield SyncedTabs.getTabClients(); + equal(clients.length, 1); + clients.sort((a, b) => { return a.name.localeCompare(b.name);}); + equal(clients[0].tabs.length, 1); + equal(clients[0].tabs[0].url, "http://foo.com/"); + // expect the default favicon due to the pref being false. + equal(clients[0].tabs[0].icon, faviconService.defaultFavicon.spec); + Services.prefs.clearUserPref("services.sync.syncedTabs.showRemoteIcons"); +}); + add_task(function* test_filter() { // Nothing matches. yield configureClients({ guid_desktop: { clientName: "My Desktop", tabs: [ { urlHistory: ["http://foo.com/"],
--- a/toolkit/components/places/PlacesRemoteTabsAutocompleteProvider.jsm +++ b/toolkit/components/places/PlacesRemoteTabsAutocompleteProvider.jsm @@ -58,41 +58,59 @@ let _items = null; // Ensure the cache is good. function ensureItems() { if (!_items) { _items = buildItems(); } return _items; } -// An observer to invalidate _items. +// A preference used to disable the showing of icons in remote tab records. +const PREF_SHOW_REMOTE_ICONS = "services.sync.syncedTabs.showRemoteIcons"; +let showRemoteIcons; + +// An observer to invalidate _items and watch for changed prefs. function observe(subject, topic, data) { switch (topic) { case "weave:engine:sync:finish": if (data == "tabs") { // The tabs engine just finished syncing, so may have a different list // of tabs then we previously cached. _items = null; } break; case "weave:service:start-over": // Sync is being reset due to the user disconnecting - we must invalidate // the cache so we don't supply tabs from a different user. _items = null; break; + case "nsPref:changed": + if (data == PREF_SHOW_REMOTE_ICONS) { + try { + showRemoteIcons = Services.prefs.getBoolPref(PREF_SHOW_REMOTE_ICONS); + } catch(_) { + showRemoteIcons = true; // no such pref - default is to show the icons. + } + } + break; + default: break; } } Services.obs.addObserver(observe, "weave:engine:sync:finish", false); Services.obs.addObserver(observe, "weave:service:start-over", false); +// Observe the pref for showing remote icons and prime our bool that reflects its value. +Services.prefs.addObserver(PREF_SHOW_REMOTE_ICONS, observe, false); +observe(null, "nsPref:changed", PREF_SHOW_REMOTE_ICONS); + // This public object is a static singleton. this.PlacesRemoteTabsAutocompleteProvider = { // a promise that resolves with an array of matching remote tabs. getMatches(searchString) { // If Sync isn't configured we bail early. if (!Services.prefs.prefHasUserValue("services.sync.username")) { return Promise.resolve([]); } @@ -100,20 +118,20 @@ this.PlacesRemoteTabsAutocompleteProvide let re = new RegExp(escapeRegExp(searchString), "i"); let matches = []; let { tabs, clients } = ensureItems(); for (let [url, { clientId, tab }] of tabs) { let title = tab.title; if (url.match(re) || (title && title.match(re))) { // lookup the client record. let client = clients.get(clientId); + let icon = showRemoteIcons ? tab.icon : null; // create the record we return for auto-complete. let record = { - url, title, - icon: tab.icon, + url, title, icon, deviceClass: Weave.Service.clientsEngine.isMobile(clientId) ? "mobile" : "desktop", deviceName: client.clientName, }; matches.push(record); } } return Promise.resolve(matches); },
--- a/toolkit/components/places/tests/unifiedcomplete/test_remotetabmatches.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_remotetabmatches.js @@ -49,16 +49,17 @@ function configureEngine(clients) { } // Make a match object suitable for passing to check_autocomplete. function makeRemoteTabMatch(url, deviceName, extra = {}) { return { uri: makeActionURI("remotetab", {url, deviceName}), title: extra.title || url, style: [ "action" ], + icon: extra.icon, } } // The tests. add_task(function* test_nomatch() { // Nothing matches. configureEngine({ guid_desktop: { @@ -110,22 +111,49 @@ add_task(function* test_maximal() { }); yield check_autocomplete({ search: "ex", searchParam: "enable-actions", matches: [ makeSearchMatch("ex", { heuristic: true }), makeRemoteTabMatch("http://example.com/", "My Phone", { title: "An Example", - icon: "moz-anno:favicon:http://favicon" + icon: "moz-anno:favicon:http://favicon/" }), ], }); }); +add_task(function* test_noShowIcons() { + Services.prefs.setBoolPref("services.sync.syncedTabs.showRemoteIcons", false); + configureEngine({ + guid_mobile: { + clientName: "My Phone", + tabs: [{ + urlHistory: ["http://example.com/"], + title: "An Example", + icon: "http://favicon", + }], + } + }); + + yield check_autocomplete({ + search: "ex", + searchParam: "enable-actions", + matches: [ makeSearchMatch("ex", { heuristic: true }), + makeRemoteTabMatch("http://example.com/", "My Phone", + { title: "An Example", + // expecting the default favicon due to that pref. + icon: PlacesUtils.favicons.defaultFavicon.spec, + }), + ], + }); + Services.prefs.clearUserPref("services.sync.syncedTabs.showRemoteIcons"); +}); + add_task(function* test_matches_title() { // URL doesn't match search expression, should still match the title. configureEngine({ guid_mobile: { clientName: "My Phone", tabs: [{ urlHistory: ["http://foo.com/"], title: "An Example",
--- a/toolkit/modules/tests/xpcshell/test_web_channel_broker.js +++ b/toolkit/modules/tests/xpcshell/test_web_channel_broker.js @@ -56,16 +56,17 @@ add_task(function test_web_channel_broke var channel = new Object({ id: VALID_WEB_CHANNEL_ID, _originCheckCallback: requestPrincipal => { return VALID_WEB_CHANNEL_ORIGIN.prePath === requestPrincipal.origin; }, deliver: function(data, sender) { do_check_eq(data.id, VALID_WEB_CHANNEL_ID); do_check_eq(data.message.command, "hello"); + do_check_neq(sender, undefined); WebChannelBroker.unregisterChannel(channel); resolve(); } }); WebChannelBroker.registerChannel(channel); var mockEvent = {