author | Carsten "Tomcat" Book <cbook@mozilla.com> |
Thu, 13 Oct 2016 11:58:40 +0200 | |
changeset 317858 | 6359c12a6399b356da32d77cb2416a387c475844 |
parent 317857 | d1ed33f3fdd269bbe543d822b5a0cbd390ba0c68 (current diff) |
parent 317781 | f03e2740d604d339ed553dad62a3fc54c317f8fa (diff) |
child 317859 | 3e30051824704f61374030a645c5be6219d2e201 |
push id | 33170 |
push user | cbook@mozilla.com |
push date | Fri, 14 Oct 2016 10:37:07 +0000 |
treeherder | autoland@0d101ebfd95c [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
milestone | 52.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
netwerk/protocol/http/HttpBaseChannel.cpp | file | annotate | diff | comparison | revisions | |
security/manager/pki/resources/content/editcacert.js | file | annotate | diff | comparison | revisions | |
security/manager/ssl/tests/mochitest/browser/browser_editCACertTrust.js | file | annotate | diff | comparison | revisions |
--- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1415,19 +1415,21 @@ pref("privacy.trackingprotection.ui.enab #endif pref("privacy.trackingprotection.introCount", 0); pref("privacy.trackingprotection.introURL", "https://www.mozilla.org/%LOCALE%/firefox/%VERSION%/tracking-protection/start/"); // Enable Contextual Identity Containers #ifdef NIGHTLY_BUILD pref("privacy.userContext.enabled", true); pref("privacy.userContext.ui.enabled", true); +pref("privacy.usercontext.about_newtab_segregation.enabled", true); #else pref("privacy.userContext.enabled", false); pref("privacy.userContext.ui.enabled", false); +pref("privacy.usercontext.about_newtab_segregation.enabled", false); #endif #ifndef RELEASE_OR_BETA // At the moment, autostart.2 is used, while autostart.1 is unused. // We leave it here set to false to reset users' defaults and allow // us to change everybody to true in the future, when desired. pref("browser.tabs.remote.autostart.1", false); pref("browser.tabs.remote.autostart.2", true);
--- a/browser/components/contextualidentity/test/browser/browser_aboutURLs.js +++ b/browser/components/contextualidentity/test/browser/browser_aboutURLs.js @@ -33,16 +33,17 @@ add_task(function* () { } } catch (e) { // getService might have thrown if the component doesn't actually // implement nsIAboutModule } } for (let url of aboutURLs) { + info("Loading about:" + url); let tab = gBrowser.addTab("about:"+url, {userContextId: 1}); yield BrowserTestUtils.browserLoaded(tab.linkedBrowser); - ok(true); + ok(true, "Done loading about:" + url); yield BrowserTestUtils.removeTab(tab); } });
--- a/devtools/client/inspector/rules/test/browser.ini +++ b/devtools/client/inspector/rules/test/browser.ini @@ -123,16 +123,18 @@ skip-if = os == "mac" # Bug 1245996 : cl [browser_rules_edit-selector_02.js] [browser_rules_edit-selector_03.js] [browser_rules_edit-selector_04.js] [browser_rules_edit-selector_05.js] [browser_rules_edit-selector_06.js] [browser_rules_edit-selector_07.js] [browser_rules_edit-selector_08.js] [browser_rules_edit-selector_09.js] +[browser_rules_edit-selector_10.js] +[browser_rules_edit-selector_11.js] [browser_rules_edit-value-after-name_01.js] [browser_rules_edit-value-after-name_02.js] [browser_rules_edit-value-after-name_03.js] [browser_rules_edit-value-after-name_04.js] [browser_rules_editable-field-focus_01.js] [browser_rules_editable-field-focus_02.js] [browser_rules_eyedropper.js] [browser_rules_filtereditor-appears-on-swatch-click.js]
new file mode 100644 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_10.js @@ -0,0 +1,64 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Regression test for bug 1293616: make sure that editing a selector +// keeps the rule in the proper position. + +const TEST_URI = ` + <style type="text/css"> + #testid span, #testid p { + background: aqua; + } + span { + background: fuchsia; + } + </style> + <div id="testid"> + <span class="pickme"> + Styled Node + </span> + </div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode(".pickme", inspector); + yield testEditSelector(view); +}); + +function* testEditSelector(view) { + let ruleEditor = getRuleViewRuleEditor(view, 1); + let editor = yield focusEditableField(view, ruleEditor.selectorText); + + editor.input.value = "#testid span"; + let onRuleViewChanged = once(view, "ruleview-changed"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + // Escape the new property editor after editing the selector + let onBlur = once(view.styleDocument.activeElement, "blur"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow); + yield onBlur; + + // Get the new rule editor that replaced the original + ruleEditor = getRuleViewRuleEditor(view, 1); + + info("Check that the correct rules are visible"); + is(view._elementStyle.rules.length, 3, "Should have 3 rules."); + is(ruleEditor.element.getAttribute("unmatched"), "false", "Rule editor is matched."); + + let props = ruleEditor.rule.textProps; + is(props.length, 1, "Rule has correct number of properties"); + is(props[0].name, "background", "Found background property"); + ok(!props[0].overridden, "Background property is not overridden"); + + ruleEditor = getRuleViewRuleEditor(view, 2); + props = ruleEditor.rule.textProps; + is(props.length, 1, "Rule has correct number of properties"); + is(props[0].name, "background", "Found background property"); + ok(props[0].overridden, "Background property is overridden"); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_11.js @@ -0,0 +1,69 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Regression test for bug 1293616, where editing a selector should +// change the relative priority of the rule. + +const TEST_URI = ` + <style type="text/css"> + #testid { + background: aqua; + } + .pickme { + background: seagreen; + } + span { + background: fuchsia; + } + </style> + <div> + <span id="testid" class="pickme"> + Styled Node + </span> + </div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode(".pickme", inspector); + yield testEditSelector(view); +}); + +function* testEditSelector(view) { + let ruleEditor = getRuleViewRuleEditor(view, 1); + let editor = yield focusEditableField(view, ruleEditor.selectorText); + + editor.input.value = ".pickme"; + let onRuleViewChanged = once(view, "ruleview-changed"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + // Escape the new property editor after editing the selector + let onBlur = once(view.styleDocument.activeElement, "blur"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow); + yield onBlur; + + // Get the new rule editor that replaced the original + ruleEditor = getRuleViewRuleEditor(view, 1); + + info("Check that the correct rules are visible"); + is(view._elementStyle.rules.length, 4, "Should have 4 rules."); + is(ruleEditor.element.getAttribute("unmatched"), "false", "Rule editor is matched."); + + let props = ruleEditor.rule.textProps; + is(props.length, 1, "Rule has correct number of properties"); + is(props[0].name, "background", "Found background property"); + is(props[0].value, "aqua", "Background property is aqua"); + ok(props[0].overridden, "Background property is overridden"); + + ruleEditor = getRuleViewRuleEditor(view, 2); + props = ruleEditor.rule.textProps; + is(props.length, 1, "Rule has correct number of properties"); + is(props[0].name, "background", "Found background property"); + is(props[0].value, "seagreen", "Background property is seagreen"); + ok(!props[0].overridden, "Background property is not overridden"); +}
--- a/devtools/client/inspector/rules/views/rule-editor.js +++ b/devtools/client/inspector/rules/views/rule-editor.js @@ -22,16 +22,17 @@ const { parsePseudoClassesAndAttributes, SELECTOR_ATTRIBUTE, SELECTOR_ELEMENT, SELECTOR_PSEUDO_CLASS } = require("devtools/shared/css/parsing-utils"); const promise = require("promise"); const Services = require("Services"); const EventEmitter = require("devtools/shared/event-emitter"); +const {Task} = require("devtools/shared/task"); const STYLE_INSPECTOR_PROPERTIES = "devtools-shared/locale/styleinspector.properties"; const {LocalizationHelper} = require("devtools/shared/l10n"); const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES); const HTML_NS = "http://www.w3.org/1999/xhtml"; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; @@ -512,72 +513,98 @@ RuleEditor.prototype = { * * @param {String} value * The value contained in the editor. * @param {Boolean} commit * True if the change should be applied. * @param {Number} direction * The move focus direction number. */ - _onSelectorDone: function (value, commit, direction) { + _onSelectorDone: Task.async(function* (value, commit, direction) { if (!commit || this.isEditing || value === "" || value === this.rule.selectorText) { return; } let ruleView = this.ruleView; let elementStyle = ruleView._elementStyle; let element = elementStyle.element; let supportsUnmatchedRules = this.rule.domRule.supportsModifySelectorUnmatched; this.isEditing = true; - this.rule.domRule.modifySelector(element, value).then(response => { - this.isEditing = false; + try { + let response = yield this.rule.domRule.modifySelector(element, value); if (!supportsUnmatchedRules) { + this.isEditing = false; + if (response) { this.ruleView.refreshPanel(); } return; } + // We recompute the list of applied styles, because editing a + // selector might cause this rule's position to change. + let applied = yield elementStyle.pageStyle.getApplied(element, { + inherited: true, + matchedSelectors: true, + filter: elementStyle.showUserAgentStyles ? "ua" : undefined + }); + + this.isEditing = false; + let {ruleProps, isMatching} = response; if (!ruleProps) { // Notify for changes, even when nothing changes, // just to allow tests being able to track end of this request. ruleView.emit("ruleview-invalid-selector"); return; } ruleProps.isUnmatched = !isMatching; let newRule = new Rule(elementStyle, ruleProps); let editor = new RuleEditor(ruleView, newRule); let rules = elementStyle.rules; - rules.splice(rules.indexOf(this.rule), 1); - rules.push(newRule); + let newRuleIndex = applied.findIndex((r) => r.rule == ruleProps.rule); + let oldIndex = rules.indexOf(this.rule); + + // If the selector no longer matches, then we leave the rule in + // the same relative position. + if (newRuleIndex === -1) { + newRuleIndex = oldIndex; + } + + // Remove the old rule and insert the new rule. + rules.splice(oldIndex, 1); + rules.splice(newRuleIndex, 0, newRule); elementStyle._changed(); elementStyle.markOverriddenAll(); + // We install the new editor in place of the old -- you might + // think we would replicate the list-modification logic above, + // but that is complicated due to the way the UI installs + // pseudo-element rules and the like. this.element.parentNode.replaceChild(editor.element, this.element); // Remove highlight for modified selector if (ruleView.highlightedSelector) { ruleView.toggleSelectorHighlighter(ruleView.lastSelectorIcon, ruleView.highlightedSelector); } editor._moveSelectorFocus(direction); - }).then(null, err => { + } catch (err) { this.isEditing = false; promiseWarn(err); - }); - }, + } + }), /** * Handle moving the focus change after a tab or return keypress in the * selector inplace editor. * * @param {Number} direction * The move focus direction number. */
new file mode 100644 --- /dev/null +++ b/devtools/client/netmonitor/events.js @@ -0,0 +1,86 @@ +"use strict"; + +// The panel's window global is an EventEmitter firing the following events: +const EVENTS = { + // When the monitored target begins and finishes navigating. + TARGET_WILL_NAVIGATE: "NetMonitor:TargetWillNavigate", + TARGET_DID_NAVIGATE: "NetMonitor:TargetNavigate", + + // When a network or timeline event is received. + // See https://developer.mozilla.org/docs/Tools/Web_Console/remoting for + // more information about what each packet is supposed to deliver. + NETWORK_EVENT: "NetMonitor:NetworkEvent", + TIMELINE_EVENT: "NetMonitor:TimelineEvent", + + // When a network event is added to the view + REQUEST_ADDED: "NetMonitor:RequestAdded", + + // When request headers begin and finish receiving. + UPDATING_REQUEST_HEADERS: "NetMonitor:NetworkEventUpdating:RequestHeaders", + RECEIVED_REQUEST_HEADERS: "NetMonitor:NetworkEventUpdated:RequestHeaders", + + // When request cookies begin and finish receiving. + UPDATING_REQUEST_COOKIES: "NetMonitor:NetworkEventUpdating:RequestCookies", + RECEIVED_REQUEST_COOKIES: "NetMonitor:NetworkEventUpdated:RequestCookies", + + // When request post data begins and finishes receiving. + UPDATING_REQUEST_POST_DATA: "NetMonitor:NetworkEventUpdating:RequestPostData", + RECEIVED_REQUEST_POST_DATA: "NetMonitor:NetworkEventUpdated:RequestPostData", + + // When security information begins and finishes receiving. + UPDATING_SECURITY_INFO: "NetMonitor::NetworkEventUpdating:SecurityInfo", + RECEIVED_SECURITY_INFO: "NetMonitor::NetworkEventUpdated:SecurityInfo", + + // When response headers begin and finish receiving. + UPDATING_RESPONSE_HEADERS: "NetMonitor:NetworkEventUpdating:ResponseHeaders", + RECEIVED_RESPONSE_HEADERS: "NetMonitor:NetworkEventUpdated:ResponseHeaders", + + // When response cookies begin and finish receiving. + UPDATING_RESPONSE_COOKIES: "NetMonitor:NetworkEventUpdating:ResponseCookies", + RECEIVED_RESPONSE_COOKIES: "NetMonitor:NetworkEventUpdated:ResponseCookies", + + // When event timings begin and finish receiving. + UPDATING_EVENT_TIMINGS: "NetMonitor:NetworkEventUpdating:EventTimings", + RECEIVED_EVENT_TIMINGS: "NetMonitor:NetworkEventUpdated:EventTimings", + + // When response content begins, updates and finishes receiving. + STARTED_RECEIVING_RESPONSE: "NetMonitor:NetworkEventUpdating:ResponseStart", + UPDATING_RESPONSE_CONTENT: "NetMonitor:NetworkEventUpdating:ResponseContent", + RECEIVED_RESPONSE_CONTENT: "NetMonitor:NetworkEventUpdated:ResponseContent", + + // When the request post params are displayed in the UI. + REQUEST_POST_PARAMS_DISPLAYED: "NetMonitor:RequestPostParamsAvailable", + + // When the response body is displayed in the UI. + RESPONSE_BODY_DISPLAYED: "NetMonitor:ResponseBodyAvailable", + + // When the html response preview is displayed in the UI. + RESPONSE_HTML_PREVIEW_DISPLAYED: "NetMonitor:ResponseHtmlPreviewAvailable", + + // When the image response thumbnail is displayed in the UI. + RESPONSE_IMAGE_THUMBNAIL_DISPLAYED: + "NetMonitor:ResponseImageThumbnailAvailable", + + // When a tab is selected in the NetworkDetailsView and subsequently rendered. + TAB_UPDATED: "NetMonitor:TabUpdated", + + // Fired when Sidebar has finished being populated. + SIDEBAR_POPULATED: "NetMonitor:SidebarPopulated", + + // Fired when NetworkDetailsView has finished being populated. + NETWORKDETAILSVIEW_POPULATED: "NetMonitor:NetworkDetailsViewPopulated", + + // Fired when CustomRequestView has finished being populated. + CUSTOMREQUESTVIEW_POPULATED: "NetMonitor:CustomRequestViewPopulated", + + // Fired when charts have been displayed in the PerformanceStatisticsView. + PLACEHOLDER_CHARTS_DISPLAYED: "NetMonitor:PlaceholderChartsDisplayed", + PRIMED_CACHE_CHART_DISPLAYED: "NetMonitor:PrimedChartsDisplayed", + EMPTY_CACHE_CHART_DISPLAYED: "NetMonitor:EmptyChartsDisplayed", + + // Fired once the NetMonitorController establishes a connection to the debug + // target. + CONNECTED: "connected", +}; + +exports.EVENTS = EVENTS;
--- a/devtools/client/netmonitor/moz.build +++ b/devtools/client/netmonitor/moz.build @@ -3,17 +3,19 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. DIRS += [ 'har' ] DevToolsModules( + 'events.js', 'filter-predicates.js', 'l10n.js', 'panel.js', 'prefs.js', 'request-utils.js', 'requests-menu-view.js', + 'sort-predicates.js', ) BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
--- a/devtools/client/netmonitor/netmonitor-controller.js +++ b/devtools/client/netmonitor/netmonitor-controller.js @@ -4,99 +4,16 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* globals window, document, NetMonitorView */ /* exported loader */ "use strict"; var { utils: Cu } = Components; -// The panel's window global is an EventEmitter firing the following events: -const EVENTS = { - // When the monitored target begins and finishes navigating. - TARGET_WILL_NAVIGATE: "NetMonitor:TargetWillNavigate", - TARGET_DID_NAVIGATE: "NetMonitor:TargetNavigate", - - // When a network or timeline event is received. - // See https://developer.mozilla.org/docs/Tools/Web_Console/remoting for - // more information about what each packet is supposed to deliver. - NETWORK_EVENT: "NetMonitor:NetworkEvent", - TIMELINE_EVENT: "NetMonitor:TimelineEvent", - - // When a network event is added to the view - REQUEST_ADDED: "NetMonitor:RequestAdded", - - // When request headers begin and finish receiving. - UPDATING_REQUEST_HEADERS: "NetMonitor:NetworkEventUpdating:RequestHeaders", - RECEIVED_REQUEST_HEADERS: "NetMonitor:NetworkEventUpdated:RequestHeaders", - - // When request cookies begin and finish receiving. - UPDATING_REQUEST_COOKIES: "NetMonitor:NetworkEventUpdating:RequestCookies", - RECEIVED_REQUEST_COOKIES: "NetMonitor:NetworkEventUpdated:RequestCookies", - - // When request post data begins and finishes receiving. - UPDATING_REQUEST_POST_DATA: "NetMonitor:NetworkEventUpdating:RequestPostData", - RECEIVED_REQUEST_POST_DATA: "NetMonitor:NetworkEventUpdated:RequestPostData", - - // When security information begins and finishes receiving. - UPDATING_SECURITY_INFO: "NetMonitor::NetworkEventUpdating:SecurityInfo", - RECEIVED_SECURITY_INFO: "NetMonitor::NetworkEventUpdated:SecurityInfo", - - // When response headers begin and finish receiving. - UPDATING_RESPONSE_HEADERS: "NetMonitor:NetworkEventUpdating:ResponseHeaders", - RECEIVED_RESPONSE_HEADERS: "NetMonitor:NetworkEventUpdated:ResponseHeaders", - - // When response cookies begin and finish receiving. - UPDATING_RESPONSE_COOKIES: "NetMonitor:NetworkEventUpdating:ResponseCookies", - RECEIVED_RESPONSE_COOKIES: "NetMonitor:NetworkEventUpdated:ResponseCookies", - - // When event timings begin and finish receiving. - UPDATING_EVENT_TIMINGS: "NetMonitor:NetworkEventUpdating:EventTimings", - RECEIVED_EVENT_TIMINGS: "NetMonitor:NetworkEventUpdated:EventTimings", - - // When response content begins, updates and finishes receiving. - STARTED_RECEIVING_RESPONSE: "NetMonitor:NetworkEventUpdating:ResponseStart", - UPDATING_RESPONSE_CONTENT: "NetMonitor:NetworkEventUpdating:ResponseContent", - RECEIVED_RESPONSE_CONTENT: "NetMonitor:NetworkEventUpdated:ResponseContent", - - // When the request post params are displayed in the UI. - REQUEST_POST_PARAMS_DISPLAYED: "NetMonitor:RequestPostParamsAvailable", - - // When the response body is displayed in the UI. - RESPONSE_BODY_DISPLAYED: "NetMonitor:ResponseBodyAvailable", - - // When the html response preview is displayed in the UI. - RESPONSE_HTML_PREVIEW_DISPLAYED: "NetMonitor:ResponseHtmlPreviewAvailable", - - // When the image response thumbnail is displayed in the UI. - RESPONSE_IMAGE_THUMBNAIL_DISPLAYED: - "NetMonitor:ResponseImageThumbnailAvailable", - - // When a tab is selected in the NetworkDetailsView and subsequently rendered. - TAB_UPDATED: "NetMonitor:TabUpdated", - - // Fired when Sidebar has finished being populated. - SIDEBAR_POPULATED: "NetMonitor:SidebarPopulated", - - // Fired when NetworkDetailsView has finished being populated. - NETWORKDETAILSVIEW_POPULATED: "NetMonitor:NetworkDetailsViewPopulated", - - // Fired when CustomRequestView has finished being populated. - CUSTOMREQUESTVIEW_POPULATED: "NetMonitor:CustomRequestViewPopulated", - - // Fired when charts have been displayed in the PerformanceStatisticsView. - PLACEHOLDER_CHARTS_DISPLAYED: "NetMonitor:PlaceholderChartsDisplayed", - PRIMED_CACHE_CHART_DISPLAYED: "NetMonitor:PrimedChartsDisplayed", - EMPTY_CACHE_CHART_DISPLAYED: "NetMonitor:EmptyChartsDisplayed", - - // Fired once the NetMonitorController establishes a connection to the debug - // target. - CONNECTED: "connected", -}; - // Descriptions for what this frontend is currently doing. const ACTIVITY_TYPE = { // Standing by and handling requests normally. NONE: 0, // Forcing the target to reload with cache enabled or disabled. RELOAD: { WITH_CACHE_ENABLED: 1, @@ -119,16 +36,17 @@ var { loader, require } = BrowserLoaderM const promise = require("promise"); const Services = require("Services"); const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm"); const EventEmitter = require("devtools/shared/event-emitter"); const Editor = require("devtools/client/sourceeditor/editor"); const {TimelineFront} = require("devtools/shared/fronts/timeline"); const {Task} = require("devtools/shared/task"); const {Prefs} = require("./prefs"); +const {EVENTS} = require("./events"); XPCOMUtils.defineConstant(this, "EVENTS", EVENTS); XPCOMUtils.defineConstant(this, "ACTIVITY_TYPE", ACTIVITY_TYPE); XPCOMUtils.defineConstant(this, "Editor", Editor); XPCOMUtils.defineConstant(this, "Prefs", Prefs); XPCOMUtils.defineLazyModuleGetter(this, "Chart", "resource://devtools/client/shared/widgets/Chart.jsm");
--- a/devtools/client/netmonitor/netmonitor-view.js +++ b/devtools/client/netmonitor/netmonitor-view.js @@ -14,17 +14,21 @@ XPCOMUtils.defineLazyGetter(this, "Netwo const {VariablesView} = require("resource://devtools/client/shared/widgets/VariablesView.jsm"); const {VariablesViewController} = require("resource://devtools/client/shared/widgets/VariablesViewController.jsm"); const {ToolSidebar} = require("devtools/client/framework/sidebar"); const { testing: isTesting } = require("devtools/shared/flags"); const {ViewHelpers, Heritage} = require("devtools/client/shared/widgets/view-helpers"); const {PluralForm} = require("devtools/shared/plural-form"); const {Filters} = require("./filter-predicates"); -const {getFormDataSections, formDataURI, writeHeaderText, getKeyWithEvent} = require("./request-utils"); +const {getFormDataSections, + formDataURI, + writeHeaderText, + getKeyWithEvent, + getUriHostPort} = require("./request-utils"); const {L10N} = require("./l10n"); const {RequestsMenuView} = require("./requests-menu-view"); // ms const WDA_DEFAULT_VERIFY_INTERVAL = 50; // Use longer timeout during testing as the tests need this process to succeed // and two seconds is quite short on slow debug builds. The timeout here should @@ -1310,17 +1314,17 @@ NetworkDetailsView.prototype = { let disabledLabel = L10N.getStr("netmonitor.security.disabled"); // Connection parameters setValue("#security-protocol-version-value", securityInfo.protocolVersion); setValue("#security-ciphersuite-value", securityInfo.cipherSuite); // Host header - let domain = NetMonitorView.RequestsMenu._getUriHostPort(url); + let domain = getUriHostPort(url); let hostHeader = L10N.getFormatStr("netmonitor.security.hostHeader", domain); setValue("#security-info-host-header", hostHeader); // Parameters related to the domain setValue("#security-http-strict-transport-security-value", securityInfo.hsts ? enabledLabel : disabledLabel);
--- a/devtools/client/netmonitor/request-utils.js +++ b/devtools/client/netmonitor/request-utils.js @@ -1,13 +1,14 @@ "use strict"; const { Ci } = require("chrome"); const { KeyCodes } = require("devtools/client/shared/keycodes"); const { Task } = require("devtools/shared/task"); +const NetworkHelper = require("devtools/shared/webconsole/network-helper"); /** * Helper method to get a wrapped function which can be bound to as * an event listener directly and is executed only when data-key is * present in event.target. * * @param function callback * Function to execute execute when data-key @@ -104,16 +105,58 @@ exports.formDataURI = function (mimeType * @return string text * List of headers in text format */ exports.writeHeaderText = function (headers) { return headers.map(({name, value}) => name + ": " + value).join("\n"); }; /** + * Helper for getting an abbreviated string for a mime type. + * + * @param string mimeType + * @return string + */ +exports.getAbbreviatedMimeType = function (mimeType) { + if (!mimeType) { + return ""; + } + return (mimeType.split(";")[0].split("/")[1] || "").split("+")[0]; +}; + +/** + * Helpers for getting details about an nsIURL. + * + * @param nsIURL | string url + * @return string + */ +exports.getUriNameWithQuery = function (url) { + if (!(url instanceof Ci.nsIURL)) { + url = NetworkHelper.nsIURL(url); + } + + let name = NetworkHelper.convertToUnicode( + unescape(url.fileName || url.filePath || "/")); + let query = NetworkHelper.convertToUnicode(unescape(url.query)); + + return name + (query ? "?" + query : ""); +}; + +exports.getUriHostPort = function (url) { + if (!(url instanceof Ci.nsIURL)) { + url = NetworkHelper.nsIURL(url); + } + return NetworkHelper.convertToUnicode(unescape(url.hostPort)); +}; + +exports.getUriHost = function (url) { + return exports.getUriHostPort(url).replace(/:\d+$/, ""); +}; + +/** * Convert a nsIContentPolicy constant to a display string */ const LOAD_CAUSE_STRINGS = { [Ci.nsIContentPolicy.TYPE_INVALID]: "invalid", [Ci.nsIContentPolicy.TYPE_OTHER]: "other", [Ci.nsIContentPolicy.TYPE_SCRIPT]: "script", [Ci.nsIContentPolicy.TYPE_IMAGE]: "img", [Ci.nsIContentPolicy.TYPE_STYLESHEET]: "stylesheet",
--- a/devtools/client/netmonitor/requests-menu-view.js +++ b/devtools/client/netmonitor/requests-menu-view.js @@ -11,19 +11,27 @@ const {HTMLTooltip} = require("devtools/ const {setImageTooltip, getImageDimensions} = require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper"); const {Heritage, WidgetMethods, setNamedTimeout} = require("devtools/client/shared/widgets/view-helpers"); const {gDevTools} = require("devtools/client/framework/devtools"); const {Curl, CurlUtils} = require("devtools/client/shared/curl"); const {PluralForm} = require("devtools/shared/plural-form"); const {Filters, isFreetextMatch} = require("./filter-predicates"); -const {getFormDataSections, formDataURI, writeHeaderText, getKeyWithEvent, +const {Sorters} = require("./sort-predicates"); +const {L10N, WEBCONSOLE_L10N} = require("./l10n"); +const {getFormDataSections, + formDataURI, + writeHeaderText, + getKeyWithEvent, + getAbbreviatedMimeType, + getUriNameWithQuery, + getUriHostPort, + getUriHost, loadCauseString} = require("./request-utils"); -const {L10N, WEBCONSOLE_L10N} = require("./l10n"); loader.lazyServiceGetter(this, "clipboardHelper", "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper"); loader.lazyRequireGetter(this, "HarExporter", "devtools/client/netmonitor/har/har-exporter", true); loader.lazyRequireGetter(this, "NetworkHelper", @@ -87,19 +95,16 @@ function RequestsMenuView() { dumpn("RequestsMenuView was instantiated"); this._flushRequests = this._flushRequests.bind(this); this._onHover = this._onHover.bind(this); this._onSelect = this._onSelect.bind(this); this._onSwap = this._onSwap.bind(this); this._onResize = this._onResize.bind(this); this._onScroll = this._onScroll.bind(this); - this._byFile = this._byFile.bind(this); - this._byDomain = this._byDomain.bind(this); - this._byType = this._byType.bind(this); this._onSecurityIconClick = this._onSecurityIconClick.bind(this); } RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { /** * Initialization function, called when the network monitor is started. */ initialize: function () { @@ -117,17 +122,17 @@ RequestsMenuView.prototype = Heritage.ex this.tooltip = new HTMLTooltip(NetMonitorController._toolbox.doc, { type: "arrow" }); this.tooltip.startTogglingOnHover(widgetParentEl, this._onHover, { toggleDelay: REQUESTS_TOOLTIP_TOGGLE_DELAY, interactive: true }); $("#requests-menu-contents").addEventListener("scroll", this._onScroll, true); Prefs.filters.forEach(type => this.filterOn(type)); - this.sortContents(this._byTiming); + this.sortContents((a, b) => Sorters.waterfall(a.attachment, b.attachment)); this.allowFocusOnRightClick = true; this.maintainSelectionVisible = true; this.widget.addEventListener("select", this._onSelect, false); this.widget.addEventListener("swap", this._onSwap, false); this._splitter.addEventListener("mousemove", this._onResize, false); window.addEventListener("resize", this._onResize, false); @@ -784,75 +789,75 @@ RequestsMenuView.prototype = Heritage.ex // Used to style the next column. target.parentNode.setAttribute("active", "true"); } // Sort by whatever was requested. switch (type) { case "status": if (direction == "ascending") { - this.sortContents(this._byStatus); + this.sortContents((a, b) => Sorters.status(a.attachment, b.attachment)); } else { - this.sortContents((a, b) => !this._byStatus(a, b)); + this.sortContents((a, b) => -Sorters.status(a.attachment, b.attachment)); } break; case "method": if (direction == "ascending") { - this.sortContents(this._byMethod); + this.sortContents((a, b) => Sorters.method(a.attachment, b.attachment)); } else { - this.sortContents((a, b) => !this._byMethod(a, b)); + this.sortContents((a, b) => -Sorters.method(a.attachment, b.attachment)); } break; case "file": if (direction == "ascending") { - this.sortContents(this._byFile); + this.sortContents((a, b) => Sorters.file(a.attachment, b.attachment)); } else { - this.sortContents((a, b) => !this._byFile(a, b)); + this.sortContents((a, b) => -Sorters.file(a.attachment, b.attachment)); } break; case "domain": if (direction == "ascending") { - this.sortContents(this._byDomain); + this.sortContents((a, b) => Sorters.domain(a.attachment, b.attachment)); } else { - this.sortContents((a, b) => !this._byDomain(a, b)); + this.sortContents((a, b) => -Sorters.domain(a.attachment, b.attachment)); } break; case "cause": if (direction == "ascending") { - this.sortContents(this._byCause); + this.sortContents((a, b) => Sorters.cause(a.attachment, b.attachment)); } else { - this.sortContents((a, b) => !this._byCause(a, b)); + this.sortContents((a, b) => -Sorters.cause(a.attachment, b.attachment)); } break; case "type": if (direction == "ascending") { - this.sortContents(this._byType); + this.sortContents((a, b) => Sorters.type(a.attachment, b.attachment)); } else { - this.sortContents((a, b) => !this._byType(a, b)); + this.sortContents((a, b) => -Sorters.type(a.attachment, b.attachment)); } break; case "transferred": if (direction == "ascending") { - this.sortContents(this._byTransferred); + this.sortContents((a, b) => Sorters.transferred(a.attachment, b.attachment)); } else { - this.sortContents((a, b) => !this._byTransferred(a, b)); + this.sortContents((a, b) => -Sorters.transferred(a.attachment, b.attachment)); } break; case "size": if (direction == "ascending") { - this.sortContents(this._bySize); + this.sortContents((a, b) => Sorters.size(a.attachment, b.attachment)); } else { - this.sortContents((a, b) => !this._bySize(a, b)); + this.sortContents((a, b) => -Sorters.size(a.attachment, b.attachment)); } break; case "waterfall": if (direction == "ascending") { - this.sortContents(this._byTiming); + this.sortContents((a, b) => Sorters.waterfall(a.attachment, b.attachment)); } else { - this.sortContents((a, b) => !this._byTiming(a, b)); + this.sortContents((a, b) => -Sorters.waterfall(a.attachment, b.attachment)); } break; } this.refreshSummary(); this.refreshZebra(); }, @@ -866,86 +871,16 @@ RequestsMenuView.prototype = Heritage.ex $("#details-pane-toggle").disabled = true; $("#requests-menu-empty-notice").hidden = false; this.empty(); this.refreshSummary(); }, /** - * Predicates used when sorting items. - * - * @param object aFirst - * The first item used in the comparison. - * @param object aSecond - * The second item used in the comparison. - * @return number - * -1 to sort aFirst to a lower index than aSecond - * 0 to leave aFirst and aSecond unchanged with respect to each other - * 1 to sort aSecond to a lower index than aFirst - */ - _byTiming: function ({ attachment: first }, { attachment: second }) { - return first.startedMillis > second.startedMillis; - }, - - _byStatus: function ({ attachment: first }, { attachment: second }) { - return first.status == second.status - ? first.startedMillis > second.startedMillis - : first.status > second.status; - }, - - _byMethod: function ({ attachment: first }, { attachment: second }) { - return first.method == second.method - ? first.startedMillis > second.startedMillis - : first.method > second.method; - }, - - _byFile: function ({ attachment: first }, { attachment: second }) { - let firstUrl = this._getUriNameWithQuery(first.url).toLowerCase(); - let secondUrl = this._getUriNameWithQuery(second.url).toLowerCase(); - return firstUrl == secondUrl - ? first.startedMillis > second.startedMillis - : firstUrl > secondUrl; - }, - - _byDomain: function ({ attachment: first }, { attachment: second }) { - let firstDomain = this._getUriHostPort(first.url).toLowerCase(); - let secondDomain = this._getUriHostPort(second.url).toLowerCase(); - return firstDomain == secondDomain - ? first.startedMillis > second.startedMillis - : firstDomain > secondDomain; - }, - - _byCause: function ({ attachment: first }, { attachment: second }) { - let firstCause = loadCauseString(first.cause.type); - let secondCause = loadCauseString(second.cause.type); - - return firstCause == secondCause - ? first.startedMillis > second.startedMillis - : firstCause > secondCause; - }, - - _byType: function ({ attachment: first }, { attachment: second }) { - let firstType = this._getAbbreviatedMimeType(first.mimeType).toLowerCase(); - let secondType = this._getAbbreviatedMimeType(second.mimeType).toLowerCase(); - - return firstType == secondType - ? first.startedMillis > second.startedMillis - : firstType > secondType; - }, - - _byTransferred: function ({ attachment: first }, { attachment: second }) { - return first.transferredSize > second.transferredSize; - }, - - _bySize: function ({ attachment: first }, { attachment: second }) { - return first.contentSize > second.contentSize; - }, - - /** * Refreshes the status displayed in this container's footer, providing * concise information about all requests. */ refreshSummary: function () { let visibleItems = this.visibleItems; let visibleRequestsCount = visibleItems.length; if (!visibleRequestsCount) { this._summary.setAttribute("label", L10N.getStr("networkMenu.empty")); @@ -1340,19 +1275,19 @@ RequestsMenuView.prototype = Heritage.ex case "url": { let uri; try { uri = NetworkHelper.nsIURL(value); } catch (e) { // User input may not make a well-formed url yet. break; } - let nameWithQuery = this._getUriNameWithQuery(uri); - let hostPort = this._getUriHostPort(uri); - let host = this._getUriHost(uri); + let nameWithQuery = getUriNameWithQuery(uri); + let hostPort = getUriHostPort(uri); + let host = getUriHost(uri); let unicodeUrl = NetworkHelper.convertToUnicode(unescape(uri.spec)); let file = $(".requests-menu-file", target); file.setAttribute("value", nameWithQuery); file.setAttribute("tooltiptext", unicodeUrl); let domain = $(".requests-menu-domain", target); domain.setAttribute("value", hostPort); @@ -1459,17 +1394,17 @@ RequestsMenuView.prototype = Heritage.ex text = this.getFormattedSize(value); } node.setAttribute("value", text); node.setAttribute("tooltiptext", text); break; } case "mimeType": { - let type = this._getAbbreviatedMimeType(value); + let type = getAbbreviatedMimeType(value); let node = $(".requests-menu-type", target); let text = CONTENT_MIME_TYPE_ABBREVIATIONS[type] || type; node.setAttribute("value", text); node.setAttribute("tooltiptext", value); break; } case "responseContent": { let { mimeType } = item.attachment; @@ -1989,58 +1924,16 @@ RequestsMenuView.prototype = Heritage.ex */ _registerLastRequestEnd: function (unixTime) { if (this._lastRequestEndedMillis < unixTime) { this._lastRequestEndedMillis = unixTime; } }, /** - * Helpers for getting details about an nsIURL. - * - * @param nsIURL | string url - * @return string - */ - _getUriNameWithQuery: function (url) { - if (!(url instanceof Ci.nsIURL)) { - url = NetworkHelper.nsIURL(url); - } - - let name = NetworkHelper.convertToUnicode( - unescape(url.fileName || url.filePath || "/")); - let query = NetworkHelper.convertToUnicode(unescape(url.query)); - - return name + (query ? "?" + query : ""); - }, - - _getUriHostPort: function (url) { - if (!(url instanceof Ci.nsIURL)) { - url = NetworkHelper.nsIURL(url); - } - return NetworkHelper.convertToUnicode(unescape(url.hostPort)); - }, - - _getUriHost: function (url) { - return this._getUriHostPort(url).replace(/:\d+$/, ""); - }, - - /** - * Helper for getting an abbreviated string for a mime type. - * - * @param string mimeType - * @return string - */ - _getAbbreviatedMimeType: function (mimeType) { - if (!mimeType) { - return ""; - } - return (mimeType.split(";")[0].split("/")[1] || "").split("+")[0]; - }, - - /** * Gets the total number of bytes representing the cumulated content size of * a set of requests. Returns 0 for an empty set. * * @param array itemsArray * @return number */ _getTotalBytesOfRequests: function (itemsArray) { if (!itemsArray.length) {
new file mode 100644 --- /dev/null +++ b/devtools/client/netmonitor/sort-predicates.js @@ -0,0 +1,92 @@ +"use strict"; + +const { getAbbreviatedMimeType, + getUriNameWithQuery, + getUriHostPort, + loadCauseString } = require("./request-utils"); + +/** + * Predicates used when sorting items. + * + * @param object first + * The first item used in the comparison. + * @param object second + * The second item used in the comparison. + * @return number + * <0 to sort first to a lower index than second + * =0 to leave first and second unchanged with respect to each other + * >0 to sort second to a lower index than first + */ + +function waterfall(first, second) { + return first.startedMillis - second.startedMillis; +} + +function status(first, second) { + return first.status == second.status + ? first.startedMillis - second.startedMillis + : first.status - second.status; +} + +function method(first, second) { + if (first.method == second.method) { + return first.startedMillis - second.startedMillis; + } + return first.method > second.method ? 1 : -1; +} + +function file(first, second) { + let firstUrl = getUriNameWithQuery(first.url).toLowerCase(); + let secondUrl = getUriNameWithQuery(second.url).toLowerCase(); + if (firstUrl == secondUrl) { + return first.startedMillis - second.startedMillis; + } + return firstUrl > secondUrl ? 1 : -1; +} + +function domain(first, second) { + let firstDomain = getUriHostPort(first.url).toLowerCase(); + let secondDomain = getUriHostPort(second.url).toLowerCase(); + if (firstDomain == secondDomain) { + return first.startedMillis - second.startedMillis; + } + return firstDomain > secondDomain ? 1 : -1; +} + +function cause(first, second) { + let firstCause = loadCauseString(first.cause.type); + let secondCause = loadCauseString(second.cause.type); + if (firstCause == secondCause) { + return first.startedMillis - second.startedMillis; + } + return firstCause > secondCause ? 1 : -1; +} + +function type(first, second) { + let firstType = getAbbreviatedMimeType(first.mimeType).toLowerCase(); + let secondType = getAbbreviatedMimeType(second.mimeType).toLowerCase(); + if (firstType == secondType) { + return first.startedMillis - second.startedMillis; + } + return firstType > secondType ? 1 : -1; +} + +function transferred(first, second) { + return first.transferredSize - second.transferredSize; +} + +function size(first, second) { + return first.contentSize - second.contentSize; +} + +exports.Sorters = { + status, + method, + file, + domain, + cause, + type, + transferred, + size, + waterfall, +};
--- a/devtools/client/shared/components/moz.build +++ b/devtools/client/shared/components/moz.build @@ -11,16 +11,17 @@ DIRS += [ 'tree' ] DevToolsModules( 'frame.js', 'h-split-box.js', 'notification-box.css', 'notification-box.js', + 'search-box.js', 'sidebar-toggle.css', 'sidebar-toggle.js', 'stack-trace.js', 'tree.js', ) MOCHITEST_CHROME_MANIFESTS += ['test/mochitest/chrome.ini'] BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
new file mode 100644 --- /dev/null +++ b/devtools/client/shared/components/search-box.js @@ -0,0 +1,107 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* global window */ + +"use strict"; + +const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react"); +const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts"); + +/** + * A generic search box component for use across devtools + */ +module.exports = createClass({ + displayName: "SearchBox", + + propTypes: { + delay: PropTypes.number, + keyShortcut: PropTypes.string, + onChange: PropTypes.func, + placeholder: PropTypes.string, + type: PropTypes.string + }, + + getInitialState() { + return { + value: "" + }; + }, + + componentDidMount() { + if (!this.props.keyShortcut) { + return; + } + + this.shortcuts = new KeyShortcuts({ + window + }); + this.shortcuts.on(this.props.keyShortcut, (name, event) => { + event.preventDefault(); + this.refs.input.focus(); + }); + }, + + componentWillUnmount() { + this.shortcuts.destroy(); + // Clean up an existing timeout. + if (this.searchTimeout) { + clearTimeout(this.searchTimeout); + } + }, + + onChange() { + if (this.state.value !== this.refs.input.value) { + this.setState({ value: this.refs.input.value }); + } + + if (!this.props.delay) { + this.props.onChange(this.state.value); + return; + } + + // Clean up an existing timeout before creating a new one. + if (this.searchTimeout) { + clearTimeout(this.searchTimeout); + } + + // Execute the search after a timeout. It makes the UX + // smoother if the user is typing quickly. + this.searchTimeout = setTimeout(() => { + this.searchTimeout = null; + this.props.onChange(this.state.value); + }, this.props.delay); + }, + + onClearButtonClick() { + this.refs.input.value = ""; + this.onChange(); + }, + + render() { + let { type = "search", placeholder } = this.props; + let { value } = this.state; + let divClassList = ["devtools-searchbox", "has-clear-btn"]; + let inputClassList = [`devtools-${type}input`]; + + if (value !== "") { + inputClassList.push("filled"); + } + return dom.div( + { className: divClassList.join(" ") }, + dom.input({ + className: inputClassList.join(" "), + onChange: this.onChange, + placeholder, + ref: "input", + value + }), + dom.button({ + className: "devtools-searchinput-clear", + hidden: value == "", + onClick: this.onClearButtonClick + }) + ); + } +});
--- a/devtools/client/themes/images/filters.svg +++ b/devtools/client/themes/images/filters.svg @@ -14,18 +14,18 @@ <feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 0 0 0 0 0 1 0 0 0 0 0.212 0 0 0 1 0"/> </filter> <!-- Web Audio Gradients --> - <linearGradient id="bypass-light" x1="6%" y1="8%" x2="12%" y2="12%" spreadMethod="repeat"> - <stop offset="0%" stop-color="#f0f1f2"/> <!-- theme-toolbar-background --> - <stop offset="50%" stop-color="#fff"/> + <linearGradient id="bypass-light" x1="8%" y1="10%" x2="16%" y2="16%" spreadMethod="repeat"> + <stop offset="0%" stop-color="#dde1e4a0"/> <!-- theme-splitter-color (0.5 opacity) --> + <stop offset="50%" stop-color="transparent"/> </linearGradient> - <linearGradient id="bypass-dark" x1="6%" y1="8%" x2="12%" y2="12%" spreadMethod="repeat"> - <stop offset="0%" stop-color="#343c45"/> <!-- theme-toolbar-background --> + <linearGradient id="bypass-dark" x1="8%" y1="10%" x2="16%" y2="16%" spreadMethod="repeat"> + <stop offset="0%" stop-color="#454d5d"/> <!-- theme-splitter-color --> <stop offset="50%" stop-color="transparent"/> </linearGradient> </svg>
--- a/devtools/client/themes/memory.css +++ b/devtools/client/themes/memory.css @@ -590,50 +590,48 @@ html, body, #app, #memory-tool { text-align: center; padding: 5px; } /** * Dagre-D3 graphs */ +svg { + --arrow-color: var(--theme-splitter-color); + --text-color: var(--theme-body-color-alt); +} + +.theme-dark svg { + --arrow-color: var(--theme-body-color-alt); +} + +svg #arrowhead { + /* !important is needed to override inline style */ + fill: var(--arrow-color) !important; +} + .edgePath path { stroke-width: 1px; fill: none; -} - -.theme-dark .edgePath path { - stroke: var(--theme-body-color-alt); -} -.theme-light .edgePath path { - stroke: var(--theme-splitter-color); + stroke: var(--arrow-color); } g.edgeLabel rect { fill: var(--theme-body-background); } + g.edgeLabel tspan { - fill: var(--theme-body-color-alt); + fill: var(--text-color); } .nodes rect { stroke-width: 1px; -} - -.nodes rect { - stroke: var(--theme-tab-toolbar-background); -} -.theme-light rect { - fill: var(--theme-tab-toolbar-background); -} -.theme-dark rect { + stroke: var(--theme-splitter-color); fill: var(--theme-toolbar-background); } text { - font-weight: 300; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; - font-size: 14px; + font-size: 1.25em; + fill: var(--text-color); + /* Make sure text stays inside its container in RTL locales */ + direction: ltr; } - -text { - fill: var(--theme-body-color-alt); -}
--- a/devtools/client/themes/webaudioeditor.css +++ b/devtools/client/themes/webaudioeditor.css @@ -15,107 +15,100 @@ #waiting-notice { font-size: 110%; } /* Context Graph */ svg { overflow: hidden; -moz-box-flex: 1; + --arrow-color: var(--theme-splitter-color); + --text-color: var(--theme-body-color-alt); +} + +.theme-dark svg { + --arrow-color: var(--theme-body-color-alt); } /* Edges in graph */ .edgePath path { stroke-width: 1px; - fill: none; + stroke: var(--arrow-color); } - -.theme-dark .edgePath path { - stroke: var(--theme-body-color-alt); -} -.theme-light .edgePath path { - stroke: var(--theme-splitter-color); +svg #arrowhead { + /* !important is needed to override inline style */ + fill: var(--arrow-color) !important; } /* AudioParam connection edges */ -g.edgePath.param-connection { +g.edgePath.param-connection path { stroke-dasharray: 5,5; -} - -.theme-dark .edgePath.param-connection path { - stroke: var(--theme-body-color-alt); -} -.theme-light .edgePath.param-connection path { - stroke: var(--theme-splitter-color); + stroke: var(--arrow-colo); } /* Labels in AudioParam connection should have background that match * the main background so there's whitespace around the label, on top of the * dotted lines. */ g.edgeLabel rect { fill: var(--theme-body-background); } g.edgeLabel tspan { - fill: var(--theme-body-color-alt); + fill: var(--text-color); } /* Audio Nodes */ .nodes rect { stroke-width: 1px; cursor: pointer; - stroke: var(--theme-tab-toolbar-background); + stroke: var(--theme-splitter-color); fill: var(--theme-toolbar-background); } /** * Bypassed Nodes */ .theme-light .nodes g.bypassed rect { fill: url(chrome://devtools/skin/images/filters.svg#bypass-light); } + .theme-dark .nodes g.bypassed rect { fill: url(chrome://devtools/skin/images/filters.svg#bypass-dark); } + .nodes g.bypassed.selected rect { stroke: var(--theme-selection-background); } -/* .nodes g.bypassed text { - opacity: 0.8; + opacity: 0.6; } -*/ /** * Selected Nodes */ .nodes g.selected rect { fill: var(--theme-selection-background); } -/* Don't style bypassed nodes text different because it'd be illegible in light-theme */ -.theme-light g.selected:not(.bypassed) text { - fill: var(--theme-toolbar-background); +/* Don't style bypassed nodes text differently because it'd be illegible in light-theme */ +g.selected:not(.bypassed) text { + fill: var(--theme-selection-color); } /* Text in nodes and edges */ text { cursor: default; /* override the "text" cursor */ - font-weight: 300; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; - font-size: 14px; + fill: var(--text-color); + font-size: 1.25em; + /* Make sure text stays inside its container in RTL locales */ + direction: ltr; } -text { - fill: var(--theme-body-color-alt); -} - - .nodes text { cursor: pointer; } /** * Inspector Styles */
--- a/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js +++ b/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js @@ -67,44 +67,50 @@ NewConsoleOutputWrapper.prototype = { React.DOM.div( {className: "webconsole-output-wrapper"}, filterBar, childComponent )); this.body = ReactDOM.render(provider, this.parentNode); }, - dispatchMessageAdd: function(message, waitForResponse) { - let action = actions.messageAdd(message); - let messageId = action.message.get("id"); - batchedMessageAdd(action); + + dispatchMessageAdd: function (message, waitForResponse) { + let action = actions.messageAdd(message); + batchedMessageAdd(action); - // Wait for the message to render to resolve with the DOM node. - // This is just for backwards compatibility with old tests, and should - // be removed once it's not needed anymore. - if (waitForResponse) { - return new Promise(resolve => { - let jsterm = this.jsterm; - jsterm.hud.on("new-messages", function onThisMessage(e, messages) { - for (let m of messages) { - if (m.messageId == messageId) { - resolve(m.node); - jsterm.hud.off("new-messages", onThisMessage); - return; - } + // Wait for the message to render to resolve with the DOM node. + // This is just for backwards compatibility with old tests, and should + // be removed once it's not needed anymore. + // Can only wait for response if the action contains a valid message. + if (waitForResponse && action.message) { + let messageId = action.message.get("id"); + return new Promise(resolve => { + let jsterm = this.jsterm; + jsterm.hud.on("new-messages", function onThisMessage(e, messages) { + for (let m of messages) { + if (m.messageId == messageId) { + resolve(m.node); + jsterm.hud.off("new-messages", onThisMessage); + return; } - }); + } }); - } + }); + } + + return Promise.resolve(); }, - dispatchMessagesAdd: function(messages) { + + dispatchMessagesAdd: function (messages) { const batchedActions = messages.map(message => actions.messageAdd(message)); store.dispatch(actions.batchActions(batchedActions)); }, - dispatchMessagesClear: function() { + + dispatchMessagesClear: function () { store.dispatch(actions.messagesClear()); }, }; function batchedMessageAdd(action) { queuedActions.push(action); if (!throttledDispatchTimeout) { throttledDispatchTimeout = setTimeout(() => {
--- a/devtools/shared/css/generated/generate-properties-db.js +++ b/devtools/shared/css/generated/generate-properties-db.js @@ -6,25 +6,27 @@ /* * This is an xpcshell script that runs to generate a static list of CSS properties * as known by the platform. It is run from ./mach_commands.py by running * `mach devtools-css-db`. */ var {require} = Components.utils.import("resource://devtools/shared/Loader.jsm", {}); var {generateCssProperties} = require("devtools/server/actors/css-properties"); +// xpcshell can output extra information, so place some delimiter text between +// the output of the css properties database. +dump("DEVTOOLS_CSS_DB_DELIMITER"); + // Output JSON dump(JSON.stringify({ cssProperties: cssProperties(), pseudoElements: pseudoElements() })); -// In a debug build, xpcshell might print extra debugging information, -// so we emit a trailing newline and then arrange to just read a -// single (long) line of JSON from the output. -dump("\n"); + +dump("DEVTOOLS_CSS_DB_DELIMITER"); /* * A list of CSS Properties and their various characteristics. This is used on the * client-side when the CssPropertiesActor is not found, or when the client and server * are the same version. A single property takes the form: * * "animation": { * "isInherited": false,
--- a/devtools/shared/css/generated/mach_commands.py +++ b/devtools/shared/css/generated/mach_commands.py @@ -1,26 +1,23 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """ -This script implements the `mach devtools-css-db` command. It runs the C preprocessor -on the CSS properties header file to get the list of preferences associated with -a specific property, and it runs an xpcshell script that uses inIDOMUtils to query -the CSS properties used by the browser. This information is used to generate the -properties-db.js file. +This script implements the `mach devtools-css-db` command. It runs an xpcshell script +that uses inIDOMUtils to query the CSS properties used by the browser. This information +is used to generate the properties-db.js file. """ import json import os import sys import string import subprocess -from mozbuild import shellutil from mozbuild.base import ( MozbuildObject, MachCommandBase, ) from mach.decorators import ( CommandProvider, Command, ) @@ -37,56 +34,22 @@ def stringify(obj): class MachCommands(MachCommandBase): @Command( 'devtools-css-db', category='post-build', description='Rebuild the devtool\'s static css properties database.') def generate_css_db(self): """Generate the static css properties database for devtools and write it to file.""" print("Re-generating the css properties database...") - preferences = self.get_preferences() db = self.get_properties_db_from_xpcshell() self.output_template({ - 'preferences': stringify(preferences), 'cssProperties': stringify(db['cssProperties']), 'pseudoElements': stringify(db['pseudoElements'])}) - def get_preferences(self): - """Get all of the preferences associated with enabling and disabling a property.""" - # Build the command to run the preprocessor on PythonCSSProps.h - headerPath = resolve_path(self.topsrcdir, 'layout/style/PythonCSSProps.h') - - cpp = self.substs['CPP'] - - if not cpp: - print("Unable to find the cpp program. Please do a full, non-artifact") - print("build and try this again.") - sys.exit(1) - - if type(cpp) is list: - cmd = cpp - else: - cmd = shellutil.split(cpp) - cmd += shellutil.split(self.substs['ACDEFINES']) - cmd.append(headerPath) - - # The preprocessed list takes the following form: - # [ (name, prop, id, flags, pref, proptype), ... ] - preprocessed = eval(subprocess.check_output(cmd)) - - # Map this list - # (name, prop, id, flags, pref, proptype) => (name, pref) - preferences = [ - (name, pref) - for name, prop, id, flags, pref, proptype in preprocessed - if 'CSS_PROPERTY_INTERNAL' not in flags] - - return preferences - def get_properties_db_from_xpcshell(self): """Generate the static css properties db for devtools from an xpcshell script.""" build = MozbuildObject.from_environment() # Get the paths script_path = resolve_path(self.topsrcdir, 'devtools/shared/css/generated/generate-properties-db.js') gre_path = resolve_path(self.topobjdir, 'dist/bin') @@ -98,19 +61,19 @@ class MachCommands(MachCommandBase): if sys.platform.startswith('linux'): sub_env["LD_LIBRARY_PATH"] = gre_path # Run the xcpshell script, and set the appdir flag to the browser path so that # we have the proper dependencies for requiring the loader. contents = subprocess.check_output([xpcshell_path, '-g', gre_path, '-a', browser_path, script_path], env = sub_env) - # Extract just the first line of output, since a debug-build - # xpcshell might emit extra output that we don't want. - contents = contents.split('\n')[0] + # Extract just the output between the delimiters as the xpcshell output can + # have extra output that we don't want. + contents = contents.split('DEVTOOLS_CSS_DB_DELIMITER')[1] return json.loads(contents) def output_template(self, substitutions): """Output a the properties-db.js from a template.""" js_template_path = resolve_path(self.topsrcdir, 'devtools/shared/css/generated/properties-db.js.in') destination_path = resolve_path(self.topsrcdir,
--- a/devtools/shared/css/generated/properties-db.js +++ b/devtools/shared/css/generated/properties-db.js @@ -9297,1839 +9297,8 @@ exports.PSEUDO_ELEMENTS = [ ":-moz-range-track", ":-moz-range-progress", ":-moz-range-thumb", ":-moz-meter-bar", ":-moz-placeholder", ":placeholder", ":-moz-color-swatch" ]; - -/** - * A list of the preferences keys for whether a CSS property is enabled or not. This is - * exposed for testing purposes. - */ -exports.PREFERENCES = [ - [ - "align-content", - "" - ], - [ - "align-items", - "" - ], - [ - "align-self", - "" - ], - [ - "all", - "layout.css.all-shorthand.enabled" - ], - [ - "animation", - "" - ], - [ - "animation-delay", - "" - ], - [ - "animation-direction", - "" - ], - [ - "animation-duration", - "" - ], - [ - "animation-fill-mode", - "" - ], - [ - "animation-iteration-count", - "" - ], - [ - "animation-name", - "" - ], - [ - "animation-play-state", - "" - ], - [ - "animation-timing-function", - "" - ], - [ - "-moz-appearance", - "" - ], - [ - "backface-visibility", - "" - ], - [ - "background", - "" - ], - [ - "background-attachment", - "" - ], - [ - "background-blend-mode", - "layout.css.background-blend-mode.enabled" - ], - [ - "background-clip", - "" - ], - [ - "background-color", - "" - ], - [ - "background-image", - "" - ], - [ - "background-origin", - "" - ], - [ - "background-position", - "" - ], - [ - "background-position-x", - "" - ], - [ - "background-position-y", - "" - ], - [ - "background-repeat", - "" - ], - [ - "background-size", - "" - ], - [ - "-moz-binding", - "" - ], - [ - "block-size", - "" - ], - [ - "border", - "" - ], - [ - "border-block-end", - "" - ], - [ - "border-block-end-color", - "" - ], - [ - "border-block-end-style", - "" - ], - [ - "border-block-end-width", - "" - ], - [ - "border-block-start", - "" - ], - [ - "border-block-start-color", - "" - ], - [ - "border-block-start-style", - "" - ], - [ - "border-block-start-width", - "" - ], - [ - "border-bottom", - "" - ], - [ - "border-bottom-color", - "" - ], - [ - "-moz-border-bottom-colors", - "" - ], - [ - "border-bottom-left-radius", - "" - ], - [ - "border-bottom-right-radius", - "" - ], - [ - "border-bottom-style", - "" - ], - [ - "border-bottom-width", - "" - ], - [ - "border-collapse", - "" - ], - [ - "border-color", - "" - ], - [ - "border-image", - "" - ], - [ - "border-image-outset", - "" - ], - [ - "border-image-repeat", - "" - ], - [ - "border-image-slice", - "" - ], - [ - "border-image-source", - "" - ], - [ - "border-image-width", - "" - ], - [ - "border-inline-end", - "" - ], - [ - "border-inline-end-color", - "" - ], - [ - "border-inline-end-style", - "" - ], - [ - "border-inline-end-width", - "" - ], - [ - "border-inline-start", - "" - ], - [ - "border-inline-start-color", - "" - ], - [ - "border-inline-start-style", - "" - ], - [ - "border-inline-start-width", - "" - ], - [ - "border-left", - "" - ], - [ - "border-left-color", - "" - ], - [ - "-moz-border-left-colors", - "" - ], - [ - "border-left-style", - "" - ], - [ - "border-left-width", - "" - ], - [ - "border-radius", - "" - ], - [ - "border-right", - "" - ], - [ - "border-right-color", - "" - ], - [ - "-moz-border-right-colors", - "" - ], - [ - "border-right-style", - "" - ], - [ - "border-right-width", - "" - ], - [ - "border-spacing", - "" - ], - [ - "border-style", - "" - ], - [ - "border-top", - "" - ], - [ - "border-top-color", - "" - ], - [ - "-moz-border-top-colors", - "" - ], - [ - "border-top-left-radius", - "" - ], - [ - "border-top-right-radius", - "" - ], - [ - "border-top-style", - "" - ], - [ - "border-top-width", - "" - ], - [ - "border-width", - "" - ], - [ - "bottom", - "" - ], - [ - "-moz-box-align", - "" - ], - [ - "box-decoration-break", - "layout.css.box-decoration-break.enabled" - ], - [ - "-moz-box-direction", - "" - ], - [ - "-moz-box-flex", - "" - ], - [ - "-moz-box-ordinal-group", - "" - ], - [ - "-moz-box-orient", - "" - ], - [ - "-moz-box-pack", - "" - ], - [ - "box-shadow", - "" - ], - [ - "box-sizing", - "" - ], - [ - "caption-side", - "" - ], - [ - "clear", - "" - ], - [ - "clip", - "" - ], - [ - "clip-path", - "" - ], - [ - "clip-rule", - "" - ], - [ - "color", - "" - ], - [ - "color-adjust", - "layout.css.color-adjust.enabled" - ], - [ - "color-interpolation", - "" - ], - [ - "color-interpolation-filters", - "" - ], - [ - "column-count", - "" - ], - [ - "column-fill", - "" - ], - [ - "column-gap", - "" - ], - [ - "column-rule", - "" - ], - [ - "column-rule-color", - "" - ], - [ - "column-rule-style", - "" - ], - [ - "column-rule-width", - "" - ], - [ - "column-width", - "" - ], - [ - "columns", - "" - ], - [ - "contain", - "layout.css.contain.enabled" - ], - [ - "content", - "" - ], - [ - "counter-increment", - "" - ], - [ - "counter-reset", - "" - ], - [ - "cursor", - "" - ], - [ - "direction", - "" - ], - [ - "display", - "" - ], - [ - "dominant-baseline", - "" - ], - [ - "empty-cells", - "" - ], - [ - "fill", - "" - ], - [ - "fill-opacity", - "" - ], - [ - "fill-rule", - "" - ], - [ - "filter", - "" - ], - [ - "flex", - "" - ], - [ - "flex-basis", - "" - ], - [ - "flex-direction", - "" - ], - [ - "flex-flow", - "" - ], - [ - "flex-grow", - "" - ], - [ - "flex-shrink", - "" - ], - [ - "flex-wrap", - "" - ], - [ - "float", - "" - ], - [ - "-moz-float-edge", - "" - ], - [ - "flood-color", - "" - ], - [ - "flood-opacity", - "" - ], - [ - "font", - "" - ], - [ - "font-family", - "" - ], - [ - "font-feature-settings", - "" - ], - [ - "font-kerning", - "" - ], - [ - "font-language-override", - "" - ], - [ - "font-size", - "" - ], - [ - "font-size-adjust", - "" - ], - [ - "font-stretch", - "" - ], - [ - "font-style", - "" - ], - [ - "font-synthesis", - "" - ], - [ - "font-variant", - "" - ], - [ - "font-variant-alternates", - "" - ], - [ - "font-variant-caps", - "" - ], - [ - "font-variant-east-asian", - "" - ], - [ - "font-variant-ligatures", - "" - ], - [ - "font-variant-numeric", - "" - ], - [ - "font-variant-position", - "" - ], - [ - "font-weight", - "" - ], - [ - "-moz-force-broken-image-icon", - "" - ], - [ - "grid", - "layout.css.grid.enabled" - ], - [ - "grid-area", - "layout.css.grid.enabled" - ], - [ - "grid-auto-columns", - "layout.css.grid.enabled" - ], - [ - "grid-auto-flow", - "layout.css.grid.enabled" - ], - [ - "grid-auto-rows", - "layout.css.grid.enabled" - ], - [ - "grid-column", - "layout.css.grid.enabled" - ], - [ - "grid-column-end", - "layout.css.grid.enabled" - ], - [ - "grid-column-gap", - "layout.css.grid.enabled" - ], - [ - "grid-column-start", - "layout.css.grid.enabled" - ], - [ - "grid-gap", - "layout.css.grid.enabled" - ], - [ - "grid-row", - "layout.css.grid.enabled" - ], - [ - "grid-row-end", - "layout.css.grid.enabled" - ], - [ - "grid-row-gap", - "layout.css.grid.enabled" - ], - [ - "grid-row-start", - "layout.css.grid.enabled" - ], - [ - "grid-template", - "layout.css.grid.enabled" - ], - [ - "grid-template-areas", - "layout.css.grid.enabled" - ], - [ - "grid-template-columns", - "layout.css.grid.enabled" - ], - [ - "grid-template-rows", - "layout.css.grid.enabled" - ], - [ - "height", - "" - ], - [ - "hyphens", - "" - ], - [ - "initial-letter", - "layout.css.initial-letter.enabled" - ], - [ - "image-orientation", - "layout.css.image-orientation.enabled" - ], - [ - "-moz-image-region", - "" - ], - [ - "image-rendering", - "" - ], - [ - "ime-mode", - "" - ], - [ - "inline-size", - "" - ], - [ - "isolation", - "layout.css.isolation.enabled" - ], - [ - "justify-content", - "" - ], - [ - "justify-items", - "" - ], - [ - "justify-self", - "" - ], - [ - "left", - "" - ], - [ - "letter-spacing", - "" - ], - [ - "lighting-color", - "" - ], - [ - "line-height", - "" - ], - [ - "list-style", - "" - ], - [ - "list-style-image", - "" - ], - [ - "list-style-position", - "" - ], - [ - "list-style-type", - "" - ], - [ - "margin", - "" - ], - [ - "margin-block-end", - "" - ], - [ - "margin-block-start", - "" - ], - [ - "margin-bottom", - "" - ], - [ - "margin-inline-end", - "" - ], - [ - "margin-inline-start", - "" - ], - [ - "margin-left", - "" - ], - [ - "margin-right", - "" - ], - [ - "margin-top", - "" - ], - [ - "marker", - "" - ], - [ - "marker-end", - "" - ], - [ - "marker-mid", - "" - ], - [ - "marker-offset", - "" - ], - [ - "marker-start", - "" - ], - [ - "mask", - "" - ], - [ - "mask-clip", - "" - ], - [ - "mask-composite", - "" - ], - [ - "mask-image", - "" - ], - [ - "mask-mode", - "" - ], - [ - "mask-origin", - "" - ], - [ - "mask-position", - "" - ], - [ - "mask-position-x", - "" - ], - [ - "mask-position-y", - "" - ], - [ - "mask-repeat", - "" - ], - [ - "mask-size", - "" - ], - [ - "mask-type", - "" - ], - [ - "max-block-size", - "" - ], - [ - "max-height", - "" - ], - [ - "max-inline-size", - "" - ], - [ - "max-width", - "" - ], - [ - "min-block-size", - "" - ], - [ - "min-height", - "" - ], - [ - "min-inline-size", - "" - ], - [ - "min-width", - "" - ], - [ - "mix-blend-mode", - "layout.css.mix-blend-mode.enabled" - ], - [ - "object-fit", - "layout.css.object-fit-and-position.enabled" - ], - [ - "object-position", - "layout.css.object-fit-and-position.enabled" - ], - [ - "offset-block-end", - "" - ], - [ - "offset-block-start", - "" - ], - [ - "offset-inline-end", - "" - ], - [ - "offset-inline-start", - "" - ], - [ - "opacity", - "" - ], - [ - "order", - "" - ], - [ - "-moz-orient", - "" - ], - [ - "-moz-osx-font-smoothing", - "layout.css.osx-font-smoothing.enabled" - ], - [ - "outline", - "" - ], - [ - "outline-color", - "" - ], - [ - "outline-offset", - "" - ], - [ - "-moz-outline-radius", - "" - ], - [ - "-moz-outline-radius-bottomleft", - "" - ], - [ - "-moz-outline-radius-bottomright", - "" - ], - [ - "-moz-outline-radius-topleft", - "" - ], - [ - "-moz-outline-radius-topright", - "" - ], - [ - "outline-style", - "" - ], - [ - "outline-width", - "" - ], - [ - "overflow", - "" - ], - [ - "overflow-clip-box", - "layout.css.overflow-clip-box.enabled" - ], - [ - "overflow-x", - "" - ], - [ - "overflow-y", - "" - ], - [ - "padding", - "" - ], - [ - "padding-block-end", - "" - ], - [ - "padding-block-start", - "" - ], - [ - "padding-bottom", - "" - ], - [ - "padding-inline-end", - "" - ], - [ - "padding-inline-start", - "" - ], - [ - "padding-left", - "" - ], - [ - "padding-right", - "" - ], - [ - "padding-top", - "" - ], - [ - "page-break-after", - "" - ], - [ - "page-break-before", - "" - ], - [ - "page-break-inside", - "" - ], - [ - "paint-order", - "svg.paint-order.enabled" - ], - [ - "perspective", - "" - ], - [ - "perspective-origin", - "" - ], - [ - "pointer-events", - "" - ], - [ - "position", - "" - ], - [ - "quotes", - "" - ], - [ - "resize", - "" - ], - [ - "right", - "" - ], - [ - "ruby-align", - "" - ], - [ - "ruby-position", - "" - ], - [ - "scroll-behavior", - "layout.css.scroll-behavior.property-enabled" - ], - [ - "scroll-snap-coordinate", - "layout.css.scroll-snap.enabled" - ], - [ - "scroll-snap-destination", - "layout.css.scroll-snap.enabled" - ], - [ - "scroll-snap-points-x", - "layout.css.scroll-snap.enabled" - ], - [ - "scroll-snap-points-y", - "layout.css.scroll-snap.enabled" - ], - [ - "scroll-snap-type", - "layout.css.scroll-snap.enabled" - ], - [ - "scroll-snap-type-x", - "layout.css.scroll-snap.enabled" - ], - [ - "scroll-snap-type-y", - "layout.css.scroll-snap.enabled" - ], - [ - "shape-outside", - "layout.css.shape-outside.enabled" - ], - [ - "shape-rendering", - "" - ], - [ - "-moz-stack-sizing", - "" - ], - [ - "stop-color", - "" - ], - [ - "stop-opacity", - "" - ], - [ - "stroke", - "" - ], - [ - "stroke-dasharray", - "" - ], - [ - "stroke-dashoffset", - "" - ], - [ - "stroke-linecap", - "" - ], - [ - "stroke-linejoin", - "" - ], - [ - "stroke-miterlimit", - "" - ], - [ - "stroke-opacity", - "" - ], - [ - "stroke-width", - "" - ], - [ - "-moz-tab-size", - "" - ], - [ - "table-layout", - "" - ], - [ - "text-align", - "" - ], - [ - "text-align-last", - "" - ], - [ - "text-anchor", - "" - ], - [ - "text-combine-upright", - "layout.css.text-combine-upright.enabled" - ], - [ - "text-decoration", - "" - ], - [ - "text-decoration-color", - "" - ], - [ - "text-decoration-line", - "" - ], - [ - "text-decoration-style", - "" - ], - [ - "text-emphasis", - "" - ], - [ - "text-emphasis-color", - "" - ], - [ - "text-emphasis-position", - "" - ], - [ - "text-emphasis-style", - "" - ], - [ - "-webkit-text-fill-color", - "layout.css.prefixes.webkit" - ], - [ - "text-indent", - "" - ], - [ - "text-orientation", - "" - ], - [ - "text-overflow", - "" - ], - [ - "text-rendering", - "" - ], - [ - "text-shadow", - "" - ], - [ - "-moz-text-size-adjust", - "" - ], - [ - "-webkit-text-stroke", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-text-stroke-color", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-text-stroke-width", - "layout.css.prefixes.webkit" - ], - [ - "text-transform", - "" - ], - [ - "top", - "" - ], - [ - "touch-action", - "layout.css.touch_action.enabled" - ], - [ - "transform", - "" - ], - [ - "-moz-transform", - "layout.css.prefixes.transforms" - ], - [ - "transform-box", - "svg.transform-box.enabled" - ], - [ - "transform-origin", - "" - ], - [ - "transform-style", - "" - ], - [ - "transition", - "" - ], - [ - "transition-delay", - "" - ], - [ - "transition-duration", - "" - ], - [ - "transition-property", - "" - ], - [ - "transition-timing-function", - "" - ], - [ - "unicode-bidi", - "" - ], - [ - "-moz-user-focus", - "" - ], - [ - "-moz-user-input", - "" - ], - [ - "-moz-user-modify", - "" - ], - [ - "-moz-user-select", - "" - ], - [ - "vector-effect", - "" - ], - [ - "vertical-align", - "" - ], - [ - "visibility", - "" - ], - [ - "white-space", - "" - ], - [ - "width", - "" - ], - [ - "will-change", - "" - ], - [ - "-moz-window-dragging", - "" - ], - [ - "word-break", - "" - ], - [ - "word-spacing", - "" - ], - [ - "overflow-wrap", - "" - ], - [ - "writing-mode", - "" - ], - [ - "z-index", - "" - ], - [ - "word-wrap", - "" - ], - [ - "-moz-transform-origin", - "layout.css.prefixes.transforms" - ], - [ - "-moz-perspective-origin", - "layout.css.prefixes.transforms" - ], - [ - "-moz-perspective", - "layout.css.prefixes.transforms" - ], - [ - "-moz-transform-style", - "layout.css.prefixes.transforms" - ], - [ - "-moz-backface-visibility", - "layout.css.prefixes.transforms" - ], - [ - "-moz-border-image", - "layout.css.prefixes.border-image" - ], - [ - "-moz-transition", - "layout.css.prefixes.transitions" - ], - [ - "-moz-transition-delay", - "layout.css.prefixes.transitions" - ], - [ - "-moz-transition-duration", - "layout.css.prefixes.transitions" - ], - [ - "-moz-transition-property", - "layout.css.prefixes.transitions" - ], - [ - "-moz-transition-timing-function", - "layout.css.prefixes.transitions" - ], - [ - "-moz-animation", - "layout.css.prefixes.animations" - ], - [ - "-moz-animation-delay", - "layout.css.prefixes.animations" - ], - [ - "-moz-animation-direction", - "layout.css.prefixes.animations" - ], - [ - "-moz-animation-duration", - "layout.css.prefixes.animations" - ], - [ - "-moz-animation-fill-mode", - "layout.css.prefixes.animations" - ], - [ - "-moz-animation-iteration-count", - "layout.css.prefixes.animations" - ], - [ - "-moz-animation-name", - "layout.css.prefixes.animations" - ], - [ - "-moz-animation-play-state", - "layout.css.prefixes.animations" - ], - [ - "-moz-animation-timing-function", - "layout.css.prefixes.animations" - ], - [ - "-moz-box-sizing", - "layout.css.prefixes.box-sizing" - ], - [ - "-moz-font-feature-settings", - "layout.css.prefixes.font-features" - ], - [ - "-moz-font-language-override", - "layout.css.prefixes.font-features" - ], - [ - "-moz-padding-end", - "" - ], - [ - "-moz-padding-start", - "" - ], - [ - "-moz-margin-end", - "" - ], - [ - "-moz-margin-start", - "" - ], - [ - "-moz-border-end", - "" - ], - [ - "-moz-border-end-color", - "" - ], - [ - "-moz-border-end-style", - "" - ], - [ - "-moz-border-end-width", - "" - ], - [ - "-moz-border-start", - "" - ], - [ - "-moz-border-start-color", - "" - ], - [ - "-moz-border-start-style", - "" - ], - [ - "-moz-border-start-width", - "" - ], - [ - "-moz-hyphens", - "" - ], - [ - "-moz-text-align-last", - "" - ], - [ - "-moz-column-count", - "" - ], - [ - "-moz-column-fill", - "" - ], - [ - "-moz-column-gap", - "" - ], - [ - "-moz-column-rule", - "" - ], - [ - "-moz-column-rule-color", - "" - ], - [ - "-moz-column-rule-style", - "" - ], - [ - "-moz-column-rule-width", - "" - ], - [ - "-moz-column-width", - "" - ], - [ - "-moz-columns", - "" - ], - [ - "-webkit-animation", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-animation-delay", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-animation-direction", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-animation-duration", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-animation-fill-mode", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-animation-iteration-count", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-animation-name", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-animation-play-state", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-animation-timing-function", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-filter", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-text-size-adjust", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-transform", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-transform-origin", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-transform-style", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-backface-visibility", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-perspective", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-perspective-origin", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-transition", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-transition-delay", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-transition-duration", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-transition-property", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-transition-timing-function", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-border-radius", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-border-top-left-radius", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-border-top-right-radius", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-border-bottom-left-radius", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-border-bottom-right-radius", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-background-clip", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-background-origin", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-background-size", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-border-image", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-box-shadow", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-box-sizing", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-box-flex", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-box-ordinal-group", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-box-orient", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-box-direction", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-box-align", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-box-pack", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-flex-direction", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-flex-wrap", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-flex-flow", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-order", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-flex", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-flex-grow", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-flex-shrink", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-flex-basis", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-justify-content", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-align-items", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-align-self", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-align-content", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-user-select", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-mask", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-mask-clip", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-mask-composite", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-mask-image", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-mask-origin", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-mask-position", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-mask-position-x", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-mask-position-y", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-mask-repeat", - "layout.css.prefixes.webkit" - ], - [ - "-webkit-mask-size", - "layout.css.prefixes.webkit" - ] -];
--- a/devtools/shared/css/generated/properties-db.js.in +++ b/devtools/shared/css/generated/properties-db.js.in @@ -13,14 +13,8 @@ * A list of CSS Properties and their various characteristics. */ exports.CSS_PROPERTIES = ${cssProperties}; /** * A list of the pseudo elements. */ exports.PSEUDO_ELEMENTS = ${pseudoElements}; - -/** - * A list of the preferences keys for whether a CSS property is enabled or not. This is - * exposed for testing purposes. - */ -exports.PREFERENCES = ${preferences};
--- a/devtools/shared/css/properties-db.js +++ b/devtools/shared/css/properties-db.js @@ -5,17 +5,30 @@ "use strict"; /** * This file contains static lists of CSS properties and values. Some of the small lists * are edited manually, while the larger ones are generated by a script. The comments * above each list indicates how it should be updated. */ -const { CSS_PROPERTIES, PSEUDO_ELEMENTS } = require("devtools/shared/css/generated/properties-db"); +let db; + +// Allow this require to fail in case it's been deleted in the process of running +// `mach devtools-css-db` to regenerate the database. +try { + db = require("devtools/shared/css/generated/properties-db"); +} catch (error) { + console.error(`If this error is being displayed and "mach devtools-css-db" is not ` + + `being run, then it needs to be fixed.`, error); + db = { + CSS_PROPERTIES: {}, + PSEUDO_ELEMENTS: [] + }; +} /** * All CSS types that properties can support. This list can be manually edited. */ exports.CSS_TYPES = { "ANGLE": 1, "COLOR": 2, "FREQUENCY": 3, @@ -52,26 +65,26 @@ exports.ANGLE_TAKING_FUNCTIONS = ["linea "rotateY", "rotateZ", "rotate3d", "skew", "skewX", "skewY", "hue-rotate"]; /** * The list of all CSS Pseudo Elements. * * This list can be updated with `mach devtools-css-db`. */ -exports.PSEUDO_ELEMENTS = PSEUDO_ELEMENTS; +exports.PSEUDO_ELEMENTS = db.PSEUDO_ELEMENTS; /** * A list of CSS Properties and their various characteristics. This is used on the * client-side when the CssPropertiesActor is not found, or when the client and server * are the same version. A single property takes the form: * * "animation": { * "isInherited": false, * "supports": [ 7, 9, 10 ] * } */ -exports.CSS_PROPERTIES = CSS_PROPERTIES; +exports.CSS_PROPERTIES = db.CSS_PROPERTIES; exports.CSS_PROPERTIES_DB = { - properties: CSS_PROPERTIES, - pseudoElements: PSEUDO_ELEMENTS + properties: db.CSS_PROPERTIES, + pseudoElements: db.PSEUDO_ELEMENTS };
--- a/devtools/shared/tests/unit/test_css-properties-db.js +++ b/devtools/shared/tests/unit/test_css-properties-db.js @@ -1,45 +1,49 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ /** * Test that the devtool's client-side CSS properties database is in sync with the values - * on the platform. If they are not, then `mach devtools-css-db` needs to be run to - * make everything up to date. Nightly, aurora, beta, and release may have different - * preferences for what CSS values are enabled. The static CSS properties database can - * be slightly different from the target platform as long as there is a preference that - * exists that turns off that CSS property. + * on the platform (in Nightly only). If they are not, then `mach devtools-css-db` needs + * to be run to make everything up to date. Nightly, aurora, beta, and release may have + * different CSS properties and values. These are based on preferences and compiler flags. + * + * This test broke uplifts as the database needed to be regenerated every uplift. The + * combination of compiler flags and preferences means that it's too difficult to + * statically determine which properties are enabled between Firefox releases. + * + * Because of these difficulties, the database only needs to be up to date with Nightly. + * It is a fallback that is only used if the remote debugging protocol doesn't support + * providing a CSS database, so it's ok if the provided properties don't exactly match + * the inspected target in this particular case. */ "use strict"; const DOMUtils = Components.classes["@mozilla.org/inspector/dom-utils;1"] .getService(Components.interfaces.inIDOMUtils); -const {PSEUDO_ELEMENTS, CSS_PROPERTIES, PREFERENCES} = require("devtools/shared/css/generated/properties-db"); +const {PSEUDO_ELEMENTS, CSS_PROPERTIES} = require("devtools/shared/css/generated/properties-db"); const {generateCssProperties} = require("devtools/server/actors/css-properties"); -const { Preferences } = require("resource://gre/modules/Preferences.jsm"); function run_test() { const propertiesErrorMessage = "If this assertion fails, then the client side CSS " + "properties list in devtools is out of sync with the " + "CSS properties on the platform. To fix this " + "assertion run `mach devtools-css-db` to re-generate " + "the client side properties."; // Check that the platform and client match for pseudo elements. deepEqual(PSEUDO_ELEMENTS, DOMUtils.getCSSPseudoElementNames(), `The pseudo elements ` + `match on the client and platform. ${propertiesErrorMessage}`); /** * Check that the platform and client match for the details on their CSS properties. - * Enumerate each property to aid in debugging. Sometimes these properties don't - * completely agree due to differences in preferences. Check the currently set - * preference for that property to see if it's enabled. + * Enumerate each property to aid in debugging. */ const platformProperties = generateCssProperties(); for (let propertyName in CSS_PROPERTIES) { const platformProperty = platformProperties[propertyName]; const clientProperty = CSS_PROPERTIES[propertyName]; const deepEqual = isJsonDeepEqual(platformProperty, clientProperty); @@ -66,24 +70,18 @@ function run_test() { // Filter out OS-specific properties. .filter(name => name && name.indexOf("-moz-osx-") === -1); if (mismatches.length === 0) { ok(true, "No client and platform CSS property database mismatches were found."); } mismatches.forEach(propertyName => { - if (getPreference(propertyName) === false) { - ok(true, `The static database and platform do not agree on the property ` + - `"${propertyName}" This is ok because it is currently disabled through ` + - `a preference.`); - } else { - ok(false, `The static database and platform do not agree on the property ` + - `"${propertyName}" ${propertiesErrorMessage}`); - } + ok(false, `The static database and platform do not agree on the property ` + + `"${propertyName}" ${propertiesErrorMessage}`); }); } /** * Check JSON-serializable objects for deep equality. */ function isJsonDeepEqual(a, b) { // Handle primitives. @@ -115,34 +113,16 @@ function isJsonDeepEqual(a, b) { return Object.keys(a).length === Object.keys(b).length; } // Not something handled by these cases, therefore not equal. return false; } /** - * Get the preference value of whether this property is enabled. Returns an empty string - * if no preference exists. - * - * @param {String} propertyName - * @return {Boolean|undefined} - */ -function getPreference(propertyName) { - const preference = PREFERENCES.find(([prefPropertyName, preferenceKey]) => { - return prefPropertyName === propertyName && !!preferenceKey; - }); - - if (preference) { - return Preferences.get(preference[1]); - } - return undefined; -} - -/** * Take the keys of two objects, and return the ones that don't match. * * @param {Object} a * @param {Object} b * @return {Array} keys */ function getKeyMismatches(a, b) { const aNames = Object.keys(a);
--- a/dom/browser-element/BrowserElementChild.js +++ b/dom/browser-element/BrowserElementChild.js @@ -54,37 +54,24 @@ if (!BrowserElementIsReady) { Services.scriptloader.loadSubScript("chrome://global/content/BrowserElementCopyPaste.js"); } } else { // rocketbar in system app and other in-process case (ex. B2G desktop client) Services.scriptloader.loadSubScript("chrome://global/content/BrowserElementCopyPaste.js"); } Services.scriptloader.loadSubScript("chrome://global/content/BrowserElementChildPreload.js"); - } else { - if (Services.prefs.getIntPref("dom.w3c_touch_events.enabled") != 0) { - if (docShell.asyncPanZoomEnabled === false) { - ContentPanningAPZDisabled.init(); - } - ContentPanning.init(); - } } function onDestroy() { removeMessageListener("browser-element-api:destroy", onDestroy); if (api) { api.destroy(); } - if ("ContentPanning" in this) { - ContentPanning.destroy(); - } - if ("ContentPanningAPZDisabled" in this) { - ContentPanningAPZDisabled.destroy(); - } if ("CopyPasteAssistent" in this) { CopyPasteAssistent.destroy(); } BrowserElementIsReady = false; } addMessageListener("browser-element-api:destroy", onDestroy);
--- a/dom/media/MediaDecoderStateMachine.cpp +++ b/dom/media/MediaDecoderStateMachine.cpp @@ -67,28 +67,30 @@ using namespace mozilla::media; #undef DECODER_LOG #undef VERBOSE_LOG #undef SAMPLE_LOG #undef DECODER_WARN #undef DUMP_LOG #undef SFMT #undef SLOG #undef SWARN +#undef SDUMP #define FMT(x, ...) "Decoder=%p " x, mDecoderID, ##__VA_ARGS__ -#define DECODER_LOG(...) MOZ_LOG(gMediaDecoderLog, LogLevel::Debug, (FMT(__VA_ARGS__))) -#define VERBOSE_LOG(...) MOZ_LOG(gMediaDecoderLog, LogLevel::Verbose, (FMT(__VA_ARGS__))) -#define SAMPLE_LOG(...) MOZ_LOG(gMediaSampleLog, LogLevel::Debug, (FMT(__VA_ARGS__))) -#define DECODER_WARN(...) NS_WARNING(nsPrintfCString(FMT(__VA_ARGS__)).get()) -#define DUMP_LOG(...) NS_DebugBreak(NS_DEBUG_WARNING, nsPrintfCString(FMT(__VA_ARGS__)).get(), nullptr, nullptr, -1) +#define DECODER_LOG(x, ...) MOZ_LOG(gMediaDecoderLog, LogLevel::Debug, (FMT(x, ##__VA_ARGS__))) +#define VERBOSE_LOG(x, ...) MOZ_LOG(gMediaDecoderLog, LogLevel::Verbose, (FMT(x, ##__VA_ARGS__))) +#define SAMPLE_LOG(x, ...) MOZ_LOG(gMediaSampleLog, LogLevel::Debug, (FMT(x, ##__VA_ARGS__))) +#define DECODER_WARN(x, ...) NS_WARNING(nsPrintfCString(FMT(x, ##__VA_ARGS__)).get()) +#define DUMP_LOG(x, ...) NS_DebugBreak(NS_DEBUG_WARNING, nsPrintfCString(FMT(x, ##__VA_ARGS__)).get(), nullptr, nullptr, -1) // Used by StateObject and its sub-classes #define SFMT(x, ...) "Decoder=%p state=%s " x, mMaster->mDecoderID, ToStateStr(GetState()), ##__VA_ARGS__ -#define SLOG(...) MOZ_LOG(gMediaDecoderLog, LogLevel::Debug, (SFMT(__VA_ARGS__))) -#define SWARN(...) NS_WARNING(nsPrintfCString(SFMT(__VA_ARGS__)).get()) +#define SLOG(x, ...) MOZ_LOG(gMediaDecoderLog, LogLevel::Debug, (SFMT(x, ##__VA_ARGS__))) +#define SWARN(x, ...) NS_WARNING(nsPrintfCString(SFMT(x, ##__VA_ARGS__)).get()) +#define SDUMP(x, ...) NS_DebugBreak(NS_DEBUG_WARNING, nsPrintfCString(SFMT(x, ##__VA_ARGS__)).get(), nullptr, nullptr, -1) // Certain constants get stored as member variables and then adjusted by various // scale factors on a per-decoder basis. We want to make sure to avoid using these // constants directly, so we put them in a namespace. namespace detail { // If audio queue has less than this many usecs of decoded audio, we won't risk // trying to decode the video, we'll skip decoding video up to the next @@ -216,21 +218,23 @@ public: virtual bool HandleVideoDecoded(MediaData* aVideo, TimeStamp aDecodeStart) { return false; } virtual bool HandleEndOfStream() { return false; } - virtual RefPtr<MediaDecoder::SeekPromise> HandleSeek(SeekTarget aTarget) - { - MOZ_ASSERT(false, "Can't seek in this state"); - return nullptr; - } + virtual bool HandleWaitingForData() { return false; } + + virtual RefPtr<MediaDecoder::SeekPromise> HandleSeek(SeekTarget aTarget) = 0; + + virtual bool HandleAudioCaptured() { return false; } + + virtual void DumpDebugInfo() {} protected: using Master = MediaDecoderStateMachine; explicit StateObject(Master* aPtr) : mMaster(aPtr) {} TaskQueue* OwnerThread() const { return mMaster->mTaskQueue; } MediaResource* Resource() const { return mMaster->mResource; } MediaDecoderReaderWrapper* Reader() const { return mMaster->mReader; } const MediaInfo& Info() const { return mMaster->Info(); } @@ -281,16 +285,22 @@ public: } bool HandleDormant(bool aDormant) override { mPendingDormant = aDormant; return true; } + RefPtr<MediaDecoder::SeekPromise> HandleSeek(SeekTarget aTarget) override + { + MOZ_DIAGNOSTIC_ASSERT(false, "Can't seek while decoding metadata."); + return MediaDecoder::SeekPromise::CreateAndReject(true, __func__); + } + private: void OnMetadataRead(MetadataHolder* aMetadata) { mMetadataRequest.Complete(); // Set mode to PLAYBACK after reading metadata. Resource()->SetReadMode(MediaCacheStream::MODE_PLAYBACK); @@ -556,45 +566,45 @@ public: if (mMaster->CheckIfDecodeComplete()) { SetState(DECODER_STATE_COMPLETED); return; } mDecodeStartTime = TimeStamp::Now(); - mMaster->mIsPrerolling = true; - mMaster->MaybeStopPrerolling(); + MaybeStopPrerolling(); // Ensure that we've got tasks enqueued to decode data if we need to. mMaster->DispatchDecodeTasksIfNeeded(); mMaster->ScheduleStateMachine(); } void Exit() override { if (!mDecodeStartTime.IsNull()) { TimeDuration decodeDuration = TimeStamp::Now() - mDecodeStartTime; SLOG("Exiting DECODING, decoded for %.3lfs", decodeDuration.ToSeconds()); } - mMaster->mIsPrerolling = false; } void Step() override { if (mMaster->mPlayState != MediaDecoder::PLAY_STATE_PLAYING && mMaster->IsPlaying()) { // We're playing, but the element/decoder is in paused state. Stop // playing! mMaster->StopPlayback(); } // Start playback if necessary so that the clock can be properly queried. - mMaster->MaybeStartPlayback(); + if (!mIsPrerolling) { + mMaster->MaybeStartPlayback(); + } mMaster->UpdatePlaybackPositionPeriodically(); MOZ_ASSERT(!mMaster->IsPlaying() || mMaster->IsStateMachineScheduled(), "Must have timer scheduled"); mMaster->MaybeStartBuffering(); @@ -603,39 +613,68 @@ public: State GetState() const override { return DECODER_STATE_DECODING; } bool HandleAudioDecoded(MediaData* aAudio) override { mMaster->Push(aAudio, MediaData::AUDIO_DATA); - mMaster->MaybeStopPrerolling(); + MaybeStopPrerolling(); return true; } bool HandleVideoDecoded(MediaData* aVideo, TimeStamp aDecodeStart) override { mMaster->Push(aVideo, MediaData::VIDEO_DATA); - mMaster->MaybeStopPrerolling(); + MaybeStopPrerolling(); CheckSlowDecoding(aDecodeStart); return true; } RefPtr<MediaDecoder::SeekPromise> HandleSeek(SeekTarget aTarget) override { mMaster->mQueuedSeek.RejectIfExists(__func__); SLOG("Changed state to SEEKING (to %lld)", aTarget.GetTime().ToMicroseconds()); SeekJob seekJob; seekJob.mTarget = aTarget; RefPtr<MediaDecoder::SeekPromise> p = seekJob.mPromise.Ensure(__func__); mMaster->InitiateSeek(Move(seekJob)); return p.forget(); } + bool HandleEndOfStream() override + { + if (mMaster->CheckIfDecodeComplete()) { + SetState(DECODER_STATE_COMPLETED); + } else { + MaybeStopPrerolling(); + } + return true; + } + + bool HandleWaitingForData() override + { + MaybeStopPrerolling(); + return true; + } + + bool HandleAudioCaptured() override + { + MaybeStopPrerolling(); + // MediaSink is changed. Schedule Step() to check if we can start playback. + mMaster->ScheduleStateMachine(); + return true; + } + + void DumpDebugInfo() override + { + SDUMP("mIsPrerolling=%d", mIsPrerolling); + } + private: void CheckSlowDecoding(TimeStamp aDecodeStart) { // For non async readers, if the requested video sample was slow to // arrive, increase the amount of audio we buffer to ensure that we // don't run out of audio. This is unnecessary for async readers, // since they decode audio and video on different threads so they // are unlikely to run out of decoded audio. @@ -658,26 +697,52 @@ private: SLOG("Slow video decode, set " "mLowAudioThresholdUsecs=%lld " "mAmpleAudioThresholdUsecs=%lld", mMaster->mLowAudioThresholdUsecs, mMaster->mAmpleAudioThresholdUsecs); } } - bool HandleEndOfStream() override + bool DonePrerollingAudio() + { + return !mMaster->IsAudioDecoding() || + mMaster->GetDecodedAudioDuration() >= + mMaster->AudioPrerollUsecs() * mMaster->mPlaybackRate; + } + + bool DonePrerollingVideo() { - if (mMaster->CheckIfDecodeComplete()) { - SetState(DECODER_STATE_COMPLETED); + return !mMaster->IsVideoDecoding() || + static_cast<uint32_t>(mMaster->VideoQueue().GetSize()) >= + mMaster->VideoPrerollFrames() * mMaster->mPlaybackRate + 1; + } + + void MaybeStopPrerolling() + { + if (mIsPrerolling && + (DonePrerollingAudio() || Reader()->IsWaitingAudioData()) && + (DonePrerollingVideo() || Reader()->IsWaitingVideoData())) { + mIsPrerolling = false; + // Check if we can start playback. + mMaster->ScheduleStateMachine(); } - return true; } // Time at which we started decoding. TimeStamp mDecodeStartTime; + + // When we start decoding (either for the first time, or after a pause) + // we may be low on decoded data. We don't want our "low data" logic to + // kick in and decide that we're low on decoded data because the download + // can't keep up with the decode, and cause us to pause playback. So we + // have a "preroll" stage, where we ignore the results of our "low data" + // logic during the first few frames of our decode. This occurs during + // playback. + bool mIsPrerolling = true; }; class MediaDecoderStateMachine::SeekingState : public MediaDecoderStateMachine::StateObject { public: explicit SeekingState(Master* aPtr, SeekJob aSeekJob) : StateObject(aPtr), mSeekJob(Move(aSeekJob)) {} @@ -1104,16 +1169,23 @@ public: SLOG("Changed state to SEEKING (to %lld)", aTarget.GetTime().ToMicroseconds()); SeekJob seekJob; seekJob.mTarget = aTarget; RefPtr<MediaDecoder::SeekPromise> p = seekJob.mPromise.Ensure(__func__); mMaster->InitiateSeek(Move(seekJob)); return p.forget(); } + bool HandleAudioCaptured() override + { + // MediaSink is changed. Schedule Step() to check if we can start playback. + mMaster->ScheduleStateMachine(); + return true; + } + private: bool mSentPlaybackEndedEvent = false; }; class MediaDecoderStateMachine::ShutdownState : public MediaDecoderStateMachine::StateObject { public: @@ -1133,16 +1205,22 @@ public: { return DECODER_STATE_SHUTDOWN; } bool HandleDormant(bool aDormant) override { return true; } + + RefPtr<MediaDecoder::SeekPromise> HandleSeek(SeekTarget aTarget) override + { + MOZ_DIAGNOSTIC_ASSERT(false, "Can't seek in shutdown state."); + return MediaDecoder::SeekPromise::CreateAndReject(true, __func__); + } }; #define INIT_WATCHABLE(name, val) \ name(val, "MediaDecoderStateMachine::" #name) #define INIT_MIRROR(name, val) \ name(mTaskQueue, val, "MediaDecoderStateMachine::" #name " (Mirror)") #define INIT_CANONICAL(name, val) \ name(mTaskQueue, val, "MediaDecoderStateMachine::" #name " (Canonical)") @@ -1537,17 +1615,17 @@ MediaDecoderStateMachine::OnNotDecoded(M } // If the decoder is waiting for data, we tell it to call us back when the // data arrives. if (aError == NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA) { MOZ_ASSERT(mReader->IsWaitForDataSupported(), "Readers that send WAITING_FOR_DATA need to implement WaitForData"); mReader->WaitForData(aType); - MaybeStopPrerolling(); + mStateObj->HandleWaitingForData(); return; } if (aError == NS_ERROR_DOM_MEDIA_CANCELED) { if (isAudio) { EnsureAudioDecodeTaskQueued(); } else { EnsureVideoDecodeTaskQueued(); @@ -1564,18 +1642,16 @@ MediaDecoderStateMachine::OnNotDecoded(M // This is an EOS. Finish off the queue, and then handle things based on our // state. if (isAudio) { AudioQueue().Finish(); } else { VideoQueue().Finish(); } - MaybeStopPrerolling(); - mStateObj->HandleEndOfStream(); } void MediaDecoderStateMachine::OnVideoDecoded(MediaData* aVideo, TimeStamp aDecodeStartTime) { MOZ_ASSERT(OnTaskQueue()); @@ -1714,47 +1790,34 @@ void MediaDecoderStateMachine::StopPlayb if (IsPlaying()) { mMediaSink->SetPlaying(false); MOZ_ASSERT(!IsPlaying()); } DispatchDecodeTasksIfNeeded(); } -void -MediaDecoderStateMachine::MaybeStopPrerolling() -{ - MOZ_ASSERT(OnTaskQueue()); - if (mIsPrerolling && - (DonePrerollingAudio() || mReader->IsWaitingAudioData()) && - (DonePrerollingVideo() || mReader->IsWaitingVideoData())) { - mIsPrerolling = false; - // Check if we can start playback. - ScheduleStateMachine(); - } -} - void MediaDecoderStateMachine::MaybeStartPlayback() { MOZ_ASSERT(OnTaskQueue()); // Should try to start playback only after decoding first frames. MOZ_ASSERT(mSentFirstFrameLoadedEvent); MOZ_ASSERT(mState == DECODER_STATE_DECODING || mState == DECODER_STATE_COMPLETED); if (IsPlaying()) { // Logging this case is really spammy - don't do it. return; } bool playStatePermits = mPlayState == MediaDecoder::PLAY_STATE_PLAYING; - if (!playStatePermits || mIsPrerolling || mAudioOffloading) { + if (!playStatePermits || mAudioOffloading) { DECODER_LOG("Not starting playback [playStatePermits: %d, " - "mIsPrerolling: %d, mAudioOffloading: %d]", - playStatePermits, mIsPrerolling, mAudioOffloading); + "mAudioOffloading: %d]", + playStatePermits, mAudioOffloading); return; } DECODER_LOG("MaybeStartPlayback() starting playback"); mOnPlaybackEvent.Notify(MediaEventType::PlaybackStarted); StartMediaSink(); if (!IsPlaying()) { @@ -2982,30 +3045,25 @@ MediaDecoderStateMachine::SetAudioCaptur mMediaSink->Shutdown(); // Create a new sink according to whether audio is captured. mMediaSink = CreateMediaSink(aCaptured); // Restore playback parameters. mMediaSink->SetPlaybackParams(params); - // We don't need to call StartMediaSink() here because IsPlaying() is now - // always in sync with the playing state of MediaSink. It will be started in - // MaybeStartPlayback() in the next cycle if necessary. - mAudioCaptured = aCaptured; - ScheduleStateMachine(); // Don't buffer as much when audio is captured because we don't need to worry // about high latency audio devices. mAmpleAudioThresholdUsecs = mAudioCaptured ? detail::AMPLE_AUDIO_USECS / 2 : detail::AMPLE_AUDIO_USECS; - MaybeStopPrerolling(); + mStateObj->HandleAudioCaptured(); } uint32_t MediaDecoderStateMachine::GetAmpleVideoFrames() const { MOZ_ASSERT(OnTaskQueue()); return (mReader->IsAsync() && mReader->VideoIsHardwareAccelerated()) ? std::max<uint32_t>(sVideoQueueHWAccelSize, MIN_VIDEO_QUEUE_SIZE) : std::max<uint32_t>(sVideoQueueDefaultSize, MIN_VIDEO_QUEUE_SIZE); @@ -3015,25 +3073,26 @@ void MediaDecoderStateMachine::DumpDebugInfo() { MOZ_ASSERT(NS_IsMainThread()); // It is fine to capture a raw pointer here because MediaDecoder only call // this function before shutdown begins. nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction([this] () { mMediaSink->DumpDebugInfo(); + mStateObj->DumpDebugInfo(); DUMP_LOG( "GetMediaTime=%lld GetClock=%lld mMediaSink=%p " "mState=%s mPlayState=%d mSentFirstFrameLoadedEvent=%d IsPlaying=%d " "mAudioStatus=%s mVideoStatus=%s mDecodedAudioEndTime=%lld mDecodedVideoEndTime=%lld " - "mIsPrerolling=%d mAudioCompleted=%d mVideoCompleted=%d", + "mAudioCompleted=%d mVideoCompleted=%d", GetMediaTime(), mMediaSink->IsStarted() ? GetClock() : -1, mMediaSink.get(), ToStateStr(), mPlayState.Ref(), mSentFirstFrameLoadedEvent, IsPlaying(), AudioRequestStatus(), VideoRequestStatus(), mDecodedAudioEndTime, mDecodedVideoEndTime, - mIsPrerolling, mAudioCompleted.Ref(), mVideoCompleted.Ref()); + mAudioCompleted.Ref(), mVideoCompleted.Ref()); }); OwnerThread()->DispatchStateChange(r.forget()); } void MediaDecoderStateMachine::AddOutputStream(ProcessedMediaStream* aStream, bool aFinishWhenEnded) {
--- a/dom/media/MediaDecoderStateMachine.h +++ b/dom/media/MediaDecoderStateMachine.h @@ -672,42 +672,16 @@ private: } uint32_t VideoPrerollFrames() const { MOZ_ASSERT(OnTaskQueue()); return GetAmpleVideoFrames() / 2; } - bool DonePrerollingAudio() - { - MOZ_ASSERT(OnTaskQueue()); - return !IsAudioDecoding() || - GetDecodedAudioDuration() >= AudioPrerollUsecs() * mPlaybackRate; - } - - bool DonePrerollingVideo() - { - MOZ_ASSERT(OnTaskQueue()); - return !IsVideoDecoding() || - static_cast<uint32_t>(VideoQueue().GetSize()) >= - VideoPrerollFrames() * mPlaybackRate + 1; - } - - void MaybeStopPrerolling(); - - // When we start decoding (either for the first time, or after a pause) - // we may be low on decoded data. We don't want our "low data" logic to - // kick in and decide that we're low on decoded data because the download - // can't keep up with the decode, and cause us to pause playback. So we - // have a "preroll" stage, where we ignore the results of our "low data" - // logic during the first few frames of our decode. This occurs during - // playback. - bool mIsPrerolling = false; - // Only one of a given pair of ({Audio,Video}DataPromise, WaitForDataPromise) // should exist at any given moment. MediaEventListener mAudioCallback; MediaEventListener mVideoCallback; MediaEventListener mAudioWaitCallback; MediaEventListener mVideoWaitCallback;
--- a/dom/media/MediaManager.cpp +++ b/dom/media/MediaManager.cpp @@ -1508,28 +1508,23 @@ public: } if (mAudioDevice) { mAudioDevice->Deallocate(); } } } if (errorMsg) { LOG(("%s %d", errorMsg, rv)); - switch (rv) { - case NS_ERROR_NOT_AVAILABLE: { - MOZ_ASSERT(badConstraint); - Fail(NS_LITERAL_STRING("OverconstrainedError"), - NS_LITERAL_STRING(""), - NS_ConvertUTF8toUTF16(badConstraint)); - break; - } - default: - Fail(NS_LITERAL_STRING("NotReadableError"), - NS_ConvertUTF8toUTF16(errorMsg)); - break; + if (badConstraint) { + Fail(NS_LITERAL_STRING("OverconstrainedError"), + NS_LITERAL_STRING(""), + NS_ConvertUTF8toUTF16(badConstraint)); + } else { + Fail(NS_LITERAL_STRING("NotReadableError"), + NS_ConvertUTF8toUTF16(errorMsg)); } return NS_OK; } PeerIdentity* peerIdentity = nullptr; if (!mConstraints.mPeerIdentity.IsEmpty()) { peerIdentity = new PeerIdentity(mConstraints.mPeerIdentity); } @@ -3515,17 +3510,17 @@ GetUserMediaCallbackMediaStreamListener: } RefPtr<PledgeVoid> p = mgr->mOutstandingVoidPledges.Remove(id); if (p) { if (NS_SUCCEEDED(rv)) { p->Resolve(false); } else { auto* window = nsGlobalWindow::GetInnerWindowWithId(windowId); if (window) { - if (rv == NS_ERROR_NOT_AVAILABLE) { + if (badConstraint) { nsString constraint; constraint.AssignASCII(badConstraint); RefPtr<MediaStreamError> error = new MediaStreamError(window->AsInner(), NS_LITERAL_STRING("OverconstrainedError"), NS_LITERAL_STRING(""), constraint); p->Reject(error);
--- a/dom/media/mediasink/VideoSink.cpp +++ b/dom/media/mediasink/VideoSink.cpp @@ -11,19 +11,19 @@ namespace mozilla { extern LazyLogModule gMediaDecoderLog; #undef FMT #undef DUMP_LOG #define FMT(x, ...) "VideoSink=%p " x, this, ##__VA_ARGS__ -#define VSINK_LOG(...) MOZ_LOG(gMediaDecoderLog, LogLevel::Debug, (FMT(__VA_ARGS__))) -#define VSINK_LOG_V(...) MOZ_LOG(gMediaDecoderLog, LogLevel::Verbose, (FMT(__VA_ARGS__))) -#define DUMP_LOG(...) NS_DebugBreak(NS_DEBUG_WARNING, nsPrintfCString(FMT(__VA_ARGS__)).get(), nullptr, nullptr, -1) +#define VSINK_LOG(x, ...) MOZ_LOG(gMediaDecoderLog, LogLevel::Debug, (FMT(x, ##__VA_ARGS__))) +#define VSINK_LOG_V(x, ...) MOZ_LOG(gMediaDecoderLog, LogLevel::Verbose, (FMT(x, ##__VA_ARGS__))) +#define DUMP_LOG(x, ...) NS_DebugBreak(NS_DEBUG_WARNING, nsPrintfCString(FMT(x, ##__VA_ARGS__)).get(), nullptr, nullptr, -1) using namespace mozilla::layers; namespace media { VideoSink::VideoSink(AbstractThread* aThread, MediaSink* aAudioSink, MediaQueue<MediaData>& aVideoQueue,
--- a/dom/media/tests/mochitest/head.js +++ b/dom/media/tests/mochitest/head.js @@ -61,17 +61,18 @@ AudioStreamAnalyser.prototype = { }, /** * Append a canvas to the DOM where the frequency data are drawn. * Useful to debug tests. */ enableDebugCanvas: function() { var cvs = this.debugCanvas = document.createElement("canvas"); - document.getElementById("content").appendChild(cvs); + const content = document.getElementById("content"); + content.insertBefore(cvs, content.children[0]); // Easy: 1px per bin cvs.width = this.analyser.frequencyBinCount; cvs.height = 128; cvs.style.border = "1px solid red"; var c = cvs.getContext('2d'); c.fillStyle = 'black';
--- a/dom/workers/WorkerPrivate.cpp +++ b/dom/workers/WorkerPrivate.cpp @@ -2557,17 +2557,17 @@ WorkerPrivateParent<Derived>::Freeze(nsP AssertIsOnParentThread(); // Shared workers are only frozen if all of their owning documents are // frozen. It can happen that mSharedWorkers is empty but this thread has // not been unregistered yet. if ((IsSharedWorker() || IsServiceWorker()) && !mSharedWorkers.IsEmpty()) { AssertIsOnMainThread(); - bool allFrozen = false; + bool allFrozen = true; for (uint32_t i = 0; i < mSharedWorkers.Length(); ++i) { if (aWindow && mSharedWorkers[i]->GetOwner() == aWindow) { // Calling Freeze() may change the refcount, ensure that the worker // outlives this call. RefPtr<SharedWorker> kungFuDeathGrip = mSharedWorkers[i]; kungFuDeathGrip->Freeze();
--- a/js/src/jsfriendapi.cpp +++ b/js/src/jsfriendapi.cpp @@ -592,17 +592,17 @@ js::AreGCGrayBitsValid(JSContext* cx) { return cx->gc.areGrayBitsValid(); } JS_FRIEND_API(bool) js::ZoneGlobalsAreAllGray(JS::Zone* zone) { for (CompartmentsInZoneIter comp(zone); !comp.done(); comp.next()) { - JSObject* obj = comp->maybeGlobal(); + JSObject* obj = comp->unsafeUnbarrieredMaybeGlobal(); if (!obj || !JS::ObjectIsMarkedGray(obj)) return false; } return true; } namespace { struct VisitGrayCallbackFunctor {
--- a/layout/generic/nsFlexContainerFrame.cpp +++ b/layout/generic/nsFlexContainerFrame.cpp @@ -2776,21 +2776,25 @@ CrossAxisPositionTracker:: // 'normal' behaves as 'stretch' if (mAlignContent == NS_STYLE_ALIGN_NORMAL) { mAlignContent = NS_STYLE_ALIGN_STRETCH; } // XXX strip of the <overflow-position> bit until we implement that mAlignContent &= ~NS_STYLE_ALIGN_FLAG_BITS; - if (!aFirstLine->getNext()) { + const bool isSingleLine = + NS_STYLE_FLEX_WRAP_NOWRAP == aReflowInput.mStylePosition->mFlexWrap; + if (isSingleLine) { + MOZ_ASSERT(!aFirstLine->getNext(), + "If we're styled as single-line, we should only have 1 line"); // "If the flex container is single-line and has a definite cross size, the // cross size of the flex line is the flex container's inner cross size." // - // SOURCE: http://dev.w3.org/csswg/css-flexbox/#algo-line-break + // SOURCE: https://drafts.csswg.org/css-flexbox/#algo-cross-line // NOTE: This means (by definition) that there's no packing space, which // means we don't need to be concerned with "align-conent" at all and we // can return early. This is handy, because this is the usual case (for // single-line flexbox). if (aIsCrossSizeDefinite) { aFirstLine->SetLineCrossSize(aContentBoxCrossSize); return; }
--- a/layout/reftests/font-face/src-list-local-full-quotes.html +++ b/layout/reftests/font-face/src-list-local-full-quotes.html @@ -10,28 +10,28 @@ body { margin: 50px; font-size: 24pt; } /* use full names */ @font-face { font-family: test-regular; - src: local("Helvetica Neue"), local("Bitstream Vera Sans"), local("Bitstream Vera Sans Roman"), local("FreeSans"), local("Free Sans"), local("SwissA"), local("DejaVu Sans"), local("Arial"); + src: local("Helvetica Neue"), local("Bitstream Vera Sans"), local("Bitstream Vera Sans Roman"), local("DejaVu Sans"), local("FreeSans"), local("Free Sans"), local("SwissA"), local("Arial"); } /* use Helvetica on the Mac, since Futura has no bold face on 10.4, 10.5 */ @font-face { font-family: test-bold; - src: local("Helvetica Neue Bold"), local("Bitstream Vera Sans Bold"), local("FreeSans Bold"), local("Free Sans Bold"), local("SwissA Bold"), local("DejaVu Sans Bold"), local("Arial Bold"); + src: local("Helvetica Neue Bold"), local("Bitstream Vera Sans Bold"), local("DejaVu Sans Bold"), local("FreeSans Bold"), local("Free Sans Bold"), local("SwissA Bold"), local("Arial Bold"); } @font-face { font-family: test-italic; - src: local("Helvetica Neue Italic"), local("Bitstream Vera Sans Oblique"), local("FreeSans Oblique"), local("Free Sans Oblique"), local("SwissA Italic"), local("DejaVu Sans Oblique"), local("Arial Italic"); + src: local("Helvetica Neue Italic"), local("Bitstream Vera Sans Oblique"), local("DejaVu Sans Oblique"), local("FreeSans Oblique"), local("Free Sans Oblique"), local("SwissA Italic"), local("Arial Italic"); } .regular { font-family: test-regular, serif; } .bold { font-family: test-bold, serif; } .italic { font-family: test-italic, serif; } </style>
--- a/layout/reftests/font-face/src-list-local-full-ref.html +++ b/layout/reftests/font-face/src-list-local-full-ref.html @@ -6,19 +6,20 @@ <style type="text/css"> body { margin: 50px; font-size: 24pt; } -/* use full names */ +/* Bitstream Vera must be adjacent to DejaVu to because the latter is picked + up by fontconfig aliases for the former. */ -p { font-family: Helvetica Neue, Bitstream Vera Sans, FreeSans, SwissA, DejaVu Sans, Arial, serif; } +p { font-family: Helvetica Neue, Bitstream Vera Sans, DejaVu Sans, FreeSans, SwissA, Arial, serif; } .regular { } .bold { font-weight: bold; } .italic { font-style: italic; } </style> <script type="text/javascript">
--- a/layout/reftests/font-face/src-list-local-full.html +++ b/layout/reftests/font-face/src-list-local-full.html @@ -10,28 +10,28 @@ body { margin: 50px; font-size: 24pt; } /* use full names */ @font-face { font-family: test-regular; - src: local(Helvetica Neue), local(Bitstream Vera Sans), local(Bitstream Vera Sans Roman), local(FreeSans), local(Free Sans), local(SwissA), local(DejaVu Sans), local(Arial); + src: local(Helvetica Neue), local(Bitstream Vera Sans), local(Bitstream Vera Sans Roman), local(DejaVu Sans), local(FreeSans), local(Free Sans), local(SwissA), local(Arial); } /* use Helvetica on the Mac, since Futura has no bold face on 10.4, 10.5 */ @font-face { font-family: test-bold; - src: local(Helvetica Neue Bold), local(Bitstream Vera Sans Bold), local(FreeSans Bold), local(Free Sans Bold), local(SwissA Bold), local(DejaVu Sans Bold), local(Arial Bold); + src: local(Helvetica Neue Bold), local(Bitstream Vera Sans Bold), local(DejaVu Sans Bold), local(FreeSans Bold), local(Free Sans Bold), local(SwissA Bold), local(Arial Bold); } @font-face { font-family: test-italic; - src: local(Helvetica Neue Italic), local(Bitstream Vera Sans Oblique), local(FreeSans Oblique), local(Free Sans Oblique), local(SwissA Italic), local(DejaVu Sans Oblique), local(Arial Italic); + src: local(Helvetica Neue Italic), local(Bitstream Vera Sans Oblique), local(DejaVu Sans Oblique), local(FreeSans Oblique), local(Free Sans Oblique), local(SwissA Italic), local(Arial Italic); } .regular { font-family: test-regular, serif; } .bold { font-family: test-bold, serif; } .italic { font-family: test-italic, serif; } </style>
--- a/layout/reftests/w3c-css/submitted/flexbox/flexbox-align-content-horiz-001-ref.xhtml +++ b/layout/reftests/w3c-css/submitted/flexbox/flexbox-align-content-horiz-001-ref.xhtml @@ -66,31 +66,31 @@ <div class="flexbox"> <div class="a"/> <div class="b"><div class="fixedSizeChild"/></div> <div class="c"/> </div> <!-- flex-end --> <div class="flexbox"> - <div class="a"/> + <div class="a" style="margin-top: 190px"/> </div> <div class="flexbox"> <div class="a" style="margin-top: 160px"/> <div class="b"><div class="fixedSizeChild"/></div> </div> <div class="flexbox"> <div class="a" style="margin-top: 120px"/> <div class="b"><div class="fixedSizeChild"/></div> <div class="c"/> </div> <!-- center --> <div class="flexbox"> - <div class="a"/> + <div class="a" style="margin-top: 95px"/> </div> <div class="flexbox"> <div class="a" style="margin-top: 80px"/> <div class="b"><div class="fixedSizeChild"/></div> </div> <div class="flexbox"> <div class="a" style="margin-top: 60px"/> <div class="b"><div class="fixedSizeChild"/></div> @@ -108,17 +108,17 @@ <div class="flexbox"> <div class="a"/> <div class="b" style="margin-top: 60px"><div class="fixedSizeChild"/></div> <div class="c" style="margin-top: 60px"/> </div> <!-- space-around --> <div class="flexbox"> - <div class="a"/> + <div class="a" style="margin-top: 95px"/> </div> <div class="flexbox"> <div class="a" style="margin-top: 40px"/> <div class="b" style="margin-top: 80px"><div class="fixedSizeChild"/></div> </div> <div class="flexbox"> <div class="a" style="margin-top: 20px"/> <div class="b" style="margin-top: 40px"><div class="fixedSizeChild"/></div>
--- a/layout/reftests/w3c-css/submitted/flexbox/flexbox-align-content-vert-001-ref.xhtml +++ b/layout/reftests/w3c-css/submitted/flexbox/flexbox-align-content-vert-001-ref.xhtml @@ -69,31 +69,31 @@ <div class="flexbox"> <div class="a"/> <div class="b"><div class="fixedSizeChild"/></div> <div class="c"/> </div> <!-- flex-end --> <div class="flexbox"> - <div class="a"/> + <div class="a" style="margin-left: 190px"/> </div> <div class="flexbox"> <div class="a" style="margin-left: 160px"/> <div class="b"><div class="fixedSizeChild"/></div> </div> <div class="flexbox"> <div class="a" style="margin-left: 120px"/> <div class="b"><div class="fixedSizeChild"/></div> <div class="c"/> </div> <!-- center --> <div class="flexbox"> - <div class="a"/> + <div class="a" style="margin-left: 95px"/> </div> <div class="flexbox"> <div class="a" style="margin-left: 80px"/> <div class="b"><div class="fixedSizeChild"/></div> </div> <div class="flexbox"> <div class="a" style="margin-left: 60px"/> <div class="b"><div class="fixedSizeChild"/></div> @@ -111,17 +111,17 @@ <div class="flexbox"> <div class="a"/> <div class="b" style="margin-left: 60px"><div class="fixedSizeChild"/></div> <div class="c" style="margin-left: 60px"/> </div> <!-- space-around --> <div class="flexbox"> - <div class="a"/> + <div class="a" style="margin-left: 95px"/> </div> <div class="flexbox"> <div class="a" style="margin-left: 40px"/> <div class="b" style="margin-left: 80px"><div class="fixedSizeChild"/></div> </div> <div class="flexbox"> <div class="a" style="margin-left: 20px"/> <div class="b" style="margin-left: 40px"><div class="fixedSizeChild"/></div> @@ -139,17 +139,17 @@ <div class="flexbox"> <div class="a"/> <div class="b"><div class="fixedSizeChild"/></div> <div class="c"/> </div> <!-- right --> <div class="flexbox"> - <div class="a"/> + <div class="a" style="margin-left: 190px"/> </div> <div class="flexbox"> <div class="a" style="margin-left: 160px"/> <div class="b"><div class="fixedSizeChild"/></div> </div> <div class="flexbox"> <div class="a" style="margin-left: 120px"/> <div class="b"><div class="fixedSizeChild"/></div>
--- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -1450,23 +1450,21 @@ pref("network.http.accept.default", "tex // Prefs allowing granular control of referers // 0=don't send any, 1=send only on clicks, 2=send on image requests as well pref("network.http.sendRefererHeader", 2); // false=real referer, true=spoof referer (use target URI as referer) pref("network.http.referer.spoofSource", false); // 0=full URI, 1=scheme+host+port+path, 2=scheme+host+port pref("network.http.referer.trimmingPolicy", 0); +// 0=full URI, 1=scheme+host+port+path, 2=scheme+host+port +pref("network.http.referer.XOriginTrimmingPolicy", 0); // 0=always send, 1=send iff base domains match, 2=send iff hosts match pref("network.http.referer.XOriginPolicy", 0); -// Controls whether we send HTTPS referres to other HTTPS sites. -// By default this is enabled for compatibility (see bug 141641) -pref("network.http.sendSecureXSiteReferrer", true); - // Controls whether referrer attributes in <a>, <img>, <area>, <iframe>, and <link> are honoured pref("network.http.enablePerElementReferrer", true); // Maximum number of consecutive redirects before aborting. pref("network.http.redirection-limit", 20); // Enable http compression: comment this out in case of problems with 1.1 // NOTE: support for "compress" has been disabled per bug 196406.
--- a/netwerk/protocol/http/HttpBaseChannel.cpp +++ b/netwerk/protocol/http/HttpBaseChannel.cpp @@ -1403,32 +1403,16 @@ HttpBaseChannel::SetReferrerWithPolicy(n // It's ok to send referrer for https-to-http scenarios if the referrer // policy is "unsafe-url", "origin", or "origin-when-cross-origin". if (referrerPolicy != REFERRER_POLICY_UNSAFE_URL && referrerPolicy != REFERRER_POLICY_ORIGIN_WHEN_XORIGIN && referrerPolicy != REFERRER_POLICY_ORIGIN) { // in other referrer policies, https->http is not allowed... if (!match) return NS_OK; - - // ...and https->https is possibly only allowed if the hosts match. - if (!gHttpHandler->SendSecureXSiteReferrer()) { - nsAutoCString referrerHost; - nsAutoCString host; - - rv = referrer->GetAsciiHost(referrerHost); - if (NS_FAILED(rv)) return rv; - - rv = mURI->GetAsciiHost(host); - if (NS_FAILED(rv)) return rv; - - // GetAsciiHost returns lowercase hostname. - if (!referrerHost.Equals(host)) - return NS_OK; - } } } // for cross-origin-based referrer changes (not just host-based), figure out // if the referrer is being sent cross-origin. nsCOMPtr<nsIURI> triggeringURI; bool isCrossOrigin = true; if (mLoadInfo) { @@ -1512,16 +1496,25 @@ HttpBaseChannel::SetReferrerWithPolicy(n // strip away any userpass; we don't want to be giving out passwords ;-) // This is required by Referrer Policy stripping algorithm. rv = clone->SetUserPass(EmptyCString()); if (NS_FAILED(rv)) return rv; nsAutoCString spec; + // Apply the user cross-origin trimming policy if it's more + // restrictive than the general one. + if (isCrossOrigin) { + int userReferrerXOriginTrimmingPolicy = + gHttpHandler->ReferrerXOriginTrimmingPolicy(); + userReferrerTrimmingPolicy = + std::max(userReferrerTrimmingPolicy, userReferrerXOriginTrimmingPolicy); + } + // site-specified referrer trimming may affect the trim level // "unsafe-url" behaves like "origin" (send referrer in the same situations) but // "unsafe-url" sends the whole referrer and origin removes the path. // "origin-when-cross-origin" trims the referrer only when the request is // cross-origin. // "Strict" request from https->http case was bailed out, so here: // "strict-origin" behaves the same as "origin". // "strict-origin-when-cross-origin" behaves the same as "origin-when-cross-origin"
--- a/netwerk/protocol/http/nsHttpHandler.cpp +++ b/netwerk/protocol/http/nsHttpHandler.cpp @@ -161,16 +161,17 @@ nsHttpHandler *gHttpHandler = nullptr; nsHttpHandler::nsHttpHandler() : mHttpVersion(NS_HTTP_VERSION_1_1) , mProxyHttpVersion(NS_HTTP_VERSION_1_1) , mCapabilities(NS_HTTP_ALLOW_KEEPALIVE) , mReferrerLevel(0xff) // by default we always send a referrer , mSpoofReferrerSource(false) , mReferrerTrimmingPolicy(0) + , mReferrerXOriginTrimmingPolicy(0) , mReferrerXOriginPolicy(0) , mFastFallbackToIPv4(false) , mProxyPipelining(true) , mIdleTimeout(PR_SecondsToInterval(10)) , mSpdyTimeout(PR_SecondsToInterval(180)) , mResponseTimeout(PR_SecondsToInterval(300)) , mResponseTimeoutEnabled(false) , mNetworkChangedTimeout(5000) @@ -197,17 +198,16 @@ nsHttpHandler::nsHttpHandler() , mLastUniqueID(NowInSeconds()) , mSessionStartTime(0) , mLegacyAppName("Mozilla") , mLegacyAppVersion("5.0") , mProduct("Gecko") , mCompatFirefoxEnabled(false) , mUserAgentIsDirty(true) , mPromptTempRedirect(true) - , mSendSecureXSiteReferrer(true) , mEnablePersistentHttpsCaching(false) , mDoNotTrackEnabled(false) , mSafeHintEnabled(false) , mParentalControlEnabled(false) , mHandlerActive(false) , mTelemetryEnabled(false) , mAllowExperiments(true) , mDebugObservations(false) @@ -1080,16 +1080,22 @@ nsHttpHandler::PrefsChanged(nsIPrefBranc } if (PREF_CHANGED(HTTP_PREF("referer.trimmingPolicy"))) { rv = prefs->GetIntPref(HTTP_PREF("referer.trimmingPolicy"), &val); if (NS_SUCCEEDED(rv)) mReferrerTrimmingPolicy = (uint8_t) clamped(val, 0, 2); } + if (PREF_CHANGED(HTTP_PREF("referer.XOriginTrimmingPolicy"))) { + rv = prefs->GetIntPref(HTTP_PREF("referer.XOriginTrimmingPolicy"), &val); + if (NS_SUCCEEDED(rv)) + mReferrerXOriginTrimmingPolicy = (uint8_t) clamped(val, 0, 2); + } + if (PREF_CHANGED(HTTP_PREF("referer.XOriginPolicy"))) { rv = prefs->GetIntPref(HTTP_PREF("referer.XOriginPolicy"), &val); if (NS_SUCCEEDED(rv)) mReferrerXOriginPolicy = (uint8_t) clamped(val, 0, 0xff); } if (PREF_CHANGED(HTTP_PREF("redirection-limit"))) { rv = prefs->GetIntPref(HTTP_PREF("redirection-limit"), &val); @@ -1225,22 +1231,16 @@ nsHttpHandler::PrefsChanged(nsIPrefBranc } if (PREF_CHANGED(HTTP_PREF("qos"))) { rv = prefs->GetIntPref(HTTP_PREF("qos"), &val); if (NS_SUCCEEDED(rv)) mQoSBits = (uint8_t) clamped(val, 0, 0xff); } - if (PREF_CHANGED(HTTP_PREF("sendSecureXSiteReferrer"))) { - rv = prefs->GetBoolPref(HTTP_PREF("sendSecureXSiteReferrer"), &cVar); - if (NS_SUCCEEDED(rv)) - mSendSecureXSiteReferrer = cVar; - } - if (PREF_CHANGED(HTTP_PREF("accept.default"))) { nsXPIDLCString accept; rv = prefs->GetCharPref(HTTP_PREF("accept.default"), getter_Copies(accept)); if (NS_SUCCEEDED(rv)) SetAccept(accept); }
--- a/netwerk/protocol/http/nsHttpHandler.h +++ b/netwerk/protocol/http/nsHttpHandler.h @@ -77,18 +77,20 @@ public: const nsAFlatCString &UserAgent(); nsHttpVersion HttpVersion() { return mHttpVersion; } nsHttpVersion ProxyHttpVersion() { return mProxyHttpVersion; } uint8_t ReferrerLevel() { return mReferrerLevel; } bool SpoofReferrerSource() { return mSpoofReferrerSource; } uint8_t ReferrerTrimmingPolicy() { return mReferrerTrimmingPolicy; } + uint8_t ReferrerXOriginTrimmingPolicy() { + return mReferrerXOriginTrimmingPolicy; + } uint8_t ReferrerXOriginPolicy() { return mReferrerXOriginPolicy; } - bool SendSecureXSiteReferrer() { return mSendSecureXSiteReferrer; } bool PackagedAppsEnabled() { return mPackagedAppsEnabled; } uint8_t RedirectionLimit() { return mRedirectionLimit; } PRIntervalTime IdleTimeout() { return mIdleTimeout; } PRIntervalTime SpdyTimeout() { return mSpdyTimeout; } PRIntervalTime ResponseTimeout() { return mResponseTimeoutEnabled ? mResponseTimeout : 0; } PRIntervalTime ResponseTimeoutEnabled() { return mResponseTimeoutEnabled; } @@ -412,16 +414,17 @@ private: // uint8_t mHttpVersion; uint8_t mProxyHttpVersion; uint32_t mCapabilities; uint8_t mReferrerLevel; uint8_t mSpoofReferrerSource; uint8_t mReferrerTrimmingPolicy; + uint8_t mReferrerXOriginTrimmingPolicy; uint8_t mReferrerXOriginPolicy; bool mFastFallbackToIPv4; bool mProxyPipelining; PRIntervalTime mIdleTimeout; PRIntervalTime mSpdyTimeout; PRIntervalTime mResponseTimeout; bool mResponseTimeoutEnabled; @@ -487,19 +490,16 @@ private: nsCString mDeviceModelId; nsCString mUserAgent; nsXPIDLCString mUserAgentOverride; bool mUserAgentIsDirty; // true if mUserAgent should be rebuilt bool mPromptTempRedirect; - // mSendSecureXSiteReferrer: default is false, - // if true allow referrer headers between secure non-matching hosts - bool mSendSecureXSiteReferrer; // Persistent HTTPS caching flag bool mEnablePersistentHttpsCaching; // For broadcasting tracking preference bool mDoNotTrackEnabled; // for broadcasting safe hint;
--- a/netwerk/test/unit/test_referrer.js +++ b/netwerk/test/unit/test_referrer.js @@ -1,19 +1,24 @@ Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); function getTestReferrer(server_uri, referer_uri) { var uri = NetUtil.newURI(server_uri, "", null) + let referrer = NetUtil.newURI(referer_uri, null, null); + let triggeringPrincipal = Services.scriptSecurityManager.createCodebasePrincipal(referrer, {}); var chan = NetUtil.newChannel({ uri: uri, - loadUsingSystemPrincipal: true + loadingPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + triggeringPrincipal: triggeringPrincipal, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER }); chan.QueryInterface(Components.interfaces.nsIHttpChannel); - chan.referrer = NetUtil.newURI(referer_uri, null, null); + chan.referrer = referrer; var header = null; try { header = chan.getRequestHeader("Referer"); } catch (NS_ERROR_NOT_AVAILABLE) {} return header; } @@ -26,16 +31,17 @@ function run_test() { var referer_uri = "http://foo.example.com/path"; var referer_uri_2 = "http://bar.examplesite.com/path3?q=blah"; var referer_uri_2_anchor = "http://bar.examplesite.com/path3?q=blah#anchor"; var referer_uri_idn = "http://sub1.\xe4lt.example/path"; // for https tests var server_uri_https = "https://bar.example.com/anotherpath"; var referer_uri_https = "https://bar.example.com/path3?q=blah"; + var referer_uri_2_https = "https://bar.examplesite.com/path3?q=blah"; // tests for sendRefererHeader prefs.setIntPref("network.http.sendRefererHeader", 0); do_check_null(getTestReferrer(server_uri, referer_uri)); prefs.setIntPref("network.http.sendRefererHeader", 2); do_check_eq(getTestReferrer(server_uri, referer_uri), referer_uri); // test that https ref is not sent to http @@ -67,16 +73,36 @@ function run_test() { do_check_eq(getTestReferrer(server_uri, referer_uri_2), "http://bar.examplesite.com/"); do_check_eq(getTestReferrer(server_uri, referer_uri_idn), "http://sub1.xn--lt-uia.example/"); // https test do_check_eq(getTestReferrer(server_uri_https, referer_uri_https), "https://bar.example.com/"); prefs.setIntPref("network.http.referer.trimmingPolicy", 0); // test that anchor is lopped off in ordinary case do_check_eq(getTestReferrer(server_uri, referer_uri_2_anchor), referer_uri_2); + // tests for referer.XOriginTrimmingPolicy + prefs.setIntPref("network.http.referer.XOriginTrimmingPolicy", 1); + do_check_eq(getTestReferrer(server_uri, referer_uri), "http://foo.example.com/path"); + do_check_eq(getTestReferrer(server_uri, referer_uri_idn), "http://sub1.xn--lt-uia.example/path"); + do_check_eq(getTestReferrer(server_uri, referer_uri_2), "http://bar.examplesite.com/path3?q=blah"); + prefs.setIntPref("network.http.referer.trimmingPolicy", 1); + do_check_eq(getTestReferrer(server_uri, referer_uri_2), "http://bar.examplesite.com/path3"); + prefs.setIntPref("network.http.referer.XOriginTrimmingPolicy", 2); + do_check_eq(getTestReferrer(server_uri, referer_uri), "http://foo.example.com/"); + do_check_eq(getTestReferrer(server_uri, referer_uri_idn), "http://sub1.xn--lt-uia.example/"); + do_check_eq(getTestReferrer(server_uri, referer_uri_2), "http://bar.examplesite.com/path3"); + prefs.setIntPref("network.http.referer.trimmingPolicy", 0); + do_check_eq(getTestReferrer(server_uri, referer_uri_2), "http://bar.examplesite.com/path3?q=blah"); + // https tests + do_check_eq(getTestReferrer(server_uri_https, referer_uri_https), "https://bar.example.com/path3?q=blah"); + do_check_eq(getTestReferrer(server_uri_https, referer_uri_2_https), "https://bar.examplesite.com/"); + prefs.setIntPref("network.http.referer.XOriginTrimmingPolicy", 0); + // test that anchor is lopped off in ordinary case + do_check_eq(getTestReferrer(server_uri, referer_uri_2_anchor), referer_uri_2); + // combination test: send spoofed path-only when hosts match var combo_referer_uri = "http://blah.foo.com/path?q=hot"; var dest_uri = "http://blah.foo.com:9999/spoofedpath?q=bad"; prefs.setIntPref("network.http.referer.trimmingPolicy", 1); prefs.setBoolPref("network.http.referer.spoofSource", true); prefs.setIntPref("network.http.referer.XOriginPolicy", 2); do_check_eq(getTestReferrer(dest_uri, combo_referer_uri), "http://blah.foo.com:9999/spoofedpath"); do_check_null(getTestReferrer(dest_uri, "http://gah.foo.com/anotherpath"));
--- a/security/manager/pki/resources/content/certManager.js +++ b/security/manager/pki/resources/content/certManager.js @@ -18,20 +18,16 @@ const nsDialogParamBlock = "@mozilla.org const gCertFileTypes = "*.p7b; *.crt; *.cert; *.cer; *.pem; *.der"; var { NetUtil } = Components.utils.import("resource://gre/modules/NetUtil.jsm", {}); var { Services } = Components.utils.import("resource://gre/modules/Services.jsm", {}); var key; -/** - * List of certs currently selected in the active tab. - * @type nsIX509Cert[] - */ var selected_certs = []; var selected_tree_items = []; var selected_index = []; var certdb; var caTreeView; var serverTreeView; var emailTreeView; @@ -328,18 +324,18 @@ function backupAllCerts() backupCerts(); } function editCerts() { getSelectedCerts(); for (let cert of selected_certs) { - window.openDialog("chrome://pippki/content/editcacert.xul", "", - "chrome,centerscreen,modal", cert); + window.openDialog("chrome://pippki/content/editcacert.xul", cert.dbKey, + "chrome,centerscreen,modal"); } } function restoreCerts() { var bundle = document.getElementById("pippki_bundle"); var fp = Components.classes[nsFilePicker].createInstance(nsIFilePicker); fp.init(window,
--- a/security/manager/pki/resources/content/editcacert.xul +++ b/security/manager/pki/resources/content/editcacert.xul @@ -2,29 +2,28 @@ <!-- This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> <?xml-stylesheet href="chrome://global/skin/" type="text/css"?> <!DOCTYPE dialog SYSTEM "chrome://pippki/locale/certManager.dtd"> -<dialog id="editCaCert" +<dialog id="editCaCert" title="&certmgr.editcacert.title;" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" buttons="accept,cancel" - ondialogaccept="return onDialogAccept();" - onload="onLoad();" + ondialogaccept="return doOK();" + onload="setWindowName();" > <stringbundle id="pippki_bundle" src="chrome://pippki/locale/pippki.properties"/> <script type="application/javascript" src="chrome://pippki/content/pippki.js"/> - <script type="application/javascript" - src="chrome://pippki/content/editcacert.js"/> + <script type="application/javascript" src="chrome://pippki/content/editcerts.js"/> <description id="certmsg"/> <separator/> <description>&certmgr.editcert.edittrust;</description> <vbox align="start"> <checkbox label="&certmgr.editcert.trustssl;" id="trustSSL"/> <checkbox label="&certmgr.editcert.trustemail;"
rename from security/manager/pki/resources/content/editcacert.js rename to security/manager/pki/resources/content/editcerts.js --- a/security/manager/pki/resources/content/editcacert.js +++ b/security/manager/pki/resources/content/editcerts.js @@ -1,58 +1,71 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* import-globals-from pippki.js */ "use strict"; -const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; - -var gCertDB = Cc["@mozilla.org/security/x509certdb;1"] - .getService(Ci.nsIX509CertDB); -/** - * Cert to edit the trust of. - * @type nsIX509Cert - */ -var gCert; - -/** - * onload() handler. - */ -function onLoad() { - gCert = window.arguments[0]; +const nsIX509Cert = Components.interfaces.nsIX509Cert; +const nsX509CertDB = "@mozilla.org/security/x509certdb;1"; +const nsIX509CertDB = Components.interfaces.nsIX509CertDB; - let bundle = document.getElementById("pippki_bundle"); - setText("certmsg", - bundle.getFormattedString("editTrustCA", [gCert.commonName])); - - let sslCheckbox = document.getElementById("trustSSL"); - sslCheckbox.checked = gCertDB.isCertTrusted(gCert, Ci.nsIX509Cert.CA_CERT, - Ci.nsIX509CertDB.TRUSTED_SSL); +var certdb; +var cert; - let emailCheckbox = document.getElementById("trustEmail"); - emailCheckbox.checked = gCertDB.isCertTrusted(gCert, Ci.nsIX509Cert.CA_CERT, - Ci.nsIX509CertDB.TRUSTED_EMAIL); - - let objSignCheckbox = document.getElementById("trustObjSign"); - objSignCheckbox.checked = - gCertDB.isCertTrusted(gCert, Ci.nsIX509Cert.CA_CERT, - Ci.nsIX509CertDB.TRUSTED_OBJSIGN); +function doPrompt(msg) +{ + let prompts = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]. + getService(Components.interfaces.nsIPromptService); + prompts.alert(window, null, msg); } -/** - * ondialogaccept() handler. - * - * @returns {Boolean} true to make the dialog close, false otherwise. - */ -function onDialogAccept() { - let sslCheckbox = document.getElementById("trustSSL"); - let emailCheckbox = document.getElementById("trustEmail"); - let objSignCheckbox = document.getElementById("trustObjSign"); - let trustSSL = sslCheckbox.checked ? Ci.nsIX509CertDB.TRUSTED_SSL : 0; - let trustEmail = emailCheckbox.checked ? Ci.nsIX509CertDB.TRUSTED_EMAIL : 0; - let trustObjSign = objSignCheckbox.checked ? Ci.nsIX509CertDB.TRUSTED_OBJSIGN - : 0; +function setWindowName() +{ + var dbkey = self.name; + + // Get the cert from the cert database + certdb = Components.classes[nsX509CertDB].getService(nsIX509CertDB); + cert = certdb.findCertByDBKey(dbkey); + + var bundle = document.getElementById("pippki_bundle"); + + var message1 = bundle.getFormattedString("editTrustCA", [cert.commonName]); + setText("certmsg", message1); - gCertDB.setCertTrust(gCert, Ci.nsIX509Cert.CA_CERT, - trustSSL | trustEmail | trustObjSign); + var ssl = document.getElementById("trustSSL"); + if (certdb.isCertTrusted(cert, nsIX509Cert.CA_CERT, + nsIX509CertDB.TRUSTED_SSL)) { + ssl.setAttribute("checked", "true"); + } else { + ssl.setAttribute("checked", "false"); + } + var email = document.getElementById("trustEmail"); + if (certdb.isCertTrusted(cert, nsIX509Cert.CA_CERT, + nsIX509CertDB.TRUSTED_EMAIL)) { + email.setAttribute("checked", "true"); + } else { + email.setAttribute("checked", "false"); + } + var objsign = document.getElementById("trustObjSign"); + if (certdb.isCertTrusted(cert, nsIX509Cert.CA_CERT, + nsIX509CertDB.TRUSTED_OBJSIGN)) { + objsign.setAttribute("checked", "true"); + } else { + objsign.setAttribute("checked", "false"); + } +} + +function doOK() +{ + var ssl = document.getElementById("trustSSL"); + var email = document.getElementById("trustEmail"); + var objsign = document.getElementById("trustObjSign"); + var trustssl = (ssl.checked) ? nsIX509CertDB.TRUSTED_SSL : 0; + var trustemail = (email.checked) ? nsIX509CertDB.TRUSTED_EMAIL : 0; + var trustobjsign = (objsign.checked) ? nsIX509CertDB.TRUSTED_OBJSIGN : 0; + // + // Set the cert trust + // + certdb.setCertTrust(cert, nsIX509Cert.CA_CERT, + trustssl | trustemail | trustobjsign); return true; }
--- a/security/manager/pki/resources/jar.mn +++ b/security/manager/pki/resources/jar.mn @@ -14,17 +14,17 @@ pippki.jar: content/pippki/certManager.xul (content/certManager.xul) content/pippki/CAOverlay.xul (content/CAOverlay.xul) content/pippki/WebSitesOverlay.xul (content/WebSitesOverlay.xul) content/pippki/OthersOverlay.xul (content/OthersOverlay.xul) content/pippki/MineOverlay.xul (content/MineOverlay.xul) content/pippki/OrphanOverlay.xul (content/OrphanOverlay.xul) content/pippki/viewCertDetails.xul (content/viewCertDetails.xul) content/pippki/editcacert.xul (content/editcacert.xul) - content/pippki/editcacert.js (content/editcacert.js) + content/pippki/editcerts.js (content/editcerts.js) * content/pippki/exceptionDialog.xul (content/exceptionDialog.xul) content/pippki/exceptionDialog.js (content/exceptionDialog.js) content/pippki/deletecert.xul (content/deletecert.xul) content/pippki/deletecert.js (content/deletecert.js) content/pippki/viewCertDetails.js (content/viewCertDetails.js) content/pippki/setp12password.xul (content/setp12password.xul) content/pippki/pippki.js (content/pippki.js) content/pippki/clientauthask.xul (content/clientauthask.xul)
--- a/security/manager/ssl/tests/mochitest/browser/browser.ini +++ b/security/manager/ssl/tests/mochitest/browser/browser.ini @@ -5,9 +5,8 @@ support-files = *.pem [browser_bug627234_perwindowpb.js] [browser_certificateManagerLeak.js] [browser_certViewer.js] [browser_clientAuth_connection.js] [browser_clientAuth_ui.js] [browser_deleteCert_ui.js] -[browser_editCACertTrust.js]
--- a/security/manager/ssl/tests/mochitest/browser/browser_certViewer.js +++ b/security/manager/ssl/tests/mochitest/browser/browser_certViewer.js @@ -5,118 +5,129 @@ // Repeatedly opens the certificate viewer dialog with various certificates and // determines that the viewer correctly identifies either what usages those // certificates are valid for or what errors prevented the certificates from // being verified. var { OS } = Cu.import("resource://gre/modules/osfile.jsm", {}); +var certificates = []; + +registerCleanupFunction(function() { + let certdb = Cc["@mozilla.org/security/x509certdb;1"] + .getService(Ci.nsIX509CertDB); + certificates.forEach(cert => { + certdb.deleteCertificate(cert); + }); +}); + add_task(function* () { - let cert = yield readCertificate("ca.pem", "CTu,CTu,CTu"); + let cert = yield readCertificate("ca.pem", "CTu,CTu,CTu", certificates); let win = yield displayCertificate(cert); checkUsages(win, ["SSL Certificate Authority"]); yield BrowserTestUtils.closeWindow(win); }); add_task(function* () { - let cert = yield readCertificate("ssl-ee.pem", ",,"); + let cert = yield readCertificate("ssl-ee.pem", ",,", certificates); let win = yield displayCertificate(cert); checkUsages(win, ["SSL Server Certificate", "SSL Client Certificate"]); yield BrowserTestUtils.closeWindow(win); }); add_task(function* () { - let cert = yield readCertificate("email-ee.pem", ",,"); + let cert = yield readCertificate("email-ee.pem", ",,", certificates); let win = yield displayCertificate(cert); checkUsages(win, ["Email Recipient Certificate", "Email Signer Certificate"]); yield BrowserTestUtils.closeWindow(win); }); add_task(function* () { - let cert = yield readCertificate("code-ee.pem", ",,"); + let cert = yield readCertificate("code-ee.pem", ",,", certificates); let win = yield displayCertificate(cert); checkUsages(win, ["Object Signer"]); yield BrowserTestUtils.closeWindow(win); }); add_task(function* () { - let cert = yield readCertificate("expired-ca.pem", ",,"); + let cert = yield readCertificate("expired-ca.pem", ",,", certificates); let win = yield displayCertificate(cert); checkError(win, "Could not verify this certificate because it has expired."); yield BrowserTestUtils.closeWindow(win); }); add_task(function* () { - let cert = yield readCertificate("ee-from-expired-ca.pem", ",,"); + let cert = yield readCertificate("ee-from-expired-ca.pem", ",,", certificates); let win = yield displayCertificate(cert); checkError(win, "Could not verify this certificate because the CA certificate " + "is invalid."); yield BrowserTestUtils.closeWindow(win); }); add_task(function* () { - let cert = yield readCertificate("unknown-issuer.pem", ",,"); + let cert = yield readCertificate("unknown-issuer.pem", ",,", certificates); let win = yield displayCertificate(cert); checkError(win, "Could not verify this certificate because the issuer is " + "unknown."); yield BrowserTestUtils.closeWindow(win); }); add_task(function* () { - let cert = yield readCertificate("md5-ee.pem", ",,"); + let cert = yield readCertificate("md5-ee.pem", ",,", certificates); let win = yield displayCertificate(cert); checkError(win, "Could not verify this certificate because it was signed using " + "a signature algorithm that was disabled because that algorithm " + "is not secure."); yield BrowserTestUtils.closeWindow(win); }); add_task(function* () { - let cert = yield readCertificate("untrusted-ca.pem", "p,p,p"); + let cert = yield readCertificate("untrusted-ca.pem", "p,p,p", certificates); let win = yield displayCertificate(cert); checkError(win, "Could not verify this certificate because it is not trusted."); yield BrowserTestUtils.closeWindow(win); }); add_task(function* () { - let cert = yield readCertificate("ee-from-untrusted-ca.pem", ",,"); + let cert = yield readCertificate("ee-from-untrusted-ca.pem", ",,", + certificates); let win = yield displayCertificate(cert); checkError(win, "Could not verify this certificate because the issuer is not " + "trusted."); yield BrowserTestUtils.closeWindow(win); }); add_task(function* () { // Note that there's currently no way to un-do this. This should only be a // problem if another test re-uses a certificate with this same key (perhaps // likely) and subject (less likely). let certBlocklist = Cc["@mozilla.org/security/certblocklist;1"] .getService(Ci.nsICertBlocklist); certBlocklist.revokeCertBySubjectAndPubKey( "MBIxEDAOBgNVBAMMB3Jldm9rZWQ=", // CN=revoked "VCIlmPM9NkgFQtrs4Oa5TeFcDu6MWRTKSNdePEhOgD8="); // hash of the shared key - let cert = yield readCertificate("revoked.pem", ",,"); + let cert = yield readCertificate("revoked.pem", ",,", certificates); let win = yield displayCertificate(cert); checkError(win, "Could not verify this certificate because it has been revoked."); yield BrowserTestUtils.closeWindow(win); }); add_task(function* () { // This certificate has a keyUsage extension asserting cRLSign and // keyCertSign, but it doesn't have a basicConstraints extension. This // shouldn't be valid for any usage. Sadly, we give a pretty lame error // message in this case. - let cert = yield readCertificate("invalid.pem", ",,"); + let cert = yield readCertificate("invalid.pem", ",,", certificates); let win = yield displayCertificate(cert); checkError(win, "Could not verify this certificate for unknown reasons."); yield BrowserTestUtils.closeWindow(win); }); /** * Given a certificate, returns a promise that will resolve when the certificate * viewer has opened is displaying that certificate, and has finished
--- a/security/manager/ssl/tests/mochitest/browser/browser_deleteCert_ui.js +++ b/security/manager/ssl/tests/mochitest/browser/browser_deleteCert_ui.js @@ -9,16 +9,17 @@ // 2. The implementation correctly falls back through multiple cert attributes // to determine what to display to represent a cert. /** * An array of tree items corresponding to TEST_CASES. * @type nsIMutableArray<nsICertTreeItem> */ var gCertArray = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); +var gImportedCerts = []; const FAKE_HOST_PORT = "Fake host and port"; /** * @typedef {TestCase} * @type Object * @property {String} certFilename * Filename of the cert, or null if we don't want to import a cert for @@ -69,21 +70,29 @@ function openDeleteCertConfirmDialog(tab return new Promise((resolve, reject) => { win.addEventListener("load", function onLoad() { win.removeEventListener("load", onLoad); resolve([win, params]); }); }); } +registerCleanupFunction(() => { + let certdb = Cc["@mozilla.org/security/x509certdb;1"] + .getService(Ci.nsIX509CertDB); + for (let cert of gImportedCerts) { + certdb.deleteCertificate(cert); + } +}); + add_task(function* setup() { for (let testCase of TEST_CASES) { let cert = null; if (testCase.certFilename) { - cert = yield readCertificate(testCase.certFilename, ",,"); + cert = yield readCertificate(testCase.certFilename, ",,", gImportedCerts); } let certTreeItem = { hostPort: FAKE_HOST_PORT, cert: cert, QueryInterface(iid) { if (iid.equals(Ci.nsICertTreeItem)) { return this; }
deleted file mode 100644 --- a/security/manager/ssl/tests/mochitest/browser/browser_editCACertTrust.js +++ /dev/null @@ -1,119 +0,0 @@ -// Any copyright is dedicated to the Public Domain. -// http://creativecommons.org/publicdomain/zero/1.0/ -"use strict"; - -// Tests that the UI for editing the trust of a CA certificate correctly -// reflects trust in the cert DB, and correctly updates trust in the cert DB -// when requested. - -var gCertDB = Cc["@mozilla.org/security/x509certdb;1"] - .getService(Ci.nsIX509CertDB); - -/** - * The cert we're editing the trust of. - * @type nsIX509Cert - */ -var gCert; - -/** - * Opens the cert trust editing dialog. - * - * @returns {Promise} - * A promise that resolves when the dialog has finished loading with - * the window of the opened dialog. - */ -function openEditCertTrustDialog() { - let win = window.openDialog("chrome://pippki/content/editcacert.xul", "", "", - gCert); - return new Promise((resolve, reject) => { - win.addEventListener("load", function onLoad() { - win.removeEventListener("load", onLoad); - resolve(win); - }); - }); -} - -add_task(function* setup() { - // Initially trust ca.pem for SSL, but not e-mail or object signing. - gCert = yield readCertificate("ca.pem", "CT,,"); - Assert.ok(gCertDB.isCertTrusted(gCert, Ci.nsIX509Cert.CA_CERT, - Ci.nsIX509CertDB.TRUSTED_SSL), - "Sanity check: ca.pem should be trusted for SSL"); - Assert.ok(!gCertDB.isCertTrusted(gCert, Ci.nsIX509Cert.CA_CERT, - Ci.nsIX509CertDB.TRUSTED_EMAIL), - "Sanity check: ca.pem should not be trusted for e-mail"); - Assert.ok(!gCertDB.isCertTrusted(gCert, Ci.nsIX509Cert.CA_CERT, - Ci.nsIX509CertDB.TRUSTED_OBJSIGN), - "Sanity check: ca.pem should not be trusted for object signing"); -}); - -// Tests the following: -// 1. The checkboxes correctly reflect the trust set in setup(). -// 2. Accepting the dialog after flipping some of the checkboxes results in the -// correct trust being set in the cert DB. -add_task(function* testAcceptDialog() { - let win = yield openEditCertTrustDialog(); - - let sslCheckbox = win.document.getElementById("trustSSL"); - let emailCheckbox = win.document.getElementById("trustEmail"); - let objSignCheckbox = win.document.getElementById("trustObjSign"); - Assert.ok(sslCheckbox.checked, - "Cert should be trusted for SSL in UI"); - Assert.ok(!emailCheckbox.checked, - "Cert should not be trusted for e-mail in UI"); - Assert.ok(!objSignCheckbox.checked, - "Cert should not be trusted for object signing in UI"); - - sslCheckbox.checked = false; - emailCheckbox.checked = true; - - info("Accepting dialog"); - win.document.getElementById("editCaCert").acceptDialog(); - yield BrowserTestUtils.windowClosed(win); - - Assert.ok(!gCertDB.isCertTrusted(gCert, Ci.nsIX509Cert.CA_CERT, - Ci.nsIX509CertDB.TRUSTED_SSL), - "Cert should no longer be trusted for SSL"); - Assert.ok(gCertDB.isCertTrusted(gCert, Ci.nsIX509Cert.CA_CERT, - Ci.nsIX509CertDB.TRUSTED_EMAIL), - "Cert should now be trusted for e-mail"); - Assert.ok(!gCertDB.isCertTrusted(gCert, Ci.nsIX509Cert.CA_CERT, - Ci.nsIX509CertDB.TRUSTED_OBJSIGN), - "Cert should still not be trusted for object signing"); -}); - -// Tests the following: -// 1. The checkboxes correctly reflect the trust set in testAcceptDialog(). -// 2. Canceling the dialog even after flipping the checkboxes doesn't result in -// a change of trust in the cert DB. -add_task(function* testCancelDialog() { - let win = yield openEditCertTrustDialog(); - - let sslCheckbox = win.document.getElementById("trustSSL"); - let emailCheckbox = win.document.getElementById("trustEmail"); - let objSignCheckbox = win.document.getElementById("trustObjSign"); - Assert.ok(!sslCheckbox.checked, - "Cert should not be trusted for SSL in UI"); - Assert.ok(emailCheckbox.checked, - "Cert should be trusted for e-mail in UI"); - Assert.ok(!objSignCheckbox.checked, - "Cert should not be trusted for object signing in UI"); - - sslCheckbox.checked = true; - emailCheckbox.checked = false; - objSignCheckbox.checked = true; - - info("Canceling dialog"); - win.document.getElementById("editCaCert").cancelDialog(); - yield BrowserTestUtils.windowClosed(win); - - Assert.ok(!gCertDB.isCertTrusted(gCert, Ci.nsIX509Cert.CA_CERT, - Ci.nsIX509CertDB.TRUSTED_SSL), - "Cert should still not be trusted for SSL"); - Assert.ok(gCertDB.isCertTrusted(gCert, Ci.nsIX509Cert.CA_CERT, - Ci.nsIX509CertDB.TRUSTED_EMAIL), - "Cert should still be trusted for e-mail"); - Assert.ok(!gCertDB.isCertTrusted(gCert, Ci.nsIX509Cert.CA_CERT, - Ci.nsIX509CertDB.TRUSTED_OBJSIGN), - "Cert should still not be trusted for object signing"); -});
--- a/security/manager/ssl/tests/mochitest/browser/head.js +++ b/security/manager/ssl/tests/mochitest/browser/head.js @@ -1,59 +1,42 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; -var gCertDB = Cc["@mozilla.org/security/x509certdb;1"] - .getService(Ci.nsIX509CertDB); - -/** - * List of certs imported via readCertificate(). Certs in this list are - * automatically deleted from the cert DB when a test including this head file - * finishes. - * @type nsIX509Cert[] - */ -var gImportedCerts = []; - -registerCleanupFunction(() => { - for (let cert of gImportedCerts) { - gCertDB.deleteCertificate(cert); - } -}); - /** * This function serves the same purpose as the one defined in head_psm.js. */ function pemToBase64(pem) { return pem.replace(/-----BEGIN CERTIFICATE-----/, "") .replace(/-----END CERTIFICATE-----/, "") .replace(/[\r\n]/g, ""); } /** * Given the filename of a certificate, returns a promise that will resolve with * a handle to the certificate when that certificate has been read and imported * with the given trust settings. * - * Certs imported via this function will automatically be deleted from the cert - * DB once the calling test finishes. - * * @param {String} filename * The filename of the certificate (assumed to be in the same directory). * @param {String} trustString * A string describing how the certificate should be trusted (see * `certutil -A --help`). + * @param {nsIX509Cert[]} certificates + * An array to append the imported cert to. Useful for making sure + * imported certs are cleaned up. * @return {Promise} * A promise that will resolve with a handle to the certificate. */ -function readCertificate(filename, trustString) { +function readCertificate(filename, trustString, certificates) { return OS.File.read(getTestFilePath(filename)).then(data => { let decoder = new TextDecoder(); let pem = decoder.decode(data); let certdb = Cc["@mozilla.org/security/x509certdb;1"] .getService(Ci.nsIX509CertDB); let base64 = pemToBase64(pem); certdb.addCertFromBase64(base64, trustString, "unused"); let cert = certdb.constructX509FromBase64(base64); - gImportedCerts.push(cert); + certificates.push(cert); return cert; }, error => { throw error; }); }
--- a/services/sync/modules/bookmark_validator.js +++ b/services/sync/modules/bookmark_validator.js @@ -36,16 +36,18 @@ this.EXPORTED_SYMBOLS = ["BookmarkValida * had deleted parents * - childrenOnNonFolder (array of ids): list of non-folders that still have * children arrays * - duplicateChildren (array of ids): list of records who have the same * child listed multiple times in their children array * - parentNotFolder (array of ids): list of records that have parents that * aren't folders * - rootOnServer (boolean): true if the root came from the server + * - badClientRoots (array of ids): Contains any client-side root ids where + * the root is missing or isn't a (direct) child of the places root. * * - clientMissing: Array of ids on the server missing from the client * - serverMissing: Array of ids on the client missing from the server * - serverDeleted: Array of ids on the client that the server had marked as deleted. * - serverUnexpected: Array of ids that appear on the server but shouldn't * because the client attempts to never upload them. * - differences: Array of {id: string, differences: string array} recording * the non-structural properties that are differente between the client and server @@ -65,16 +67,17 @@ class BookmarkProblemData { this.missingChildren = []; this.deletedChildren = []; this.multipleParents = []; this.deletedParents = []; this.childrenOnNonFolder = []; this.duplicateChildren = []; this.parentNotFolder = []; + this.badClientRoots = []; this.clientMissing = []; this.serverMissing = []; this.serverDeleted = []; this.serverUnexpected = []; this.differences = []; this.structuralDifferences = []; } @@ -117,16 +120,17 @@ class BookmarkProblemData { { name: "missingIDs", count: this.missingIDs }, { name: "rootOnServer", count: this.rootOnServer ? 1 : 0 }, { name: "duplicates", count: this.duplicates.length }, { name: "parentChildMismatches", count: this.parentChildMismatches.length }, { name: "cycles", count: this.cycles.length }, { name: "clientCycles", count: this.clientCycles.length }, + { name: "badClientRoots", count: this.badClientRoots.length }, { name: "orphans", count: this.orphans.length }, { name: "missingChildren", count: this.missingChildren.length }, { name: "deletedChildren", count: this.deletedChildren.length }, { name: "multipleParents", count: this.multipleParents.length }, { name: "deletedParents", count: this.deletedParents.length }, { name: "childrenOnNonFolder", count: this.childrenOnNonFolder.length }, { name: "duplicateChildren", count: this.duplicateChildren.length }, { name: "parentNotFolder", count: this.parentNotFolder.length }, @@ -553,16 +557,34 @@ class BookmarkValidator { if (!seenEver.has(record)) { traverse(record); } } return cycles; } + // Perform client-side sanity checking that doesn't involve server data + _validateClient(problemData, clientRecords) { + problemData.clientCycles = this._detectCycles(clientRecords); + const rootsToCheck = [ + PlacesUtils.bookmarks.menuGuid, + PlacesUtils.bookmarks.toolbarGuid, + PlacesUtils.bookmarks.unfiledGuid, + PlacesUtils.bookmarks.mobileGuid, + ]; + for (let rootGUID of rootsToCheck) { + let record = clientRecords.find(record => + record.guid === rootGUID); + if (!record || record.parentid !== "places") { + problemData.badClientRoots.push(rootGUID); + } + } + } + /** * Compare the list of server records with the client tree. * * Returns the same data as described in the inspectServerRecords comment, * with the following additional fields. * - clientRecords: an array of client records in a similar format to * the .records (ie, server records) entry. * - problemData is the same as for inspectServerRecords, except all properties @@ -573,17 +595,17 @@ class BookmarkValidator { let clientRecords = this.createClientRecordsFromTree(clientTree); let inspectionInfo = this.inspectServerRecords(serverRecords); inspectionInfo.clientRecords = clientRecords; // Mainly do this to remove deleted items and normalize child guids. serverRecords = inspectionInfo.records; let problemData = inspectionInfo.problemData; - problemData.clientCycles = this._detectCycles(clientRecords); + this._validateClient(problemData, clientRecords); let matches = []; let allRecords = new Map(); let serverDeletedLookup = new Set(inspectionInfo.deletedRecords.map(r => r.id)); for (let sr of serverRecords) { allRecords.set(sr.id, {client: null, server: sr});
--- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -47,16 +47,36 @@ const ALLBOOKMARKS_ANNO = "AllBookmarks" const MOBILE_ANNO = "MobileBookmarks"; // The tracker ignores changes made by bookmark import and restore, and // changes made by Sync. We don't need to exclude `SOURCE_IMPORT`, but both // import and restore fire `bookmarks-restore-*` observer notifications, and // the tracker doesn't currently distinguish between the two. const IGNORED_SOURCES = [SOURCE_SYNC, SOURCE_IMPORT, SOURCE_IMPORT_REPLACE]; +// Returns the constructor for a bookmark record type. +function getTypeObject(type) { + switch (type) { + case "bookmark": + case "microsummary": + return Bookmark; + case "query": + return BookmarkQuery; + case "folder": + return BookmarkFolder; + case "livemark": + return Livemark; + case "separator": + return BookmarkSeparator; + case "item": + return PlacesItem; + } + return null; +} + this.PlacesItem = function PlacesItem(collection, id, type) { CryptoWrapper.call(this, collection, id); this.type = type || "item"; } PlacesItem.prototype = { decrypt: function PlacesItem_decrypt(keyBundle) { // Do the normal CryptoWrapper decrypt, but change types before returning let clear = CryptoWrapper.prototype.decrypt.call(this, keyBundle); @@ -64,46 +84,42 @@ PlacesItem.prototype = { // Convert the abstract places item to the actual object type if (!this.deleted) this.__proto__ = this.getTypeObject(this.type).prototype; return clear; }, getTypeObject: function PlacesItem_getTypeObject(type) { - switch (type) { - case "bookmark": - case "microsummary": - return Bookmark; - case "query": - return BookmarkQuery; - case "folder": - return BookmarkFolder; - case "livemark": - return Livemark; - case "separator": - return BookmarkSeparator; - case "item": - return PlacesItem; + let recordObj = getTypeObject(type); + if (!recordObj) { + throw new Error("Unknown places item object type: " + type); } - throw "Unknown places item object type: " + type; + return recordObj; }, __proto__: CryptoWrapper.prototype, _logName: "Sync.Record.PlacesItem", // Converts the record to a Sync bookmark object that can be passed to // `PlacesSyncUtils.bookmarks.{insert, update}`. toSyncBookmark() { return { kind: this.type, syncId: this.id, parentSyncId: this.parentid, }; }, + + // Populates the record from a Sync bookmark object returned from + // `PlacesSyncUtils.bookmarks.fetch`. + fromSyncBookmark(item) { + this.parentid = item.parentSyncId; + this.parentName = item.parentTitle; + }, }; Utils.deferGetSet(PlacesItem, "cleartext", ["hasDupe", "parentid", "parentName", "type"]); this.Bookmark = function Bookmark(collection, id, type) { PlacesItem.call(this, collection, id, type || "bookmark"); @@ -117,16 +133,26 @@ Bookmark.prototype = { info.title = this.title; info.url = this.bmkUri; info.description = this.description; info.loadInSidebar = this.loadInSidebar; info.tags = this.tags; info.keyword = this.keyword; return info; }, + + fromSyncBookmark(item) { + PlacesItem.prototype.fromSyncBookmark.call(this, item); + this.title = item.title; + this.bmkUri = item.url.href; + this.description = item.description; + this.loadInSidebar = item.loadInSidebar; + this.tags = item.tags; + this.keyword = item.keyword; + }, }; Utils.deferGetSet(Bookmark, "cleartext", ["title", "bmkUri", "description", "loadInSidebar", "tags", "keyword"]); this.BookmarkQuery = function BookmarkQuery(collection, id) { @@ -137,16 +163,22 @@ BookmarkQuery.prototype = { _logName: "Sync.Record.BookmarkQuery", toSyncBookmark() { let info = Bookmark.prototype.toSyncBookmark.call(this); info.folder = this.folderName; info.query = this.queryId; return info; }, + + fromSyncBookmark(item) { + Bookmark.prototype.fromSyncBookmark.call(this, item); + this.folderName = item.folder; + this.queryId = item.query; + }, }; Utils.deferGetSet(BookmarkQuery, "cleartext", ["folderName", "queryId"]); this.BookmarkFolder = function BookmarkFolder(collection, id, type) { PlacesItem.call(this, collection, id, type || "folder"); @@ -156,16 +188,23 @@ BookmarkFolder.prototype = { _logName: "Sync.Record.Folder", toSyncBookmark() { let info = PlacesItem.prototype.toSyncBookmark.call(this); info.description = this.description; info.title = this.title; return info; }, + + fromSyncBookmark(item) { + PlacesItem.prototype.fromSyncBookmark.call(this, item); + this.title = item.title; + this.description = item.description; + this.children = item.childSyncIds; + }, }; Utils.deferGetSet(BookmarkFolder, "cleartext", ["description", "title", "children"]); this.Livemark = function Livemark(collection, id) { BookmarkFolder.call(this, collection, id, "livemark"); } @@ -174,26 +213,39 @@ Livemark.prototype = { _logName: "Sync.Record.Livemark", toSyncBookmark() { let info = BookmarkFolder.prototype.toSyncBookmark.call(this); info.feed = this.feedUri; info.site = this.siteUri; return info; }, + + fromSyncBookmark(item) { + BookmarkFolder.prototype.fromSyncBookmark.call(this, item); + this.feedUri = item.feed.href; + if (item.site) { + this.siteUri = item.site.href; + } + }, }; Utils.deferGetSet(Livemark, "cleartext", ["siteUri", "feedUri"]); this.BookmarkSeparator = function BookmarkSeparator(collection, id) { PlacesItem.call(this, collection, id, "separator"); } BookmarkSeparator.prototype = { __proto__: PlacesItem.prototype, _logName: "Sync.Record.Separator", + + fromSyncBookmark(item) { + PlacesItem.prototype.fromSyncBookmark.call(this, item); + this.pos = item.index; + }, }; Utils.deferGetSet(BookmarkSeparator, "cleartext", "pos"); this.BookmarksEngine = function BookmarksEngine(service) { SyncEngine.call(this, "Bookmarks", service); } BookmarksEngine.prototype = { @@ -711,131 +763,33 @@ BookmarksStore.prototype = { }, changeItemID: function BStore_changeItemID(oldID, newID) { this._log.debug("Changing GUID " + oldID + " to " + newID); Async.promiseSpinningly(PlacesSyncUtils.bookmarks.changeGuid(oldID, newID)); }, - _getTags: function BStore__getTags(uri) { - try { - if (typeof(uri) == "string") - uri = Utils.makeURI(uri); - } catch(e) { - this._log.warn("Could not parse URI \"" + uri + "\": " + e); - } - return PlacesUtils.tagging.getTagsForURI(uri, {}); - }, - - _getDescription: function BStore__getDescription(id) { - try { - return PlacesUtils.annotations.getItemAnnotation(id, - PlacesSyncUtils.bookmarks.DESCRIPTION_ANNO); - } catch (e) { - return null; - } - }, - - _isLoadInSidebar: function BStore__isLoadInSidebar(id) { - return PlacesUtils.annotations.itemHasAnnotation(id, - PlacesSyncUtils.bookmarks.SIDEBAR_ANNO); - }, - // Create a record starting from the weave id (places guid) createRecord: function createRecord(id, collection) { - let placeId = this.idForGUID(id); - let record; - if (placeId <= 0) { // deleted item - record = new PlacesItem(collection, id); + let item = Async.promiseSpinningly(PlacesSyncUtils.bookmarks.fetch(id)); + if (!item) { // deleted item + let record = new PlacesItem(collection, id); record.deleted = true; return record; } - let parent = PlacesUtils.bookmarks.getFolderIdForItem(placeId); - switch (PlacesUtils.bookmarks.getItemType(placeId)) { - case PlacesUtils.bookmarks.TYPE_BOOKMARK: - let bmkUri = PlacesUtils.bookmarks.getBookmarkURI(placeId).spec; - if (bmkUri.indexOf("place:") == 0) { - record = new BookmarkQuery(collection, id); - - // Get the actual tag name instead of the local itemId - let folder = bmkUri.match(/[:&]folder=(\d+)/); - try { - // There might not be the tag yet when creating on a new client - if (folder != null) { - folder = folder[1]; - record.folderName = PlacesUtils.bookmarks.getItemTitle(folder); - this._log.trace("query id: " + folder + " = " + record.folderName); - } - } - catch(ex) {} - - // Persist the Smart Bookmark anno, if found. - try { - let anno = PlacesUtils.annotations.getItemAnnotation(placeId, - PlacesSyncUtils.bookmarks.SMART_BOOKMARKS_ANNO); - if (anno != null) { - this._log.trace("query anno: " + - PlacesSyncUtils.bookmarks.SMART_BOOKMARKS_ANNO + - " = " + anno); - record.queryId = anno; - } - } - catch(ex) {} - } - else { - record = new Bookmark(collection, id); - } - record.title = PlacesUtils.bookmarks.getItemTitle(placeId); + let recordObj = getTypeObject(item.kind); + if (!recordObj) { + this._log.warn("Unknown item type, cannot serialize: " + item.kind); + recordObj = PlacesItem; + } + let record = new recordObj(collection, id); + record.fromSyncBookmark(item); - record.parentName = PlacesUtils.bookmarks.getItemTitle(parent); - record.bmkUri = bmkUri; - record.tags = this._getTags(record.bmkUri); - record.keyword = PlacesUtils.bookmarks.getKeywordForBookmark(placeId); - record.description = this._getDescription(placeId); - record.loadInSidebar = this._isLoadInSidebar(placeId); - break; - - case PlacesUtils.bookmarks.TYPE_FOLDER: - if (PlacesUtils.annotations - .itemHasAnnotation(placeId, PlacesUtils.LMANNO_FEEDURI)) { - record = new Livemark(collection, id); - let as = PlacesUtils.annotations; - record.feedUri = as.getItemAnnotation(placeId, PlacesUtils.LMANNO_FEEDURI); - try { - record.siteUri = as.getItemAnnotation(placeId, PlacesUtils.LMANNO_SITEURI); - } catch (ex) {} - } else { - record = new BookmarkFolder(collection, id); - } - - if (parent > 0) - record.parentName = PlacesUtils.bookmarks.getItemTitle(parent); - record.title = PlacesUtils.bookmarks.getItemTitle(placeId); - record.description = this._getDescription(placeId); - record.children = Async.promiseSpinningly( - PlacesSyncUtils.bookmarks.fetchChildSyncIds(id)); - break; - - case PlacesUtils.bookmarks.TYPE_SEPARATOR: - record = new BookmarkSeparator(collection, id); - if (parent > 0) - record.parentName = PlacesUtils.bookmarks.getItemTitle(parent); - // Create a positioning identifier for the separator, used by _mapDupe - record.pos = PlacesUtils.bookmarks.getItemIndex(placeId); - break; - - default: - record = new PlacesItem(collection, id); - this._log.warn("Unknown item type, cannot serialize: " + - PlacesUtils.bookmarks.getItemType(placeId)); - } - - record.parentid = this.GUIDForId(parent); record.sortindex = this._calculateIndex(record); return record; }, _stmts: {}, _getStmt: function(query) { if (query in this._stmts) { @@ -1138,19 +1092,19 @@ BookmarksTracker.prototype = { } // Make sure the existing title is correct else { let queryTitle = PlacesUtils.bookmarks.getItemTitle(mobile[0]); if (queryTitle != title) { PlacesUtils.bookmarks.setItemTitle(mobile[0], title, SOURCE_SYNC); } let rootTitle = - PlacesUtils.bookmarks.getItemTitle(BookmarkSpecialIds.mobile); + PlacesUtils.bookmarks.getItemTitle(PlacesUtils.mobileFolderId); if (rootTitle != title) { - PlacesUtils.bookmarks.setItemTitle(BookmarkSpecialIds.mobile, title, + PlacesUtils.bookmarks.setItemTitle(PlacesUtils.mobileFolderId, title, SOURCE_SYNC); } } }, // This method is oddly structured, but the idea is to return as quickly as // possible -- this handler gets called *every time* a bookmark changes, for // *each change*.
--- a/services/sync/tests/unit/test_bookmark_tracker.js +++ b/services/sync/tests/unit/test_bookmark_tracker.js @@ -4,16 +4,17 @@ Cu.import("resource://gre/modules/PlacesUtils.jsm"); Cu.import("resource://gre/modules/PlacesSyncUtils.jsm"); Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://services-sync/constants.js"); Cu.import("resource://services-sync/engines/bookmarks.js"); Cu.import("resource://services-sync/engines.js"); Cu.import("resource://services-sync/service.js"); Cu.import("resource://services-sync/util.js"); +Cu.import("resource:///modules/PlacesUIUtils.jsm"); Service.engineManager.register(BookmarksEngine); var engine = Service.engineManager.get("bookmarks"); var store = engine._store; var tracker = engine._tracker; store.wipe(); tracker.persistChangedIDs = false; @@ -61,16 +62,23 @@ function* verifyTrackedItems(tracked) { JSON.stringify(Array.from(trackedIDs))}`); } function* verifyTrackedCount(expected) { let changes = engine.pullNewChanges(); equal(changes.count(), expected); } +// Copied from PlacesSyncUtils.jsm. +function findAnnoItems(anno, val) { + let annos = PlacesUtils.annotations; + return annos.getItemsWithAnnotation(anno, {}).filter(id => + annos.getItemAnnotation(id, anno) == val); +} + add_task(function* test_tracking() { _("Test starting and stopping the tracker"); let folder = PlacesUtils.bookmarks.createFolder( PlacesUtils.bookmarks.bookmarksMenuFolder, "Test Folder", PlacesUtils.bookmarks.DEFAULT_INDEX); function createBmk() { return PlacesUtils.bookmarks.insertBookmark( @@ -1428,16 +1436,74 @@ add_task(function* test_onItemDeleted_tr yield verifyTrackedItems([fx_guid, tb_guid, folder1_guid, folder2_guid]); do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 6); } finally { _("Clean up."); yield cleanup(); } }); +add_task(function* test_mobile_query() { + _("Ensure we correctly create the mobile query"); + + try { + // Creates the organizer queries as a side effect. + let leftPaneId = PlacesUIUtils.leftPaneFolderId; + _(`Left pane root ID: ${leftPaneId}`); + + let allBookmarksIds = findAnnoItems("PlacesOrganizer/OrganizerQuery", "AllBookmarks"); + equal(allBookmarksIds.length, 1, "Should create folder with all bookmarks queries"); + let allBookmarkGuid = yield PlacesUtils.promiseItemGuid(allBookmarksIds[0]); + + _("Try creating query after organizer is ready"); + tracker._ensureMobileQuery(); + let queryIds = findAnnoItems("PlacesOrganizer/OrganizerQuery", "MobileBookmarks"); + equal(queryIds.length, 0, "Should not create query without any mobile bookmarks"); + + _("Insert mobile bookmark, then create query"); + yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.mobileGuid, + url: "https://mozilla.org", + }); + tracker._ensureMobileQuery(); + queryIds = findAnnoItems("PlacesOrganizer/OrganizerQuery", "MobileBookmarks", {}); + equal(queryIds.length, 1, "Should create query once mobile bookmarks exist"); + + let queryId = queryIds[0]; + let queryGuid = yield PlacesUtils.promiseItemGuid(queryId); + + let queryInfo = yield PlacesUtils.bookmarks.fetch(queryGuid); + equal(queryInfo.url, `place:folder=${PlacesUtils.mobileFolderId}`, "Query should point to mobile root"); + equal(queryInfo.title, "Mobile Bookmarks", "Query title should be localized"); + equal(queryInfo.parentGuid, allBookmarkGuid, "Should append mobile query to all bookmarks queries"); + + _("Rename root and query, then recreate"); + yield PlacesUtils.bookmarks.update({ + guid: PlacesUtils.bookmarks.mobileGuid, + title: "renamed root", + }); + yield PlacesUtils.bookmarks.update({ + guid: queryGuid, + title: "renamed query", + }); + tracker._ensureMobileQuery(); + let rootInfo = yield PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.mobileGuid); + equal(rootInfo.title, "Mobile Bookmarks", "Should fix root title"); + queryInfo = yield PlacesUtils.bookmarks.fetch(queryGuid); + equal(queryInfo.title, "Mobile Bookmarks", "Should fix query title"); + + _("We shouldn't track the query or the left pane root"); + yield verifyTrackedCount(0); + do_check_eq(tracker.score, 0); + } finally { + _("Clean up."); + yield cleanup(); + } +}); + function run_test() { initTestLogging("Trace"); Log.repository.getLogger("Sync.Engine.Bookmarks").level = Log.Level.Trace; Log.repository.getLogger("Sync.Store.Bookmarks").level = Log.Level.Trace; Log.repository.getLogger("Sync.Tracker.Bookmarks").level = Log.Level.Trace; run_next_test();
--- a/services/sync/tests/unit/test_bookmark_validator.js +++ b/services/sync/tests/unit/test_bookmark_validator.js @@ -266,16 +266,17 @@ add_task(function *test_telemetry_integr let bme = ping.engines.find(e => e.name === "bookmarks"); ok(bme); ok(bme.validation); ok(bme.validation.problems) equal(bme.validation.checked, server.length); equal(bme.validation.took, duration); bme.validation.problems.sort((a, b) => String.localeCompare(a.name, b.name)); deepEqual(bme.validation.problems, [ + { name: "badClientRoots", count: 4 }, { name: "sdiff:childGUIDs", count: 1 }, { name: "serverMissing", count: 1 }, { name: "structuralDifferences", count: 1 }, ]); }); function run_test() { run_next_test();
--- a/toolkit/components/places/PlacesSyncUtils.jsm +++ b/toolkit/components/places/PlacesSyncUtils.jsm @@ -259,16 +259,93 @@ const BookmarkSyncUtils = PlacesSyncUtil * @resolves to an object representing the created bookmark. * @rejects if it's not possible to create the requested bookmark. * @throws if the arguments are invalid. */ insert: Task.async(function* (info) { let insertInfo = validateNewBookmark(info); return insertSyncBookmark(insertInfo); }), + + /** + * Fetches a Sync bookmark object for an item in the tree. The object contains + * the following properties, depending on the item's kind: + * + * - kind (all): A string representing the item's kind. + * - syncId (all): The item's sync ID. + * - parentSyncId (all): The sync ID of the item's parent. + * - parentTitle (all): The title of the item's parent, used for de-duping. + * Omitted for the Places root and parents with empty titles. + * - title ("bookmark", "folder", "livemark", "query"): The item's title. + * Omitted if empty. + * - url ("bookmark", "query"): The item's URL. + * - tags ("bookmark", "query"): An array containing the item's tags. + * - keyword ("bookmark"): The bookmark's keyword, if one exists. + * - description ("bookmark", "folder", "livemark"): The item's description. + * Omitted if one isn't set. + * - loadInSidebar ("bookmark", "query"): Whether to load the bookmark in + * the sidebar. Always `false` for queries. + * - feed ("livemark"): A `URL` object pointing to the livemark's feed URL. + * - site ("livemark"): A `URL` object pointing to the livemark's site URL, + * or `null` if one isn't set. + * - childSyncIds ("folder"): An array containing the sync IDs of the item's + * children, used to determine child order. + * - folder ("query"): The tag folder name, if this is a tag query. + * - query ("query"): The smart bookmark query name, if this is a smart + * bookmark. + * - index ("separator"): The separator's position within its parent. + */ + fetch: Task.async(function* (syncId) { + let guid = BookmarkSyncUtils.syncIdToGuid(syncId); + let bookmarkItem = yield PlacesUtils.bookmarks.fetch(guid); + if (!bookmarkItem) { + return null; + } + + // Convert the Places bookmark object to a Sync bookmark and add + // kind-specific properties. + let kind = yield getKindForItem(bookmarkItem); + let item; + switch (kind) { + case BookmarkSyncUtils.KINDS.BOOKMARK: + case BookmarkSyncUtils.KINDS.MICROSUMMARY: + item = yield fetchBookmarkItem(bookmarkItem); + break; + + case BookmarkSyncUtils.KINDS.QUERY: + item = yield fetchQueryItem(bookmarkItem); + break; + + case BookmarkSyncUtils.KINDS.FOLDER: + item = yield fetchFolderItem(bookmarkItem); + break; + + case BookmarkSyncUtils.KINDS.LIVEMARK: + item = yield fetchLivemarkItem(bookmarkItem); + break; + + case BookmarkSyncUtils.KINDS.SEPARATOR: + item = yield placesBookmarkToSyncBookmark(bookmarkItem); + item.index = bookmarkItem.index; + break; + + default: + throw new Error(`Unknown bookmark kind: ${kind}`); + } + + // Sync uses the parent title for de-duping. + if (bookmarkItem.parentGuid) { + let parent = yield PlacesUtils.bookmarks.fetch(bookmarkItem.parentGuid); + if ("title" in parent) { + item.parentTitle = parent.title; + } + } + + return item; + }), }); XPCOMUtils.defineLazyGetter(this, "BookmarkSyncLog", () => { return Log.repository.getLogger("BookmarkSyncUtils"); }); function validateSyncBookmarkObject(input, behavior) { return PlacesUtils.validateItemProperties( @@ -967,8 +1044,134 @@ function syncBookmarkToPlacesBookmark(in bookmarkInfo.siteURI = PlacesUtils.toURI(info.site); } break; } } return bookmarkInfo; } + +// Creates and returns a Sync bookmark object containing the bookmark's +// tags, keyword, description, and whether it loads in the sidebar. +var fetchBookmarkItem = Task.async(function* (bookmarkItem) { + let itemId = yield PlacesUtils.promiseItemId(bookmarkItem.guid); + let item = yield placesBookmarkToSyncBookmark(bookmarkItem); + + item.tags = PlacesUtils.tagging.getTagsForURI( + PlacesUtils.toURI(bookmarkItem.url), {}); + + let keywordEntry = yield PlacesUtils.keywords.fetch({ + url: bookmarkItem.url, + }); + if (keywordEntry) { + item.keyword = keywordEntry.keyword; + } + + let description = getItemDescription(itemId); + if (description) { + item.description = description; + } + + item.loadInSidebar = PlacesUtils.annotations.itemHasAnnotation(itemId, + BookmarkSyncUtils.SIDEBAR_ANNO); + + return item; +}); + +// Creates and returns a Sync bookmark object containing the folder's +// description and children. +var fetchFolderItem = Task.async(function* (bookmarkItem) { + let itemId = yield PlacesUtils.promiseItemId(bookmarkItem.guid); + let item = yield placesBookmarkToSyncBookmark(bookmarkItem); + + let description = getItemDescription(itemId); + if (description) { + item.description = description; + } + + let db = yield PlacesUtils.promiseDBConnection(); + let children = yield fetchAllChildren(db, bookmarkItem.guid); + item.childSyncIds = children.map(child => + BookmarkSyncUtils.guidToSyncId(child.guid) + ); + + return item; +}); + +// Creates and returns a Sync bookmark object containing the livemark's +// description, children (none), feed URI, and site URI. +var fetchLivemarkItem = Task.async(function* (bookmarkItem) { + let itemId = yield PlacesUtils.promiseItemId(bookmarkItem.guid); + let item = yield placesBookmarkToSyncBookmark(bookmarkItem); + + let description = getItemDescription(itemId); + if (description) { + item.description = description; + } + + let feedAnno = PlacesUtils.annotations.getItemAnnotation(itemId, + PlacesUtils.LMANNO_FEEDURI); + item.feed = new URL(feedAnno); + + let siteAnno = null; + try { + siteAnno = PlacesUtils.annotations.getItemAnnotation(itemId, + PlacesUtils.LMANNO_SITEURI); + } catch (ex) {} + if (siteAnno != null) { + item.site = new URL(siteAnno); + } + + return item; +}); + +// Creates and returns a Sync bookmark object containing the query's tag +// folder name and smart bookmark query ID. +var fetchQueryItem = Task.async(function* (bookmarkItem) { + let itemId = yield PlacesUtils.promiseItemId(bookmarkItem.guid); + let item = yield placesBookmarkToSyncBookmark(bookmarkItem); + + let description = getItemDescription(itemId); + if (description) { + item.description = description; + } + + let folder = null; + let params = new URLSearchParams(bookmarkItem.url.pathname); + let tagFolderId = +params.get("folder"); + if (tagFolderId) { + try { + let tagFolderGuid = yield PlacesUtils.promiseItemGuid(tagFolderId); + let tagFolder = yield PlacesUtils.bookmarks.fetch(tagFolderGuid); + folder = tagFolder.title; + } catch (ex) { + BookmarkSyncLog.warn("fetchQueryItem: Query " + bookmarkItem.url.href + + " points to nonexistent folder " + tagFolderId, ex); + } + } + if (folder != null) { + item.folder = folder; + } + + let query = null; + try { + // Throws if the bookmark doesn't have the smart bookmark anno. + query = PlacesUtils.annotations.getItemAnnotation(itemId, + BookmarkSyncUtils.SMART_BOOKMARKS_ANNO); + } catch (ex) {} + if (query != null) { + item.query = query; + } + + return item; +}); + +// Returns an item's description, or `null` if one isn't set. +function getItemDescription(id) { + try { + return PlacesUtils.annotations.getItemAnnotation(id, + BookmarkSyncUtils.DESCRIPTION_ANNO); + } catch (ex) {} + return null; +} + +
--- a/toolkit/components/places/tests/unit/test_sync_utils.js +++ b/toolkit/components/places/tests/unit/test_sync_utils.js @@ -338,23 +338,20 @@ add_task(function* test_update_keyword() yield PlacesUtils.bookmarks.eraseEverything(); }); add_task(function* test_update_annos() { let guids = yield populateTree(PlacesUtils.bookmarks.menuGuid, { kind: "folder", title: "folder", - description: "Folder description", }, { kind: "bookmark", title: "bmk", url: "https://example.com", - description: "Bookmark description", - loadInSidebar: true, }); do_print("Add folder description"); { let updatedItem = yield PlacesSyncUtils.bookmarks.update({ syncId: guids.folder, description: "Folder description", }); @@ -999,8 +996,150 @@ add_task(function* test_insert_orphans() let child = yield PlacesUtils.bookmarks.fetch({ guid: childGuid }); equal(child.parentGuid, parentGuid, "Should reparent child after inserting missing parent"); } yield PlacesUtils.bookmarks.eraseEverything(); }); + +add_task(function* test_fetch() { + let folder = yield PlacesSyncUtils.bookmarks.insert({ + syncId: makeGuid(), + parentSyncId: "menu", + kind: "folder", + description: "Folder description", + }); + let bmk = yield PlacesSyncUtils.bookmarks.insert({ + syncId: makeGuid(), + parentSyncId: "menu", + kind: "bookmark", + url: "https://example.com", + description: "Bookmark description", + loadInSidebar: true, + tags: ["taggy"], + }); + let folderBmk = yield PlacesSyncUtils.bookmarks.insert({ + syncId: makeGuid(), + parentSyncId: folder.syncId, + kind: "bookmark", + url: "https://example.org", + keyword: "kw", + }); + let folderSep = yield PlacesSyncUtils.bookmarks.insert({ + syncId: makeGuid(), + parentSyncId: folder.syncId, + kind: "separator", + }); + let tagQuery = yield PlacesSyncUtils.bookmarks.insert({ + kind: "query", + syncId: makeGuid(), + parentSyncId: "toolbar", + url: "place:type=7&folder=90", + folder: "taggy", + title: "Tagged stuff", + }); + let [, tagFolderId] = /\bfolder=(\d+)\b/.exec(tagQuery.url.pathname); + let smartBmk = yield PlacesSyncUtils.bookmarks.insert({ + kind: "query", + syncId: makeGuid(), + parentSyncId: "toolbar", + url: "place:folder=TOOLBAR", + query: "BookmarksToolbar", + title: "Bookmarks toolbar query", + }); + + do_print("Fetch empty folder with description"); + { + let item = yield PlacesSyncUtils.bookmarks.fetch(folder.syncId); + deepEqual(item, { + syncId: folder.syncId, + kind: "folder", + parentSyncId: "menu", + description: "Folder description", + childSyncIds: [folderBmk.syncId, folderSep.syncId], + parentTitle: "Bookmarks Menu", + }, "Should include description, children, and parent title in folder"); + } + + do_print("Fetch bookmark with description, sidebar anno, and tags"); + { + let item = yield PlacesSyncUtils.bookmarks.fetch(bmk.syncId); + deepEqual(Object.keys(item).sort(), ["syncId", "kind", "parentSyncId", "url", + "tags", "description", "loadInSidebar", "parentTitle"].sort(), + "Should include bookmark-specific properties"); + equal(item.syncId, bmk.syncId, "Sync ID should match"); + equal(item.url.href, "https://example.com/", "Should return URL"); + equal(item.parentSyncId, "menu", "Should return parent sync ID"); + deepEqual(item.tags, ["taggy"], "Should return tags"); + equal(item.description, "Bookmark description", "Should return bookmark description"); + strictEqual(item.loadInSidebar, true, "Should return sidebar anno"); + equal(item.parentTitle, "Bookmarks Menu", "Should return parent title"); + } + + do_print("Fetch bookmark with keyword; without parent title or annos"); + { + let item = yield PlacesSyncUtils.bookmarks.fetch(folderBmk.syncId); + deepEqual(Object.keys(item).sort(), ["syncId", "kind", "parentSyncId", + "url", "keyword", "tags", "loadInSidebar"].sort(), + "Should omit blank bookmark-specific properties"); + strictEqual(item.loadInSidebar, false, "Should not load bookmark in sidebar"); + deepEqual(item.tags, [], "Tags should be empty"); + equal(item.keyword, "kw", "Should return keyword"); + } + + do_print("Fetch separator"); + { + let item = yield PlacesSyncUtils.bookmarks.fetch(folderSep.syncId); + strictEqual(item.index, 1, "Should return separator position"); + } + + do_print("Fetch tag query"); + { + let item = yield PlacesSyncUtils.bookmarks.fetch(tagQuery.syncId); + deepEqual(Object.keys(item).sort(), ["syncId", "kind", "parentSyncId", + "url", "title", "folder", "parentTitle"].sort(), + "Should include query-specific properties"); + equal(item.url.href, `place:type=7&folder=${tagFolderId}`, "Should not rewrite outgoing tag queries"); + equal(item.folder, "taggy", "Should return tag name for tag queries"); + } + + do_print("Fetch smart bookmark"); + { + let item = yield PlacesSyncUtils.bookmarks.fetch(smartBmk.syncId); + deepEqual(Object.keys(item).sort(), ["syncId", "kind", "parentSyncId", + "url", "title", "query", "parentTitle"].sort(), + "Should include smart bookmark-specific properties"); + equal(item.query, "BookmarksToolbar", "Should return query name for smart bookmarks"); + } + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_fetch_livemark() { + let { server, site, stopServer } = makeLivemarkServer(); + + try { + do_print("Create livemark"); + let livemark = yield PlacesUtils.livemarks.addLivemark({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + feedURI: uri(site + "/feed/1"), + siteURI: uri(site), + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + }); + PlacesUtils.annotations.setItemAnnotation(livemark.id, DESCRIPTION_ANNO, + "Livemark description", 0, PlacesUtils.annotations.EXPIRE_NEVER); + + do_print("Fetch livemark"); + let item = yield PlacesSyncUtils.bookmarks.fetch(livemark.guid); + deepEqual(Object.keys(item).sort(), ["syncId", "kind", "parentSyncId", + "description", "feed", "site", "parentTitle"].sort(), + "Should include livemark-specific properties"); + equal(item.description, "Livemark description", "Should return description"); + equal(item.feed.href, site + "/feed/1", "Should return feed URL"); + equal(item.site.href, site + "/", "Should return site URL"); + } finally { + yield stopServer(); + } + + yield PlacesUtils.bookmarks.eraseEverything(); +});
--- a/toolkit/components/thumbnails/BackgroundPageThumbs.jsm +++ b/toolkit/components/thumbnails/BackgroundPageThumbs.jsm @@ -10,16 +10,18 @@ const DEFAULT_CAPTURE_TIMEOUT = 30000; / const DESTROY_BROWSER_TIMEOUT = 60000; // ms const FRAME_SCRIPT_URL = "chrome://global/content/backgroundPageThumbsContent.js"; const TELEMETRY_HISTOGRAM_ID_PREFIX = "FX_THUMBNAILS_BG_"; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; const HTML_NS = "http://www.w3.org/1999/xhtml"; +const ABOUT_NEWTAB_SEGREGATION_PREF = "privacy.usercontext.about_newtab_segregation.enabled"; + const { classes: Cc, interfaces: Ci, utils: Cu } = Components; Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); Cu.import("resource://gre/modules/PageThumbs.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/Task.jsm"); // possible FX_THUMBNAILS_BG_CAPTURE_DONE_REASON_2 telemetry values @@ -191,20 +193,22 @@ const BackgroundPageThumbs = { if (this._thumbBrowser) return; let browser = this._parentWin.document.createElementNS(XUL_NS, "browser"); browser.setAttribute("type", "content"); browser.setAttribute("remote", "true"); browser.setAttribute("disableglobalhistory", "true"); - // Use the private container for thumbnails. - let privateIdentity = - ContextualIdentityService.getPrivateIdentity("userContextIdInternal.thumbnail"); - browser.setAttribute("usercontextid", privateIdentity.userContextId); + if (Services.prefs.getBoolPref(ABOUT_NEWTAB_SEGREGATION_PREF)) { + // Use the private container for thumbnails. + let privateIdentity = + ContextualIdentityService.getPrivateIdentity("userContextIdInternal.thumbnail"); + browser.setAttribute("usercontextid", privateIdentity.userContextId); + } // Size the browser. Make its aspect ratio the same as the canvases' that // the thumbnails are drawn into; the canvases' aspect ratio is the same as // the screen's, so use that. Aim for a size in the ballpark of 1024x768. let [swidth, sheight] = [{}, {}]; Cc["@mozilla.org/gfx/screenmanager;1"]. getService(Ci.nsIScreenManager). primaryScreen. @@ -430,21 +434,23 @@ Capture.prototype = { try { callback.call(options, this.url); } catch (err) { Cu.reportError(err); } } - // Clear the data in the private container for thumbnails. - let privateIdentity = - ContextualIdentityService.getPrivateIdentity("userContextIdInternal.thumbnail"); - Services.obs.notifyObservers(null, "clear-origin-attributes-data", + if (Services.prefs.getBoolPref(ABOUT_NEWTAB_SEGREGATION_PREF)) { + // Clear the data in the private container for thumbnails. + let privateIdentity = + ContextualIdentityService.getPrivateIdentity("userContextIdInternal.thumbnail"); + Services.obs.notifyObservers(null, "clear-origin-attributes-data", JSON.stringify({ userContextId: privateIdentity.userContextId })); + } }; if (!data) { done(); return; } PageThumbs._store(this.url, data.finalURL, data.imageData, true)
--- a/toolkit/components/url-classifier/RiceDeltaDecoder.cpp +++ b/toolkit/components/url-classifier/RiceDeltaDecoder.cpp @@ -20,29 +20,19 @@ namespace { * project authors may be found in the AUTHORS file in the root of * the source tree. */ class BitBuffer { public: BitBuffer(const uint8_t* bytes, size_t byte_count); - // Gets the current offset, in bytes/bits, from the start of the buffer. The - // bit offset is the offset into the current byte, in the range [0,7]. - void GetCurrentOffset(size_t* out_byte_offset, size_t* out_bit_offset); - // The remaining bits in the byte buffer. uint64_t RemainingBitCount() const; - // Reads byte-sized values from the buffer. Returns false if there isn't - // enough data left for the specified type. - bool ReadUInt8(uint8_t* val); - bool ReadUInt16(uint16_t* val); - bool ReadUInt32(uint32_t* val); - // Reads bit-sized values from the buffer. Returns false if there isn't enough // data left for the specified bit count.. bool ReadBits(uint32_t* val, size_t bit_count); // Peeks bit-sized values from the buffer. Returns false if there isn't enough // data left for the specified number of bits. Doesn't move the current // offset. bool PeekBits(uint32_t* val, size_t bit_count); @@ -51,32 +41,21 @@ class BitBuffer { // Exponential golomb values are encoded as: // 1) x = source val + 1 // 2) In binary, write [countbits(x) - 1] 1s, then x // To decode, we count the number of leading 1 bits, read that many + 1 bits, // and increment the result by 1. // Returns false if there isn't enough data left for the specified type, or if // the value wouldn't fit in a uint32_t. bool ReadExponentialGolomb(uint32_t* val); - // Reads signed exponential golomb values at the current offset. Signed - // exponential golomb values are just the unsigned values mapped to the - // sequence 0, 1, -1, 2, -2, etc. in order. - bool ReadSignedExponentialGolomb(int32_t* val); - // Moves current position |byte_count| bytes forward. Returns false if - // there aren't enough bytes left in the buffer. - bool ConsumeBytes(size_t byte_count); // Moves current position |bit_count| bits forward. Returns false if // there aren't enough bits left in the buffer. bool ConsumeBits(size_t bit_count); - // Sets the current offset to the provied byte/bit offsets. The bit - // offset is from the given byte, in the range [0,7]. - bool Seek(size_t byte_offset, size_t bit_offset); - protected: const uint8_t* const bytes_; // The total size of |bytes_|. size_t byte_count_; // The current offset, in bytes, from the start of |bytes_|. size_t byte_offset_; // The current offset, in bits, into the current byte. size_t bit_offset_; @@ -175,40 +154,16 @@ BitBuffer::BitBuffer(const uint8_t* byte MOZ_ASSERT(static_cast<uint64_t>(byte_count_) <= std::numeric_limits<uint32_t>::max()); } uint64_t BitBuffer::RemainingBitCount() const { return (static_cast<uint64_t>(byte_count_) - byte_offset_) * 8 - bit_offset_; } -bool BitBuffer::ReadUInt8(uint8_t* val) { - uint32_t bit_val; - if (!ReadBits(&bit_val, sizeof(uint8_t) * 8)) { - return false; - } - MOZ_ASSERT(bit_val <= std::numeric_limits<uint8_t>::max()); - *val = static_cast<uint8_t>(bit_val); - return true; -} - -bool BitBuffer::ReadUInt16(uint16_t* val) { - uint32_t bit_val; - if (!ReadBits(&bit_val, sizeof(uint16_t) * 8)) { - return false; - } - MOZ_ASSERT(bit_val <= std::numeric_limits<uint16_t>::max()); - *val = static_cast<uint16_t>(bit_val); - return true; -} - -bool BitBuffer::ReadUInt32(uint32_t* val) { - return ReadBits(val, sizeof(uint32_t) * 8); -} - bool BitBuffer::PeekBits(uint32_t* val, size_t bit_count) { if (!val || bit_count > RemainingBitCount() || bit_count > 32) { return false; } const uint8_t* bytes = bytes_ + byte_offset_; size_t remaining_bits_in_current_byte = 8 - bit_offset_; uint32_t bits = LowestBits(*bytes++, remaining_bits_in_current_byte); // If we're reading fewer bits than what's left in the current byte, just @@ -233,20 +188,16 @@ bool BitBuffer::PeekBits(uint32_t* val, *val = bits; return true; } bool BitBuffer::ReadBits(uint32_t* val, size_t bit_count) { return PeekBits(val, bit_count) && ConsumeBits(bit_count); } -bool BitBuffer::ConsumeBytes(size_t byte_count) { - return ConsumeBits(byte_count * 8); -} - bool BitBuffer::ConsumeBits(size_t bit_count) { if (bit_count > RemainingBitCount()) { return false; } byte_offset_ += (bit_offset_ + bit_count) / 8; bit_offset_ = (bit_offset_ + bit_count) % 8; return true; @@ -268,41 +219,9 @@ bool BitBuffer::ReadExponentialGolomb(ui } if (!ConsumeBits(1)) { return false; // The stream is incorrectly terminated at '1'. } *val = one_bit_count; return true; } - -bool BitBuffer::ReadSignedExponentialGolomb(int32_t* val) { - uint32_t unsigned_val; - if (!ReadExponentialGolomb(&unsigned_val)) { - return false; - } - if ((unsigned_val & 1) == 0) { - *val = -static_cast<int32_t>(unsigned_val / 2); - } else { - *val = (unsigned_val + 1) / 2; - } - return true; } - -void BitBuffer::GetCurrentOffset( - size_t* out_byte_offset, size_t* out_bit_offset) { - MOZ_ASSERT(out_byte_offset != NULL); - MOZ_ASSERT(out_bit_offset != NULL); - *out_byte_offset = byte_offset_; - *out_bit_offset = bit_offset_; -} - -bool BitBuffer::Seek(size_t byte_offset, size_t bit_offset) { - if (byte_offset > byte_count_ || bit_offset > 7 || - (byte_offset == byte_count_ && bit_offset > 0)) { - return false; - } - byte_offset_ = byte_offset; - bit_offset_ = bit_offset; - return true; -} -} -
--- a/toolkit/components/url-classifier/content/listmanager.js +++ b/toolkit/components/url-classifier/content/listmanager.js @@ -191,18 +191,18 @@ PROT_ListManager.prototype.requireTableU /** * Acts as a nsIUrlClassifierCallback for getTables. */ PROT_ListManager.prototype.kickoffUpdate_ = function (onDiskTableData) { this.startingUpdate_ = false; var initialUpdateDelay = 3000; - // Add a fuzz of 0-5 minutes. - initialUpdateDelay += Math.floor(Math.random() * (5 * 60 * 1000)); + // Add a fuzz of 0-1 minutes for both v2 and v4 according to Bug 1305478. + initialUpdateDelay += Math.floor(Math.random() * (1 * 60 * 1000)); // If the user has never downloaded tables, do the check now. log("needsUpdate: " + JSON.stringify(this.needsUpdate_, undefined, 2)); for (var updateUrl in this.needsUpdate_) { // If we haven't already kicked off updates for this updateUrl, set a // non-repeating timer for it. The timer delay will be reset either on // updateSuccess to this.updateinterval, or backed off on downloadError. // Don't set the updateChecker unless at least one table has updates
--- a/toolkit/content/tests/chrome/findbar_window.xul +++ b/toolkit/content/tests/chrome/findbar_window.xul @@ -26,17 +26,17 @@ ContentTask.setTestScope(window.opener.wrappedJSObject); var gPrefsvc = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); const SAMPLE_URL = "http://www.mozilla.org/"; const SAMPLE_TEXT = "Some text in a text field."; const SEARCH_TEXT = "Text Test"; const NOT_FOUND_TEXT = "This text is not on the page." - const ITERATOR_TIMEOUT = gPrefsvc.getIntPref("findbar.iteratorTimeout") + 20; + const ITERATOR_TIMEOUT = gPrefsvc.getIntPref("findbar.iteratorTimeout"); var gFindBar = null; var gBrowser; var gClipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard); var gHasFindClipboard = gClipboard.supportsFindClipboard(); var gStatusText; @@ -250,17 +250,17 @@ let listener = { onMatchesCountResult: function() { gFindBar.browser.finder.removeResultListener(listener); resolve(); } }; gFindBar.browser.finder.addResultListener(listener); // Make sure we resolve _at least_ after five times the find iterator timeout. - setTimeout(resolve, ITERATOR_TIMEOUT * 5); + setTimeout(resolve, (ITERATOR_TIMEOUT * 5) + 20); }); } var enterStringIntoFindField = Task.async(function* (str, waitForResult = true) { for (let promise, i = 0; i < str.length; i++) { if (waitForResult) { promise = promiseFindResult(); } @@ -518,17 +518,17 @@ ok(document.commandDispatcher.focusedElement == gFindBar._findField.inputField, "testFindCountUI: find field is not focused"); let promise; let matchCase = gFindBar.getElement("find-case-sensitive"); if (matchCase.checked) { promise = promiseFindResult(); matchCase.click(); - yield new Promise(resolve => setTimeout(resolve, ITERATOR_TIMEOUT)); + yield new Promise(resolve => setTimeout(resolve, ITERATOR_TIMEOUT + 20)); yield promise; } let foundMatches = gFindBar._foundMatches; let tests = [{ text: "t", current: 5, total: 10, @@ -553,26 +553,32 @@ is(aMatches[2], String(aTest.total), `Total amount of matches should be ${aTest.total} for '${aTest.text}'`); } for (let test of tests) { gFindBar._findField.select(); gFindBar._findField.focus(); - yield new Promise(resolve => setTimeout(resolve, ITERATOR_TIMEOUT)); + let timeout = ITERATOR_TIMEOUT; + if (test.text.length == 1) + timeout *= 4; + else if (test.text.length == 2) + timeout *= 2; + timeout += 20; + yield new Promise(resolve => setTimeout(resolve, timeout)); yield enterStringIntoFindField(test.text, false); yield promiseMatchesCountResult(); let matches = foundMatches.value.match(regex); if (!test.total) { ok(!matches, "No message should be shown when 0 matches are expected"); } else { assertMatches(test, matches); for (let i = 1; i < test.total; i++) { - yield new Promise(resolve => setTimeout(resolve, ITERATOR_TIMEOUT)); + yield new Promise(resolve => setTimeout(resolve, timeout)); gFindBar.onFindAgainCommand(); yield promiseMatchesCountResult(); // test.current + 1, test.current + 2, ..., test.total, 1, ..., test.current let current = (test.current + i - 1) % test.total + 1; assertMatches({ text: test.text, current: current, total: test.total @@ -654,17 +660,17 @@ function* testToggleEntireWord() { yield openFindbar(); let promise = promiseFindResult(); yield enterStringIntoFindField("Tex", false); let result = yield promise; is(result.result, Ci.nsITypeAheadFind.FIND_FOUND, "Text should be found"); - yield new Promise(resolve => setTimeout(resolve, ITERATOR_TIMEOUT)); + yield new Promise(resolve => setTimeout(resolve, ITERATOR_TIMEOUT + 20)); promise = promiseFindResult(); let check = gFindBar.getElement("find-entire-word"); check.click(); result = yield promise; is(result.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "Text should NOT be found"); check.click(); gFindBar.close(true);
--- a/toolkit/modules/FinderIterator.jsm +++ b/toolkit/modules/FinderIterator.jsm @@ -165,16 +165,20 @@ this.FinderIterator = { stop(cachePrevious = false) { if (!this.running) return; if (this._timer) { clearTimeout(this._timer); this._timer = null; } + if (this._runningFindResolver) { + this._runningFindResolver(); + this._runningFindResolver = null; + } if (cachePrevious) { this._previousRanges = [].concat(this.ranges); this._previousParams = Object.assign({}, this._currentParams); } else { this._previousRanges = []; this._previousParams = null; } @@ -215,16 +219,20 @@ this.FinderIterator = { * previous result invalid. * If the iterator is running, it will be stopped as soon as possible. */ reset() { if (this._timer) { clearTimeout(this._timer); this._timer = null; } + if (this._runningFindResolver) { + this._runningFindResolver(); + this._runningFindResolver = null; + } this._catchingUp.clear(); this._currentParams = this._previousParams = null; this._previousRanges = []; this.ranges = []; this.running = false; this._notifyListeners("reset"); @@ -416,18 +424,32 @@ this.FinderIterator = { * is not, this identifier is used to learn if * it's supposed to still continue after a pause. * @yield {nsIDOMRange} */ _findAllRanges: Task.async(function* (finder, spawnId) { if (this._timeout) { if (this._timer) clearTimeout(this._timer); - yield new Promise(resolve => this._timer = setTimeout(resolve, this._timeout)); - this._timer = null; + if (this._runningFindResolver) + this._runningFindResolver(); + + let timeout = this._timeout; + let searchTerm = this._currentParams.word; + // Wait a little longer when the first or second character is typed into + // the findbar. + if (searchTerm.length == 1) + timeout *= 4; + else if (searchTerm.length == 2) + timeout *= 2; + yield new Promise(resolve => { + this._runningFindResolver = resolve; + this._timer = setTimeout(resolve, timeout); + }); + this._timer = this._runningFindResolver = null; // During the timeout, we could have gotten the signal to stop iterating. // Make sure we do here. if (!this.running || spawnId !== this._spawnId) return; } this._notifyListeners("start", this.params);
--- a/toolkit/modules/tests/browser/browser_FinderHighlighter.js +++ b/toolkit/modules/tests/browser/browser_FinderHighlighter.js @@ -216,17 +216,22 @@ add_task(function* testModalResults() { let url = kFixtureBaseURL + "file_FinderSample.html"; yield BrowserTestUtils.withNewTab(url, function* (browser) { let findbar = gBrowser.getFindBar(); for (let [word, expectedResult] of tests) { yield promiseOpenFindbar(findbar); Assert.ok(!findbar.hidden, "Findbar should be open now."); - yield new Promise(resolve => setTimeout(resolve, kIteratorTimeout)); + let timeout = kIteratorTimeout; + if (word.length == 1) + timeout *= 4; + else if (word.length == 2) + timeout *= 2; + yield new Promise(resolve => setTimeout(resolve, timeout)); let promise = promiseTestHighlighterOutput(browser, word, expectedResult, expectedResult.extraTest); yield promiseEnterStringIntoFindField(findbar, word); yield promise; findbar.close(true); } });
--- a/xpcom/base/nsTraceRefcnt.cpp +++ b/xpcom/base/nsTraceRefcnt.cpp @@ -569,32 +569,36 @@ LogThisType(const char* aTypeName) } static PLHashNumber HashNumber(const void* aKey) { return PLHashNumber(NS_PTR_TO_INT32(aKey)); } +// This method uses MOZ_RELEASE_ASSERT in the unlikely event that +// somebody uses this in a non-debug build. static intptr_t GetSerialNumber(void* aPtr, bool aCreate) { PLHashEntry** hep = PL_HashTableRawLookup(gSerialNumbers, HashNumber(aPtr), aPtr); if (hep && *hep) { + MOZ_RELEASE_ASSERT(!aCreate, "If an object already has a serial number, we should be destroying it."); return static_cast<SerialNumberRecord*>((*hep)->value)->serialNumber; - } else if (aCreate) { - SerialNumberRecord* record = new SerialNumberRecord(); - WalkTheStackSavingLocations(record->allocationStack); - PL_HashTableRawAdd(gSerialNumbers, hep, HashNumber(aPtr), - aPtr, static_cast<void*>(record)); - return gNextSerialNumber; } - return 0; + + MOZ_RELEASE_ASSERT(aCreate, "If an object does not have a serial number, we should be creating it."); + + SerialNumberRecord* record = new SerialNumberRecord(); + WalkTheStackSavingLocations(record->allocationStack); + PL_HashTableRawAdd(gSerialNumbers, hep, HashNumber(aPtr), + aPtr, static_cast<void*>(record)); + return gNextSerialNumber; } static int32_t* GetRefCount(void* aPtr) { PLHashEntry** hep = PL_HashTableRawLookup(gSerialNumbers, HashNumber(aPtr), aPtr);