From dc60e7a8cfc2c930fe1d0e3ea8f683855d3b2ea4 Mon Sep 17 00:00:00 2001 From: Mike Park Date: Tue, 20 Jun 2017 11:23:32 -0400 Subject: [PATCH 01/56] Bug 1373339 - Add a button in the rules view to toggle the CSS shapes highlighter. r=gl Requires pref "devtools.inspector.shapesHighlighter.enabled" to be true. MozReview-Commit-ID: Ispw7ulV5o6 --- .../client/inspector/rules/test/browser.ini | 6 + ..._grid-highlighter-restored-after-reload.js | 4 +- .../test/browser_rules_shapes-toggle_01.js | 63 ++++++ .../test/browser_rules_shapes-toggle_02.js | 72 +++++++ .../test/browser_rules_shapes-toggle_03.js | 92 ++++++++ .../test/browser_rules_shapes-toggle_04.js | 46 ++++ .../test/browser_rules_shapes-toggle_05.js | 43 ++++ .../test/browser_rules_shapes-toggle_06.js | 78 +++++++ devtools/client/inspector/rules/test/head.js | 3 + .../rules/views/text-property-editor.js | 15 ++ .../inspector/shared/highlighters-overlay.js | 203 ++++++++++++++---- devtools/client/preferences/devtools.js | 2 + devtools/client/shared/output-parser.js | 34 ++- devtools/client/themes/rules.css | 12 +- devtools/server/actors/highlighters/shapes.js | 10 + devtools/shared/css/properties-db.js | 2 + 16 files changed, 634 insertions(+), 51 deletions(-) create mode 100644 devtools/client/inspector/rules/test/browser_rules_shapes-toggle_01.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_shapes-toggle_02.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_shapes-toggle_03.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_shapes-toggle_04.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_shapes-toggle_05.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_shapes-toggle_06.js diff --git a/devtools/client/inspector/rules/test/browser.ini b/devtools/client/inspector/rules/test/browser.ini index 0de5ae759b638..fdf7816c59608 100644 --- a/devtools/client/inspector/rules/test/browser.ini +++ b/devtools/client/inspector/rules/test/browser.ini @@ -233,6 +233,12 @@ skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 [browser_rules_selector-highlighter_04.js] [browser_rules_selector-highlighter_05.js] [browser_rules_selector_highlight.js] +[browser_rules_shapes-toggle_01.js] +[browser_rules_shapes-toggle_02.js] +[browser_rules_shapes-toggle_03.js] +[browser_rules_shapes-toggle_04.js] +[browser_rules_shapes-toggle_05.js] +[browser_rules_shapes-toggle_06.js] [browser_rules_shorthand-overridden-lists.js] [browser_rules_strict-search-filter-computed-list_01.js] [browser_rules_strict-search-filter_01.js] diff --git a/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-restored-after-reload.js b/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-restored-after-reload.js index f0bdad23e30e4..22ff7756f4f27 100644 --- a/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-restored-after-reload.js +++ b/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-restored-after-reload.js @@ -50,7 +50,7 @@ add_task(function* () { ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown."); info("Reload the page, expect the highlighter to be displayed once again"); - let onStateRestored = highlighters.once("state-restored"); + let onStateRestored = highlighters.once("grid-state-restored"); yield refreshTab(gBrowser.selectedTab); let { restored } = yield onStateRestored; ok(restored, "The highlighter state was restored"); @@ -60,7 +60,7 @@ add_task(function* () { info("Navigate to another URL, and check that the highlighter is hidden"); let otherUri = "data:text/html;charset=utf-8," + encodeURIComponent(OTHER_URI); - onStateRestored = highlighters.once("state-restored"); + onStateRestored = highlighters.once("grid-state-restored"); yield navigateTo(inspector, otherUri); ({ restored } = yield onStateRestored); ok(!restored, "The highlighter state was not restored"); diff --git a/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_01.js b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_01.js new file mode 100644 index 0000000000000..a77bccfbe4e35 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_01.js @@ -0,0 +1,63 @@ +/* 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"; + +// Test toggling the shapes highlighter in the rule view and the display of the +// shapes highlighter. + +const TEST_URI = ` + +
+`; + +const HIGHLIGHTER_TYPE = "ShapesHighlighter"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + let highlighters = view.highlighters; + + yield selectNode("#shape", inspector); + let container = getRuleViewProperty(view, "#shape", "clip-path").valueSpan; + let shapesToggle = container.querySelector(".ruleview-shape"); + + info("Checking the initial state of the CSS shape toggle in the rule-view."); + ok(shapesToggle, "Shapes highlighter toggle is visible."); + ok(!shapesToggle.classList.contains("active"), + "Shapes highlighter toggle button is not active."); + ok(!highlighters.highlighters[HIGHLIGHTER_TYPE], + "No CSS shapes highlighter exists in the rule-view."); + ok(!highlighters.shapesHighlighterShown, "No CSS shapes highlighter is shown."); + + info("Toggling ON the CSS shapes highlighter from the rule-view."); + let onHighlighterShown = highlighters.once("shapes-highlighter-shown"); + shapesToggle.click(); + yield onHighlighterShown; + + info("Checking the CSS shapes highlighter is created and toggle button is active in " + + "the rule-view."); + ok(shapesToggle.classList.contains("active"), + "Shapes highlighter toggle is active."); + ok(highlighters.highlighters[HIGHLIGHTER_TYPE], + "CSS shapes highlighter created in the rule-view."); + ok(highlighters.shapesHighlighterShown, "CSS shapes highlighter is shown."); + + info("Toggling OFF the CSS shapes highlighter from the rule-view."); + let onHighlighterHidden = highlighters.once("shapes-highlighter-hidden"); + shapesToggle.click(); + yield onHighlighterHidden; + + info("Checking the CSS shapes highlighter is not shown and toggle button is not " + + "active in the rule-view."); + ok(!shapesToggle.classList.contains("active"), + "shapes highlighter toggle button is not active."); + ok(!highlighters.shapesHighlighterShown, "No CSS shapes highlighter is shown."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_02.js b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_02.js new file mode 100644 index 0000000000000..e7fc1e190571d --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_02.js @@ -0,0 +1,72 @@ +/* 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"; + +// Test toggling the shapes highlighter in the rule view from an overridden +// declaration. + +const TEST_URI = ` + +
+`; + +const HIGHLIGHTER_TYPE = "ShapesHighlighter"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + let highlighters = view.highlighters; + + yield selectNode("#shape", inspector); + let container = getRuleViewProperty(view, "#shape", "clip-path").valueSpan; + let shapeToggle = container.querySelector(".ruleview-shape"); + let overriddenContainer = getRuleViewProperty(view, "div", "clip-path").valueSpan; + let overriddenShapeToggle = overriddenContainer.querySelector(".ruleview-shape"); + + info("Checking the initial state of the CSS shapes toggle in the rule-view."); + ok(shapeToggle && overriddenShapeToggle, "Shapes highlighter toggles are visible."); + ok(!shapeToggle.classList.contains("active") && + !overriddenShapeToggle.classList.contains("active"), + "Shapes highlighter toggle buttons are not active."); + ok(!highlighters.highlighters[HIGHLIGHTER_TYPE], + "No CSS shapes highlighter exists in the rule-view."); + ok(!highlighters.shapesHighlighterShown, "No CSS shapes highlighter is shown."); + + info("Toggling ON the shapes highlighter from the overridden rule in the rule-view."); + let onHighlighterShown = highlighters.once("shapes-highlighter-shown"); + overriddenShapeToggle.click(); + yield onHighlighterShown; + + info("Checking the shapes highlighter is created and toggle buttons are active in " + + "the rule-view."); + ok(shapeToggle.classList.contains("active") && + overriddenShapeToggle.classList.contains("active"), + "shapes highlighter toggle is active."); + ok(highlighters.highlighters[HIGHLIGHTER_TYPE], + "CSS shapes highlighter created in the rule-view."); + ok(highlighters.shapesHighlighterShown, "CSS shapes highlighter is shown."); + + info("Toggling off the shapes highlighter from the normal shapes declaration in the " + + "rule-view."); + let onHighlighterHidden = highlighters.once("shapes-highlighter-hidden"); + shapeToggle.click(); + yield onHighlighterHidden; + + info("Checking the CSS shapes highlighter is not shown and toggle buttons are not " + + "active in the rule-view."); + ok(!shapeToggle.classList.contains("active") && + !overriddenShapeToggle.classList.contains("active"), + "shapes highlighter toggle buttons are not active."); + ok(!highlighters.shapesHighlighterShown, "No CSS shapes highlighter is shown."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_03.js b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_03.js new file mode 100644 index 0000000000000..07eb7201d9bcf --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_03.js @@ -0,0 +1,92 @@ +/* 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"; + +// Test toggling the shapes highlighter in the rule view with multiple shapes in the page. + +const TEST_URI = ` + +
+
+`; + +const HIGHLIGHTER_TYPE = "ShapesHighlighter"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + let highlighters = view.highlighters; + + info("Selecting the first shape container."); + yield selectNode("#shape1", inspector); + let container = getRuleViewProperty(view, ".shape", "clip-path").valueSpan; + let shapeToggle = container.querySelector(".ruleview-shape"); + + info("Checking the state of the CSS shape toggle for the first shape container " + + "in the rule-view."); + ok(shapeToggle, "shape highlighter toggle is visible."); + ok(!shapeToggle.classList.contains("active"), + "shape highlighter toggle button is not active."); + ok(!highlighters.highlighters[HIGHLIGHTER_TYPE], + "No CSS shape highlighter exists in the rule-view."); + ok(!highlighters.shapesHighlighterShown, "No CSS shapes highlighter is shown."); + + info("Toggling ON the CSS shapes highlighter for the first shapes container from the " + + "rule-view."); + let onHighlighterShown = highlighters.once("shapes-highlighter-shown"); + shapeToggle.click(); + yield onHighlighterShown; + + info("Checking the CSS shapes highlighter is created and toggle button is active in " + + "the rule-view."); + ok(shapeToggle.classList.contains("active"), + "shapes highlighter toggle is active."); + ok(highlighters.highlighters[HIGHLIGHTER_TYPE], + "CSS shapes highlighter created in the rule-view."); + ok(highlighters.shapesHighlighterShown, "CSS shapes highlighter is shown."); + + info("Selecting the second shapes container."); + yield selectNode("#shape2", inspector); + let firstShapesHighlighterShown = highlighters.shapesHighlighterShown; + container = getRuleViewProperty(view, ".shape", "clip-path").valueSpan; + shapeToggle = container.querySelector(".ruleview-shape"); + + info("Checking the state of the CSS shapes toggle for the second shapes container " + + "in the rule-view."); + ok(shapeToggle, "shapes highlighter toggle is visible."); + ok(!shapeToggle.classList.contains("active"), + "shapes highlighter toggle button is not active."); + ok(highlighters.shapesHighlighterShown, "CSS shapes highlighter is still shown."); + + info("Toggling ON the CSS shapes highlighter for the second shapes container " + + "from the rule-view."); + onHighlighterShown = highlighters.once("shapes-highlighter-shown"); + shapeToggle.click(); + yield onHighlighterShown; + + info("Checking the CSS shapes highlighter is created for the second shapes container " + + "and toggle button is active in the rule-view."); + ok(shapeToggle.classList.contains("active"), + "shapes highlighter toggle is active."); + ok(highlighters.shapesHighlighterShown != firstShapesHighlighterShown, + "shapes highlighter for the second shapes container is shown."); + + info("Selecting the first shapes container."); + yield selectNode("#shape1", inspector); + container = getRuleViewProperty(view, ".shape", "clip-path").valueSpan; + shapeToggle = container.querySelector(".ruleview-shape"); + + info("Checking the state of the CSS shapes toggle for the first shapes container " + + "in the rule-view."); + ok(shapeToggle, "shapes highlighter toggle is visible."); + ok(!shapeToggle.classList.contains("active"), + "shapes highlighter toggle button is not active."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_04.js b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_04.js new file mode 100644 index 0000000000000..b78cc6680a656 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_04.js @@ -0,0 +1,46 @@ +/* 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"; + +// Test toggling the shapes highlighter in the rule view and modifying the 'clip-path' +// declaration. + +const TEST_URI = ` + +
+`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + let highlighters = view.highlighters; + + yield selectNode("#shape", inspector); + let container = getRuleViewProperty(view, "#shape", "clip-path").valueSpan; + let shapeToggle = container.querySelector(".ruleview-shape"); + + info("Toggling ON the CSS shape highlighter from the rule-view."); + let onHighlighterShown = highlighters.once("shapes-highlighter-shown"); + shapeToggle.click(); + yield onHighlighterShown; + + info("Edit the clip-path property to ellipse."); + let editor = yield focusEditableField(view, container, 30); + let onDone = view.once("ruleview-changed"); + editor.input.value = "ellipse(30% 20%);"; + EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); + yield onDone; + + info("Check the shape highlighter and shape toggle button are still visible."); + shapeToggle = container.querySelector(".ruleview-shape"); + ok(shapeToggle, "Shape highlighter toggle is visible."); + ok(highlighters.shapesHighlighterShown, "CSS shape highlighter is shown."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_05.js b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_05.js new file mode 100644 index 0000000000000..0e371094f3d51 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_05.js @@ -0,0 +1,43 @@ +/* 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"; + +// Tests that the shapes highlighter is hidden when the highlighted shape container is +// removed from the page. + +const TEST_URI = ` + +
+`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view, testActor} = yield openRuleView(); + let highlighters = view.highlighters; + + yield selectNode("#shape", inspector); + let container = getRuleViewProperty(view, "#shape", "clip-path").valueSpan; + let shapeToggle = container.querySelector(".ruleview-shape"); + + info("Toggling ON the CSS shapes highlighter from the rule-view."); + let onHighlighterShown = highlighters.once("shapes-highlighter-shown"); + shapeToggle.click(); + yield onHighlighterShown; + ok(highlighters.shapesHighlighterShown, "CSS shapes highlighter is shown."); + + let onHighlighterHidden = highlighters.once("shapes-highlighter-hidden"); + info("Remove the #shapes container in the content page"); + testActor.eval(` + content.document.querySelector("#shape").remove(); + `); + yield onHighlighterHidden; + ok(!highlighters.shapesHighlighterShown, "CSS shapes highlighter is hidden."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_06.js b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_06.js new file mode 100644 index 0000000000000..3d52361fa1320 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_06.js @@ -0,0 +1,78 @@ +/* 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"; + +// Test toggling the shapes highlighter in the rule view with clip-path and shape-outside +// on the same element. + +const TEST_URI = ` + +
+
+`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + let highlighters = view.highlighters; + + yield selectNode("#shape1", inspector); + let clipPathContainer = getRuleViewProperty(view, ".shape", "clip-path").valueSpan; + let clipPathShapeToggle = clipPathContainer.querySelector(".ruleview-shape"); + let shapeOutsideContainer = getRuleViewProperty(view, ".shape", + "shape-outside").valueSpan; + let shapeOutsideToggle = shapeOutsideContainer.querySelector(".ruleview-shape"); + + info("Toggling ON the CSS shapes highlighter for clip-path from the rule-view."); + let onHighlighterShown = highlighters.once("shapes-highlighter-shown"); + clipPathShapeToggle.click(); + yield onHighlighterShown; + ok(highlighters.shapesHighlighterShown, "CSS shapes highlighter is shown."); + ok(clipPathShapeToggle.classList.contains("active"), + "clip-path toggle button is active."); + ok(!shapeOutsideToggle.classList.contains("active"), + "shape-outside toggle button is not active."); + + info("Toggling ON the CSS shapes highlighter for shape-outside from the rule-view."); + onHighlighterShown = highlighters.once("shapes-highlighter-shown"); + shapeOutsideToggle.click(); + yield onHighlighterShown; + ok(highlighters.shapesHighlighterShown, "CSS shapes highlighter is shown."); + ok(!clipPathShapeToggle.classList.contains("active"), + "clip-path toggle button is not active."); + ok(shapeOutsideToggle.classList.contains("active"), + "shape-outside toggle button is active."); + + info("Selecting the second shapes container."); + yield selectNode("#shape2", inspector); + clipPathContainer = getRuleViewProperty(view, ".shape", "clip-path").valueSpan; + clipPathShapeToggle = clipPathContainer.querySelector(".ruleview-shape"); + shapeOutsideContainer = getRuleViewProperty(view, ".shape", + "shape-outside").valueSpan; + shapeOutsideToggle = shapeOutsideContainer.querySelector(".ruleview-shape"); + ok(!clipPathShapeToggle.classList.contains("active"), + "clip-path toggle button is not active."); + ok(!shapeOutsideToggle.classList.contains("active"), + "shape-outside toggle button is not active."); + + info("Selecting the first shapes container."); + yield selectNode("#shape1", inspector); + clipPathContainer = getRuleViewProperty(view, ".shape", "clip-path").valueSpan; + clipPathShapeToggle = clipPathContainer.querySelector(".ruleview-shape"); + shapeOutsideContainer = getRuleViewProperty(view, ".shape", + "shape-outside").valueSpan; + shapeOutsideToggle = shapeOutsideContainer.querySelector(".ruleview-shape"); + ok(!clipPathShapeToggle.classList.contains("active"), + "clip-path toggle button is not active."); + ok(shapeOutsideToggle.classList.contains("active"), + "shape-outside toggle button is active."); +}); diff --git a/devtools/client/inspector/rules/test/head.js b/devtools/client/inspector/rules/test/head.js index 4fdd9ffa41ee6..69107b6f2afeb 100644 --- a/devtools/client/inspector/rules/test/head.js +++ b/devtools/client/inspector/rules/test/head.js @@ -23,8 +23,11 @@ const FRAME_SCRIPT_URL = ROOT_TEST_DIR + "doc_frame_script.js"; const STYLE_INSPECTOR_L10N = new LocalizationHelper("devtools/shared/locales/styleinspector.properties"); +Services.prefs.setBoolPref("devtools.inspector.shapesHighlighter.enabled", true); + registerCleanupFunction(() => { Services.prefs.clearUserPref("devtools.defaultColorUnit"); + Services.prefs.clearUserPref("devtools.inspector.shapesHighlighter.enabled"); }); /** diff --git a/devtools/client/inspector/rules/views/text-property-editor.js b/devtools/client/inspector/rules/views/text-property-editor.js index 0c30b8d13add6..532ec7b873536 100644 --- a/devtools/client/inspector/rules/views/text-property-editor.js +++ b/devtools/client/inspector/rules/views/text-property-editor.js @@ -358,6 +358,7 @@ TextPropertyEditor.prototype = { filterClass: "ruleview-filter", filterSwatchClass: SHARED_SWATCH_CLASS + " " + FILTER_SWATCH_CLASS, gridClass: "ruleview-grid", + shapeClass: "ruleview-shape", defaultColorType: !propDirty, urlClass: "theme-link", baseURI: this.sheetHref @@ -442,6 +443,20 @@ TextPropertyEditor.prototype = { } } + let shapeToggle = this.valueSpan.querySelector(".ruleview-shape"); + if (shapeToggle) { + let mode = "css" + name.split("-").map(s => { + return s[0].toUpperCase() + s.slice(1); + }).join(""); + shapeToggle.setAttribute("data-mode", mode); + + let { highlighters, inspector } = this.ruleView; + if (highlighters.shapesHighlighterShown === inspector.selection.nodeFront && + highlighters.state.shapes.options.mode === mode) { + shapeToggle.classList.add("active"); + } + } + // Now that we have updated the property's value, we might have a pending // click on the value container. If we do, we have to trigger a click event // on the right element. diff --git a/devtools/client/inspector/shared/highlighters-overlay.js b/devtools/client/inspector/shared/highlighters-overlay.js index 809b9fd287801..e1a8335960103 100644 --- a/devtools/client/inspector/shared/highlighters-overlay.js +++ b/devtools/client/inspector/shared/highlighters-overlay.js @@ -35,10 +35,12 @@ function HighlightersOverlay(inspector) { this.hoveredHighlighterShown = null; // Name of the selector highlighter shown. this.selectorHighlighterShown = null; + // NodeFront of the shape that is highlighted + this.shapesHighlighterShown = null; // Saved state to be restore on page navigation. this.state = { - // Only the grid highlighter state is saved at the moment. - grid: {} + grid: {}, + shapes: {} }; this.onClick = this.onClick.bind(this); @@ -47,6 +49,8 @@ function HighlightersOverlay(inspector) { this.onMouseOut = this.onMouseOut.bind(this); this.onWillNavigate = this.onWillNavigate.bind(this); this.onNavigate = this.onNavigate.bind(this); + this.showGridHighlighter = this.showGridHighlighter.bind(this); + this.showShapesHighlighter = this.showShapesHighlighter.bind(this); this._handleRejection = this._handleRejection.bind(this); // Add inspector events, not specific to a given view. @@ -101,6 +105,81 @@ HighlightersOverlay.prototype = { el.removeEventListener("mouseout", this.onMouseOut); }, + /** + * Toggle the shapes highlighter for the given element with a shape. + * + * @param {NodeFront} node + * The NodeFront of the element with a shape to highlight. + * @param {Object} options + * Object used for passing options to the shapes highlighter. + */ + toggleShapesHighlighter: Task.async(function* (node, options = {}) { + if (node == this.shapesHighlighterShown && + options.mode === this.state.shapes.options.mode) { + yield this.hideShapesHighlighter(node); + return; + } + + yield this.showShapesHighlighter(node, options); + }), + + /** + * Show the shapes highlighter for the given element with a shape. + * + * @param {NodeFront} node + * The NodeFront of the element with a shape to highlight. + * @param {Object} options + * Object used for passing options to the shapes highlighter. + */ + showShapesHighlighter: Task.async(function* (node, options) { + let highlighter = yield this._getHighlighter("ShapesHighlighter"); + if (!highlighter) { + return; + } + + let isShown = yield highlighter.show(node, options); + if (!isShown) { + return; + } + + this.shapesHighlighterShown = node; + let { mode } = options; + this._toggleRuleViewIcon(node, false, ".ruleview-shape"); + this._toggleRuleViewIcon(node, true, `.ruleview-shape[data-mode='${mode}']`); + + try { + // Save shapes highlighter state. + let { url } = this.inspector.target; + let selector = yield node.getUniqueSelector(); + this.state.shapes = { selector, options, url }; + + this.shapesHighlighterShown = node; + this.emit("shapes-highlighter-shown", node, options); + } catch (e) { + this._handleRejection(e); + } + }), + + /** + * Hide the shapes highlighter for the given element with a shape. + * + * @param {NodeFront} node + * The NodeFront of the element with a shape to unhighlight. + */ + hideShapesHighlighter: Task.async(function* (node) { + if (!this.shapesHighlighterShown || !this.highlighters.ShapesHighlighter) { + return; + } + + this._toggleRuleViewIcon(node, false, ".ruleview-shape"); + + yield this.highlighters.ShapesHighlighter.hide(); + this.emit("shapes-highlighter-hidden", this.shapesHighlighterShown, + this.state.shapes.options); + this.shapesHighlighterShown = null; + this.state.shapes = {}; + }), + /** * Toggle the grid highlighter for the given grid container element. * @@ -141,7 +220,7 @@ HighlightersOverlay.prototype = { return; } - this._toggleRuleViewGridIcon(node, true); + this._toggleRuleViewIcon(node, true, ".ruleview-grid"); if (trigger == "grid") { Services.telemetry.scalarAdd("devtools.grid.gridinspector.opened", 1); @@ -175,7 +254,7 @@ HighlightersOverlay.prototype = { return; } - this._toggleRuleViewGridIcon(node, false); + this._toggleRuleViewIcon(node, false, ".ruleview-grid"); yield this.highlighters.CssGridHighlighter.hide(); @@ -242,16 +321,23 @@ HighlightersOverlay.prototype = { /** * Restore the saved highlighter states. - * + * @param {String} name + * The name of the highlighter to be restored + * @param {String} selector + * The selector of the node that was previously highlighted + * @param {Object} options + * The options previously supplied to the highlighter + * @param {String} url + * The URL of the page the highlighter was active on + * @param {Function} showFunction + * The function that shows the highlighter * @return {Promise} that resolves when the highlighter state was restored, and the - * expected highlighters are displayed. + * expected highlighters are displayed. */ - restoreState: Task.async(function* () { - let { selector, options, url } = this.state.grid; - + restoreState: Task.async(function* (name, {selector, options, url}, showFunction) { if (!selector || url !== this.inspector.target.url) { // Bail out if no selector was saved, or if we are on a different page. - this.emit("state-restored", { restored: false }); + this.emit(`${name}-state-restored`, { restored: false }); return; } @@ -263,11 +349,11 @@ HighlightersOverlay.prototype = { let nodeFront = yield walker.querySelector(rootNode, selector); if (nodeFront) { - yield this.showGridHighlighter(nodeFront, options); - this.emit("state-restored", { restored: true }); + yield showFunction(nodeFront, options); + this.emit(`${name}-state-restored`, { restored: true }); } - this.emit("state-restored", { restored: false }); + this.emit(`${name}-state-restored`, { restored: false }); }), /** @@ -307,23 +393,25 @@ HighlightersOverlay.prototype = { }, /** - * Toggle all the grid icons in the rule view if the current inspector selection is the - * highlighted node. + * Toggle all the icons with the given selector in the rule view if the current + * inspector selection is the highlighted node. * * @param {NodeFront} node - * The NodeFront of the grid container element to highlight. + * The NodeFront of the element with a shape to highlight. * @param {Boolean} active - * Whether or not the grid icon should be active. + * Whether or not the shape icon should be active. + * @param {String} selector + * The selector of the rule view icon to toggle. */ - _toggleRuleViewGridIcon: function (node, active) { + _toggleRuleViewIcon: function (node, active, selector) { if (this.inspector.selection.nodeFront != node) { return; } let ruleViewEl = this.inspector.getPanel("ruleview").view.element; - for (let gridIcon of ruleViewEl.querySelectorAll(".ruleview-grid")) { - gridIcon.classList.toggle("active", active); + for (let icon of ruleViewEl.querySelectorAll(selector)) { + icon.classList.toggle("active", active); } }, @@ -373,6 +461,17 @@ HighlightersOverlay.prototype = { return this.isRuleView && node.classList.contains("ruleview-grid"); }, + /** + * Does the current clicked node have the shapes highlighter toggle in the + * rule-view. + * + * @param {DOMNode} node + * @return {Boolean} + */ + _isRuleViewShape: function (node) { + return this.isRuleView && node.classList.contains("ruleview-shape"); + }, + /** * Is the current hovered node a css transform property value in the rule-view. * @@ -389,21 +488,23 @@ HighlightersOverlay.prototype = { }, onClick: function (event) { - // Bail out if the target is not a grid property value. - if (!this._isRuleViewDisplayGrid(event.target)) { - return; - } + if (this._isRuleViewDisplayGrid(event.target)) { + event.stopPropagation(); - event.stopPropagation(); + let { store } = this.inspector; + let { grids, highlighterSettings } = store.getState(); + let grid = grids.find(g => g.nodeFront == this.inspector.selection.nodeFront); - let { store } = this.inspector; - let { grids, highlighterSettings } = store.getState(); - let grid = grids.find(g => g.nodeFront == this.inspector.selection.nodeFront); + highlighterSettings.color = grid ? grid.color : DEFAULT_GRID_COLOR; - highlighterSettings.color = grid ? grid.color : DEFAULT_GRID_COLOR; + this.toggleGridHighlighter(this.inspector.selection.nodeFront, highlighterSettings, + "rule"); + } else if (this._isRuleViewShape(event.target)) { + event.stopPropagation(); - this.toggleGridHighlighter(this.inspector.selection.nodeFront, highlighterSettings, - "rule"); + let settings = { mode: event.target.dataset.mode }; + this.toggleShapesHighlighter(this.inspector.selection.nodeFront, settings); + } }, onMouseMove: function (event) { @@ -458,26 +559,41 @@ HighlightersOverlay.prototype = { }, /** - * Handler function for "markupmutation" events. Hides the grid highlighter if the grid - * container is no longer in the DOM tree. + * Handler function for "markupmutation" events. Hides the grid/shapes highlighter + * if the grid/shapes container is no longer in the DOM tree. */ onMarkupMutation: Task.async(function* (evt, mutations) { let hasInterestingMutation = mutations.some(mut => mut.type === "childList"); - if (!hasInterestingMutation || !this.gridHighlighterShown) { + if (!hasInterestingMutation) { // Bail out if the mutations did not remove nodes, or if no grid highlighter is // displayed. return; } - let nodeFront = this.gridHighlighterShown; + if (this.gridHighlighterShown) { + let nodeFront = this.gridHighlighterShown; - try { - let isInTree = yield this.inspector.walker.isInDOMTree(nodeFront); - if (!isInTree) { - this.hideGridHighlighter(nodeFront); + try { + let isInTree = yield this.inspector.walker.isInDOMTree(nodeFront); + if (!isInTree) { + this.hideGridHighlighter(nodeFront); + } + } catch (e) { + console.error(e); + } + } + + if (this.shapesHighlighterShown) { + let nodeFront = this.shapesHighlighterShown; + + try { + let isInTree = yield this.inspector.walker.isInDOMTree(nodeFront); + if (!isInTree) { + this.hideShapesHighlighter(nodeFront); + } + } catch (e) { + console.error(e); } - } catch (e) { - console.error(e); } }), @@ -486,7 +602,8 @@ HighlightersOverlay.prototype = { */ onNavigate: Task.async(function* () { try { - yield this.restoreState(); + yield this.restoreState("grid", this.state.grid, this.showGridHighlighter); + yield this.restoreState("shapes", this.state.shapes, this.showShapesHighlighter); } catch (e) { this._handleRejection(e); } @@ -500,6 +617,7 @@ HighlightersOverlay.prototype = { this.gridHighlighterShown = null; this.hoveredHighlighterShown = null; this.selectorHighlighterShown = null; + this.shapesHighlighterShown = null; // The inspector panel should emit the new-root event when it is ready after navigate. this.onInspectorNewRoot = this.inspector.once("new-root"); @@ -534,6 +652,7 @@ HighlightersOverlay.prototype = { this.gridHighlighterShown = null; this.hoveredHighlighterShown = null; this.selectorHighlighterShown = null; + this.shapesHighlighterShown = null; this.destroyed = true; } diff --git a/devtools/client/preferences/devtools.js b/devtools/client/preferences/devtools.js index 63057ea43d61c..5736055716225 100644 --- a/devtools/client/preferences/devtools.js +++ b/devtools/client/preferences/devtools.js @@ -64,6 +64,8 @@ pref("devtools.inspector.showAllAnonymousContent", false); pref("devtools.inspector.mdnDocsTooltip.enabled", false); // Enable the new color widget pref("devtools.inspector.colorWidget.enabled", false); +// Enable the CSS shapes highlighter +pref("devtools.inspector.shapesHighlighter.enabled", false); // Enable the Font Inspector pref("devtools.fontinspector.enabled", true); diff --git a/devtools/client/shared/output-parser.js b/devtools/client/shared/output-parser.js index a14b43ea90557..b5db9637ed73c 100644 --- a/devtools/client/shared/output-parser.js +++ b/devtools/client/shared/output-parser.js @@ -10,6 +10,7 @@ const {getCSSLexer} = require("devtools/shared/css/lexer"); const EventEmitter = require("devtools/shared/event-emitter"); const { ANGLE_TAKING_FUNCTIONS, + BASIC_SHAPE_FUNCTIONS, BEZIER_KEYWORDS, COLOR_TAKING_FUNCTIONS, CSS_TYPES @@ -18,6 +19,7 @@ const Services = require("Services"); const HTML_NS = "http://www.w3.org/1999/xhtml"; const CSS_GRID_ENABLED_PREF = "layout.css.grid.enabled"; +const CSS_SHAPES_ENABLED_PREF = "devtools.inspector.shapesHighlighter.enabled"; /** * This module is used to process text for output by developer tools. This means @@ -77,6 +79,7 @@ OutputParser.prototype = { options.expectCubicBezier = this.supportsType(name, CSS_TYPES.TIMING_FUNCTION); options.expectDisplay = name === "display"; options.expectFilter = name === "filter"; + options.expectShape = name === "clip-path" || name === "shape-outside"; options.supportsColor = this.supportsType(name, CSS_TYPES.COLOR) || this.supportsType(name, CSS_TYPES.GRADIENT); @@ -162,14 +165,12 @@ OutputParser.prototype = { }; let spaceNeeded = false; - while (true) { - let token = tokenStream.nextToken(); - if (!token) { - break; - } + let token = tokenStream.nextToken(); + while (token) { if (token.tokenType === "comment") { // This doesn't change spaceNeeded, because we didn't emit // anything to the output. + token = tokenStream.nextToken(); continue; } @@ -197,6 +198,10 @@ OutputParser.prototype = { } else if (colorOK() && colorUtils.isValidCSSColor(functionText, this.cssColor4)) { this._appendColor(functionText, options); + } else if (options.expectShape && + Services.prefs.getBoolPref(CSS_SHAPES_ENABLED_PREF) && + BASIC_SHAPE_FUNCTIONS.includes(token.text)) { + this._appendShape(functionText, options); } else { this._appendTextNode(functionText); } @@ -273,6 +278,8 @@ OutputParser.prototype = { token.tokenType === "id" || token.tokenType === "hash" || token.tokenType === "number" || token.tokenType === "dimension" || token.tokenType === "percentage" || token.tokenType === "dimension"); + + token = tokenStream.nextToken(); } let result = this._toDOM(); @@ -353,6 +360,21 @@ OutputParser.prototype = { this.parsed.push(container); }, + _appendShape: function (shape, options) { + let container = this._createNode("span", {}); + + let toggle = this._createNode("span", { + class: options.shapeClass + }); + + let value = this._createNode("span", {}); + value.textContent = shape; + + container.appendChild(toggle); + container.appendChild(value); + this.parsed.push(container); + }, + /** * Append a angle value to the output * @@ -697,6 +719,7 @@ OutputParser.prototype = { * // _wrapFilter. Used only for * // previewing with the filter swatch. * - gridClass: "" // The class to use for the grid icon. + * - shapeClass: "" // The class to use for the shape icon. * - supportsColor: false // Does the CSS property support colors? * - urlClass: "" // The class to be used for url() links. * - baseURI: undefined // A string used to resolve @@ -715,6 +738,7 @@ OutputParser.prototype = { colorSwatchClass: "", filterSwatch: false, gridClass: "", + shapeClass: "", supportsColor: false, urlClass: "", baseURI: undefined, diff --git a/devtools/client/themes/rules.css b/devtools/client/themes/rules.css index 3159773c966b3..75a880d658466 100644 --- a/devtools/client/themes/rules.css +++ b/devtools/client/themes/rules.css @@ -452,7 +452,8 @@ } .ruleview-grid, -.ruleview-swatch { +.ruleview-swatch, +.ruleview-shape { cursor: pointer; border-radius: 50%; width: 1em; @@ -470,6 +471,12 @@ border-radius: 0; } +.ruleview-shape { + background: url("chrome://devtools/skin/images/tool-shadereditor.svg"); + border-radius: 0; + background-size: 1em; +} + .ruleview-colorswatch::before { content: ''; background-color: #eee; @@ -596,7 +603,8 @@ .ruleview-grid.active, .ruleview-selectorhighlighter:active, -.ruleview-selectorhighlighter.highlighted { +.ruleview-selectorhighlighter.highlighted, +.ruleview-shape.active { filter: url(images/filters.svg#checked-icon-state) brightness(0.9); } diff --git a/devtools/server/actors/highlighters/shapes.js b/devtools/server/actors/highlighters/shapes.js index 00eb83f777319..64e231b964c9f 100644 --- a/devtools/server/actors/highlighters/shapes.js +++ b/devtools/server/actors/highlighters/shapes.js @@ -39,9 +39,11 @@ class ShapesHighlighter extends AutoRefreshHighlighter { this.markup = new CanvasFrameAnonymousContentHelper(this.highlighterEnv, this._buildMarkup.bind(this)); + this.onPageHide = this.onPageHide.bind(this); let { pageListenerTarget } = this.highlighterEnv; DOM_EVENTS.forEach(event => pageListenerTarget.addEventListener(event, this)); + pageListenerTarget.addEventListener("pagehide", this.onPageHide); } _buildMarkup() { @@ -1262,6 +1264,14 @@ class ShapesHighlighter extends AutoRefreshHighlighter { setIgnoreLayoutChanges(false, this.highlighterEnv.window.document.documentElement); } + + onPageHide({ target }) { + // If a page hide event is triggered for current window's highlighter, hide the + // highlighter. + if (target.defaultView === this.win) { + this.hide(); + } + } } /** diff --git a/devtools/shared/css/properties-db.js b/devtools/shared/css/properties-db.js index 32454482a9ff3..ab50c57a258c2 100644 --- a/devtools/shared/css/properties-db.js +++ b/devtools/shared/css/properties-db.js @@ -75,6 +75,8 @@ exports.ANGLE_TAKING_FUNCTIONS = ["linear-gradient", "-moz-linear-gradient", "rotateY", "rotateZ", "rotate3d", "skew", "skewX", "skewY", "hue-rotate"]; +exports.BASIC_SHAPE_FUNCTIONS = ["polygon", "circle", "ellipse", "inset"]; + /** * The list of all CSS Pseudo Elements. * From b9e645073ee87656d4cfc5b4a3d4a4bf59ba1c6a Mon Sep 17 00:00:00 2001 From: Paul Adenot Date: Mon, 24 Jul 2017 11:17:14 +0200 Subject: [PATCH 02/56] Bug 1375119 - Consider a page active if it has running AudioContexts. r=ehsan MozReview-Commit-ID: IOQ2DY9LoTw --HG-- extra : rebase_source : f47568d3db129ff8d9a484d5b5554f1154d53133 --- dom/base/nsGlobalWindow.cpp | 5 +++ dom/base/test/browser.ini | 1 + ..._timeout_throttling_with_audio_playback.js | 2 +- dom/base/test/file_webaudioLoop.html | 17 +++------ dom/base/test/file_webaudio_startstop.html | 36 +++++++++++++++++++ dom/base/test/mochitest.ini | 1 + dom/media/webaudio/AudioContext.cpp | 6 ++++ dom/media/webaudio/AudioContext.h | 1 + 8 files changed, 55 insertions(+), 14 deletions(-) create mode 100644 dom/base/test/file_webaudio_startstop.html diff --git a/dom/base/nsGlobalWindow.cpp b/dom/base/nsGlobalWindow.cpp index cc8be444ce081..e26e725ede4f0 100644 --- a/dom/base/nsGlobalWindow.cpp +++ b/dom/base/nsGlobalWindow.cpp @@ -4425,6 +4425,11 @@ nsPIDOMWindowInner::SyncStateFromParentWindow() bool nsPIDOMWindowInner::IsPlayingAudio() { + for (uint32_t i = 0; i < mAudioContexts.Length(); i++) { + if (mAudioContexts[i]->IsRunning()) { + return true; + } + } RefPtr acs = AudioChannelService::Get(); if (!acs) { return false; diff --git a/dom/base/test/browser.ini b/dom/base/test/browser.ini index 3add6935fad71..ce3901c969140 100644 --- a/dom/base/test/browser.ini +++ b/dom/base/test/browser.ini @@ -22,6 +22,7 @@ support-files = file_use_counter_svg_fill_pattern_internal.svg file_use_counter_svg_fill_pattern_data.svg file_webaudioLoop.html + file_webaudio_startstop.html plugin.js !/image/test/mochitest/shaver.png diff --git a/dom/base/test/browser_timeout_throttling_with_audio_playback.js b/dom/base/test/browser_timeout_throttling_with_audio_playback.js index 6ffa29db02a2b..fcf7ba1cc22a2 100644 --- a/dom/base/test/browser_timeout_throttling_with_audio_playback.js +++ b/dom/base/test/browser_timeout_throttling_with_audio_playback.js @@ -11,7 +11,7 @@ var testURLs = [ "http://mochi.test:8888/browser/dom/base/test/file_audioLoop.html", "http://mochi.test:8888/browser/dom/base/test/file_audioLoopInIframe.html", "http://mochi.test:8888/browser/dom/base/test/file_pluginAudio.html", - "http://mochi.test:8888/browser/dom/base/test/file_webaudioLoop.html", + "http://mochi.test:8888/browser/dom/base/test/file_webaudio_startstop.html", ]; // We want to ensure that while audio is being played back, a background tab is diff --git a/dom/base/test/file_webaudioLoop.html b/dom/base/test/file_webaudioLoop.html index c255bc5414ba4..d298d53ad8c76 100644 --- a/dom/base/test/file_webaudioLoop.html +++ b/dom/base/test/file_webaudioLoop.html @@ -8,19 +8,10 @@ } }; }); -fetch("audio.ogg").then(response => { - return response.arrayBuffer(); -}).then(ab => { - return ac.decodeAudioData(ab); -}).then(ab => { - var src = ac.createBufferSource(); - src.buffer = ab; - src.loop = true; - src.loopStart = 0; - src.loopEnd = ab.duration; - src.start(); - src.connect(ac.destination); -}); + +var osc = ac.createOscillator(); +osc.connect(ac.destination); +osc.start(0); var suspendPromise; function suspendAC() { diff --git a/dom/base/test/file_webaudio_startstop.html b/dom/base/test/file_webaudio_startstop.html new file mode 100644 index 0000000000000..c0e4fafb01ae3 --- /dev/null +++ b/dom/base/test/file_webaudio_startstop.html @@ -0,0 +1,36 @@ + + diff --git a/dom/base/test/mochitest.ini b/dom/base/test/mochitest.ini index 1012c8d7c9875..13d005a09e199 100644 --- a/dom/base/test/mochitest.ini +++ b/dom/base/test/mochitest.ini @@ -210,6 +210,7 @@ support-files = file_audioLoop.html file_webaudioLoop.html file_webaudioLoop2.html + file_webaudio_startstop.html file_pluginAudio.html file_pluginAudioNonAutoStart.html noaudio.webm diff --git a/dom/media/webaudio/AudioContext.cpp b/dom/media/webaudio/AudioContext.cpp index 4cf6b8715ba0c..d786db4ca1aeb 100644 --- a/dom/media/webaudio/AudioContext.cpp +++ b/dom/media/webaudio/AudioContext.cpp @@ -492,6 +492,12 @@ AudioContext::Listener() return mListener; } +bool +AudioContext::IsRunning() const +{ + return mAudioContextState == AudioContextState::Running; +} + already_AddRefed AudioContext::DecodeAudioData(const ArrayBuffer& aBuffer, const Optional >& aSuccessCallback, diff --git a/dom/media/webaudio/AudioContext.h b/dom/media/webaudio/AudioContext.h index be927461e6343..b1c43fa1e0cf4 100644 --- a/dom/media/webaudio/AudioContext.h +++ b/dom/media/webaudio/AudioContext.h @@ -181,6 +181,7 @@ class AudioContext final : public DOMEventTargetHelper, AudioListener* Listener(); AudioContextState State() const { return mAudioContextState; } + bool IsRunning() const; // Those three methods return a promise to content, that is resolved when an // (possibly long) operation is completed on the MSG (and possibly other) From 4736d3662ea3409d798fb5833ede6d050a255ffe Mon Sep 17 00:00:00 2001 From: Brian Hackett Date: Mon, 24 Jul 2017 07:34:40 -0600 Subject: [PATCH 03/56] Bug 1383467 - Only cancel Ion compilations that use nursery pointers when performing a minor GC, r=jonco. --HG-- extra : rebase_source : 9032ca56a38b2be96bc2cc5dfb6a0163a54b06c3 --- js/src/gc/Nursery.cpp | 5 +++-- js/src/vm/HelperThreads.cpp | 32 +++++++++++++++++++------------- js/src/vm/HelperThreads.h | 8 ++++++++ 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/js/src/gc/Nursery.cpp b/js/src/gc/Nursery.cpp index f132e7cb6229e..6249451633691 100644 --- a/js/src/gc/Nursery.cpp +++ b/js/src/gc/Nursery.cpp @@ -701,10 +701,11 @@ js::Nursery::doCollection(JS::gcreason::Reason reason, StoreBuffer& sb = runtime()->gc.storeBuffer(); // The MIR graph only contains nursery pointers if cancelIonCompilations() - // is set on the store buffer, in which case we cancel all compilations. + // is set on the store buffer, in which case we cancel all compilations + // of such graphs. startProfile(ProfileKey::CancelIonCompilations); if (sb.cancelIonCompilations()) - js::CancelOffThreadIonCompile(rt); + js::CancelOffThreadIonCompilesUsingNurseryPointers(rt); endProfile(ProfileKey::CancelIonCompilations); startProfile(ProfileKey::TraceValues); diff --git a/js/src/vm/HelperThreads.cpp b/js/src/vm/HelperThreads.cpp index af56832ab007a..00f947addc41c 100644 --- a/js/src/vm/HelperThreads.cpp +++ b/js/src/vm/HelperThreads.cpp @@ -148,6 +148,7 @@ GetSelectorRuntime(const CompilationSelector& selector) JSRuntime* match(ZonesInState zbs) { return zbs.runtime; } JSRuntime* match(JSRuntime* runtime) { return runtime; } JSRuntime* match(AllCompilations all) { return nullptr; } + JSRuntime* match(CompilationsUsingNursery cun) { return cun.runtime; } }; return selector.match(Matcher()); @@ -163,29 +164,34 @@ JitDataStructuresExist(const CompilationSelector& selector) bool match(ZonesInState zbs) { return zbs.runtime->hasJitRuntime(); } bool match(JSRuntime* runtime) { return runtime->hasJitRuntime(); } bool match(AllCompilations all) { return true; } + bool match(CompilationsUsingNursery cun) { return cun.runtime->hasJitRuntime(); } }; return selector.match(Matcher()); } static bool -CompiledScriptMatches(const CompilationSelector& selector, JSScript* target) +IonBuilderMatches(const CompilationSelector& selector, jit::IonBuilder* builder) { - struct ScriptMatches + struct BuilderMatches { - JSScript* target_; + jit::IonBuilder* builder_; - bool match(JSScript* script) { return script == target_; } - bool match(JSCompartment* comp) { return comp == target_->compartment(); } - bool match(JSRuntime* runtime) { return runtime == target_->runtimeFromAnyThread(); } + bool match(JSScript* script) { return script == builder_->script(); } + bool match(JSCompartment* comp) { return comp == builder_->script()->compartment(); } + bool match(JSRuntime* runtime) { return runtime == builder_->script()->runtimeFromAnyThread(); } bool match(AllCompilations all) { return true; } bool match(ZonesInState zbs) { - return zbs.runtime == target_->runtimeFromAnyThread() && - zbs.state == target_->zoneFromAnyThread()->gcState(); + return zbs.runtime == builder_->script()->runtimeFromAnyThread() && + zbs.state == builder_->script()->zoneFromAnyThread()->gcState(); + } + bool match(CompilationsUsingNursery cun) { + return cun.runtime == builder_->script()->runtimeFromAnyThread() && + !builder_->safeForMinorGC(); } }; - return selector.match(ScriptMatches{target}); + return selector.match(BuilderMatches{builder}); } void @@ -203,7 +209,7 @@ js::CancelOffThreadIonCompile(const CompilationSelector& selector, bool discardL GlobalHelperThreadState::IonBuilderVector& worklist = HelperThreadState().ionWorklist(lock); for (size_t i = 0; i < worklist.length(); i++) { jit::IonBuilder* builder = worklist[i]; - if (CompiledScriptMatches(selector, builder->script())) { + if (IonBuilderMatches(selector, builder)) { FinishOffThreadIonCompile(builder, lock); HelperThreadState().remove(worklist, &i); } @@ -216,7 +222,7 @@ js::CancelOffThreadIonCompile(const CompilationSelector& selector, bool discardL bool unpaused = false; for (auto& helper : *HelperThreadState().threads) { if (helper.ionBuilder() && - CompiledScriptMatches(selector, helper.ionBuilder()->script())) + IonBuilderMatches(selector, helper.ionBuilder())) { helper.ionBuilder()->cancel(); if (helper.pause) { @@ -236,7 +242,7 @@ js::CancelOffThreadIonCompile(const CompilationSelector& selector, bool discardL GlobalHelperThreadState::IonBuilderVector& finished = HelperThreadState().ionFinishedList(lock); for (size_t i = 0; i < finished.length(); i++) { jit::IonBuilder* builder = finished[i]; - if (CompiledScriptMatches(selector, builder->script())) { + if (IonBuilderMatches(selector, builder)) { builder->script()->zone()->group()->numFinishedBuilders--; jit::FinishOffThreadBuilder(nullptr, builder, lock); HelperThreadState().remove(finished, &i); @@ -251,7 +257,7 @@ js::CancelOffThreadIonCompile(const CompilationSelector& selector, bool discardL jit::IonBuilder* builder = group->ionLazyLinkList().getFirst(); while (builder) { jit::IonBuilder* next = builder->getNext(); - if (CompiledScriptMatches(selector, builder->script())) + if (IonBuilderMatches(selector, builder)) jit::FinishOffThreadBuilder(runtime, builder, lock); builder = next; } diff --git a/js/src/vm/HelperThreads.h b/js/src/vm/HelperThreads.h index 5e91de653bfdc..ce06b121e79a9 100644 --- a/js/src/vm/HelperThreads.h +++ b/js/src/vm/HelperThreads.h @@ -494,11 +494,13 @@ StartOffThreadIonFree(jit::IonBuilder* builder, const AutoLockHelperThreadState& struct AllCompilations {}; struct ZonesInState { JSRuntime* runtime; JS::Zone::GCState state; }; +struct CompilationsUsingNursery { JSRuntime* runtime; }; using CompilationSelector = mozilla::Variant; /* @@ -531,6 +533,12 @@ CancelOffThreadIonCompile(JSRuntime* runtime) CancelOffThreadIonCompile(CompilationSelector(runtime), true); } +inline void +CancelOffThreadIonCompilesUsingNurseryPointers(JSRuntime* runtime) +{ + CancelOffThreadIonCompile(CompilationSelector(CompilationsUsingNursery{runtime}), true); +} + inline void CancelOffThreadIonCompile() { From cd310bafca8ef943edefa1a1b06686f27ed05022 Mon Sep 17 00:00:00 2001 From: Jason Orendorff Date: Tue, 18 Jul 2017 17:15:24 -0500 Subject: [PATCH 04/56] Bug 1382016 - Fix autospider.sh nonunified on Mac. r=sfink --HG-- extra : rebase_source : ac9f436dc6086477d0e5a8feab40110654da98f7 --- js/src/devtools/automation/autospider.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/js/src/devtools/automation/autospider.py b/js/src/devtools/automation/autospider.py index fabe50fa21039..015c3b4e21667 100755 --- a/js/src/devtools/automation/autospider.py +++ b/js/src/devtools/automation/autospider.py @@ -142,8 +142,11 @@ def ensure_dir_exists(name, clobber=True): # Note that this modifies the current checkout. for dirpath, dirnames, filenames in os.walk(DIR.js_src): if 'moz.build' in filenames: - subprocess.check_call(['sed', '-i', 's/UNIFIED_SOURCES/SOURCES/', - os.path.join(dirpath, 'moz.build')]) + in_place = ['-i'] + if platform.system() == 'Darwin': + in_place.append('') + subprocess.check_call(['sed'] + in_place + ['s/UNIFIED_SOURCES/SOURCES/', + os.path.join(dirpath, 'moz.build')]) OBJDIR = os.path.join(DIR.source, args.objdir) OUTDIR = os.path.join(OBJDIR, "out") From 7e3c358bf9c4acedc711278f7fa14ce0feecef12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Qu=C3=A8ze?= Date: Mon, 24 Jul 2017 16:59:22 +0200 Subject: [PATCH 05/56] Bug 1383770 - Keep the SEARCH_RESET_RESULT Telemetry probe until Firefox 60. r=bsmedberg --- toolkit/components/telemetry/Histograms.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolkit/components/telemetry/Histograms.json b/toolkit/components/telemetry/Histograms.json index 20c7080969f35..3d26cca78520a 100644 --- a/toolkit/components/telemetry/Histograms.json +++ b/toolkit/components/telemetry/Histograms.json @@ -8009,7 +8009,7 @@ "record_in_processes": ["main", "content"], "alert_emails": ["fqueze@mozilla.com"], "bug_numbers": [1203168], - "expires_in_version": "57", + "expires_in_version": "60", "kind": "enumerated", "n_values": 5, "releaseChannelCollection": "opt-out", From 9c97294062b35f0a00a14073752f4d92cfa34377 Mon Sep 17 00:00:00 2001 From: Christoph Kerschbaumer Date: Mon, 24 Jul 2017 18:51:21 +0200 Subject: [PATCH 06/56] Bug 1331351: Block toplevel window data: URI navigations. r=smaug,francois --- docshell/base/nsDocShell.cpp | 54 ++++++++++++------- docshell/base/nsDocShell.h | 1 + .../en-US/chrome/security/security.properties | 3 ++ modules/libpref/init/all.js | 6 +++ netwerk/base/nsIOService.cpp | 9 ++++ netwerk/base/nsIOService.h | 2 + toolkit/components/telemetry/Histograms.json | 9 ---- 7 files changed, 55 insertions(+), 29 deletions(-) diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp index 4724ae3e22458..649a51d5760e0 100644 --- a/docshell/base/nsDocShell.cpp +++ b/docshell/base/nsDocShell.cpp @@ -10290,8 +10290,11 @@ nsDocShell::InternalLoad(nsIURI* aURI, } } + bool loadFromExternal = false; + // Before going any further vet loads initiated by external programs. if (aLoadType == LOAD_NORMAL_EXTERNAL) { + loadFromExternal = true; // Disallow external chrome: loads targetted at content windows bool isChrome = false; if (NS_SUCCEEDED(aURI->SchemeIs("chrome", &isChrome)) && isChrome) { @@ -10794,7 +10797,8 @@ nsDocShell::InternalLoad(nsIURI* aURI, nsINetworkPredictor::PREDICT_LOAD, attrs, nullptr); nsCOMPtr req; - rv = DoURILoad(aURI, aOriginalURI, aResultPrincipalURI, aLoadReplace, aReferrer, + rv = DoURILoad(aURI, aOriginalURI, aResultPrincipalURI, aLoadReplace, + loadFromExternal, aReferrer, !(aFlags & INTERNAL_LOAD_FLAGS_DONT_SEND_REFERRER), aReferrerPolicy, aTriggeringPrincipal, principalToInherit, aTypeHint, @@ -10875,6 +10879,7 @@ nsDocShell::DoURILoad(nsIURI* aURI, nsIURI* aOriginalURI, Maybe> const& aResultPrincipalURI, bool aLoadReplace, + bool aLoadFromExternal, nsIURI* aReferrerURI, bool aSendReferrer, uint32_t aReferrerPolicy, @@ -11027,25 +11032,34 @@ nsDocShell::DoURILoad(nsIURI* aURI, new LoadInfo(loadingPrincipal, aTriggeringPrincipal, loadingNode, securityFlags, aContentPolicyType); - if (aContentPolicyType == nsIContentPolicy::TYPE_DOCUMENT) { - enum TopLevelDataState { - DATA_NAVIGATED = 0, - DATA_TYPED = 1, - NO_DATA = 2, - }; - bool isDataURI = (NS_SUCCEEDED(aURI->SchemeIs("data", &isDataURI)) && isDataURI); - if (isDataURI) { - // In all cases where the toplevel document is navigated to a data: URI - // the triggeringPrincipal is a CodeBasePrincipal. In all other cases - // e.g. typing a data: URL into the URL-Bar or also clicking a bookmark - // uses a SystemPrincipal as the triggeringPrincipal. - if (aTriggeringPrincipal->GetIsCodebasePrincipal()) { - Telemetry::Accumulate(Telemetry::DOCUMENT_DATA_URI_LOADS, DATA_NAVIGATED); - } else { - Telemetry::Accumulate(Telemetry::DOCUMENT_DATA_URI_LOADS, DATA_TYPED); - } - } else { - Telemetry::Accumulate(Telemetry::DOCUMENT_DATA_URI_LOADS, NO_DATA); + if (aContentPolicyType == nsIContentPolicy::TYPE_DOCUMENT && + nsIOService::BlockToplevelDataUriNavigations()) { + bool isDataURI = + (NS_SUCCEEDED(aURI->SchemeIs("data", &isDataURI)) && isDataURI); + // Let's block all toplevel document navigations to a data: URI. + // In all cases where the toplevel document is navigated to a + // data: URI the triggeringPrincipal is a codeBasePrincipal, or + // a NullPrincipal. In other cases, e.g. typing a data: URL into + // the URL-Bar, the triggeringPrincipal is a SystemPrincipal; + // we don't want to block those loads. Only exception, loads coming + // from an external applicaton (e.g. Thunderbird) don't load + // using a codeBasePrincipal, but we want to block those loads. + if (isDataURI && (aLoadFromExternal || + !nsContentUtils::IsSystemPrincipal(aTriggeringPrincipal))) { + NS_ConvertUTF8toUTF16 specUTF16(aURI->GetSpecOrDefault()); + if (specUTF16.Length() > 50) { + specUTF16.Truncate(50); + specUTF16.AppendLiteral("..."); + } + const char16_t* params[] = { specUTF16.get() }; + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, + NS_LITERAL_CSTRING("DATA_URI_BLOCKED"), + // no doc available, log to browser console + nullptr, + nsContentUtils::eSECURITY_PROPERTIES, + "BlockTopLevelDataURINavigation", + params, ArrayLength(params)); + return NS_OK; } } diff --git a/docshell/base/nsDocShell.h b/docshell/base/nsDocShell.h index ab6813d273927..51907223273c9 100644 --- a/docshell/base/nsDocShell.h +++ b/docshell/base/nsDocShell.h @@ -377,6 +377,7 @@ class nsDocShell final nsIURI* aOriginalURI, mozilla::Maybe> const& aResultPrincipalURI, bool aLoadReplace, + bool aLoadFromExternal, nsIURI* aReferrer, bool aSendReferrer, uint32_t aReferrerPolicy, diff --git a/dom/locales/en-US/chrome/security/security.properties b/dom/locales/en-US/chrome/security/security.properties index 60b4a75f60f63..9456c5307ed74 100644 --- a/dom/locales/en-US/chrome/security/security.properties +++ b/dom/locales/en-US/chrome/security/security.properties @@ -81,3 +81,6 @@ MimeTypeMismatch=The resource from “%1$S” was blocked due to MIME type misma XCTOHeaderValueMissing=X-Content-Type-Options header warning: value was “%1$S”; did you mean to send “nosniff”? BlockScriptWithWrongMimeType=Script from “%1$S” was blocked because of a disallowed MIME type. + +# LOCALIZATION NOTE: Do not translate "data: URI". +BlockTopLevelDataURINavigation=Navigation to toplevel data: URI not allowed (Blocked loading of: “%1$S”) diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index 7b86945b5e2f6..f2d08582399a9 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -5757,6 +5757,12 @@ pref("security.mixed_content.hsts_priming_request_timeout", 2000); // behavior of Firefox. pref("security.data_uri.unique_opaque_origin", false); +// TODO: Bug 1380959: Block toplevel data: URI navigations +// If true, all toplevel data: URI navigations will be blocked. +// Please note that manually entering a data: URI in the +// URL-Bar will not be blocked when flipping this pref. +pref("security.data_uri.block_toplevel_data_uri_navigations", false); + // Disable Storage api in release builds. #if defined(NIGHTLY_BUILD) && !defined(MOZ_WIDGET_ANDROID) pref("dom.storageManager.enabled", true); diff --git a/netwerk/base/nsIOService.cpp b/netwerk/base/nsIOService.cpp index e7a24968e0fc9..8981e8c4a30f5 100644 --- a/netwerk/base/nsIOService.cpp +++ b/netwerk/base/nsIOService.cpp @@ -167,6 +167,7 @@ uint32_t nsIOService::gDefaultSegmentSize = 4096; uint32_t nsIOService::gDefaultSegmentCount = 24; bool nsIOService::sIsDataURIUniqueOpaqueOrigin = false; +bool nsIOService::sBlockToplevelDataUriNavigations = false; //////////////////////////////////////////////////////////////////////////////// @@ -250,6 +251,8 @@ nsIOService::Init() Preferences::AddBoolVarCache(&sIsDataURIUniqueOpaqueOrigin, "security.data_uri.unique_opaque_origin", false); + Preferences::AddBoolVarCache(&sBlockToplevelDataUriNavigations, + "security.data_uri.block_toplevel_data_uri_navigations", false); Preferences::AddBoolVarCache(&mOfflineMirrorsConnectivity, OFFLINE_MIRRORS_CONNECTIVITY, true); gIOService = this; @@ -1931,5 +1934,11 @@ nsIOService::IsDataURIUniqueOpaqueOrigin() return sIsDataURIUniqueOpaqueOrigin; } +/*static*/ bool +nsIOService::BlockToplevelDataUriNavigations() +{ + return sBlockToplevelDataUriNavigations; +} + } // namespace net } // namespace mozilla diff --git a/netwerk/base/nsIOService.h b/netwerk/base/nsIOService.h index 9ea00523f8286..5de5dd8af3ae4 100644 --- a/netwerk/base/nsIOService.h +++ b/netwerk/base/nsIOService.h @@ -96,6 +96,7 @@ class nsIOService final : public nsIIOService2 bool IsLinkUp(); static bool IsDataURIUniqueOpaqueOrigin(); + static bool BlockToplevelDataUriNavigations(); // Used to count the total number of HTTP requests made void IncrementRequestNumber() { mTotalRequests++; } @@ -186,6 +187,7 @@ class nsIOService final : public nsIIOService2 bool mNetworkNotifyChanged; static bool sIsDataURIUniqueOpaqueOrigin; + static bool sBlockToplevelDataUriNavigations; uint32_t mTotalRequests; uint32_t mCacheWon; diff --git a/toolkit/components/telemetry/Histograms.json b/toolkit/components/telemetry/Histograms.json index 3d26cca78520a..fddcc3ebc6ec3 100644 --- a/toolkit/components/telemetry/Histograms.json +++ b/toolkit/components/telemetry/Histograms.json @@ -13395,15 +13395,6 @@ "bug_numbers": [1351021], "description": "Every time we run an unlabeled runnable, this histogram records the time (in ms) since the last unlabeled runnable ran." }, - "DOCUMENT_DATA_URI_LOADS": { - "record_in_processes": ["main", "content"], - "alert_emails": ["seceng-telemetry@mozilla.com"], - "bug_numbers": [1357386], - "expires_in_version": "60", - "kind": "enumerated", - "n_values": 3, - "description": "Whether a toplevel document uses data: URI? (0=data-navigated, 1=data-typed, 2=nodata)" - }, "VFC_INVALIDATE_LOCK_WAIT_MS": { "record_in_processes": ["main", "content"], "alert_emails": ["jwwang@mozilla.com"], From e116c4627b59238539d1fad15e432717a4264b6b Mon Sep 17 00:00:00 2001 From: Christoph Kerschbaumer Date: Mon, 24 Jul 2017 18:52:01 +0200 Subject: [PATCH 07/56] Bug 1331351: Test block toplevel window data: URI navigations. r=smaug --- .../file_block_toplevel_data_navigation.html | 14 ++++ .../file_block_toplevel_data_navigation2.html | 29 +++++++ .../file_block_toplevel_data_navigation3.html | 13 ++++ dom/security/test/general/mochitest.ini | 4 + .../test_block_toplevel_data_navigation.html | 78 +++++++++++++++++++ 5 files changed, 138 insertions(+) create mode 100644 dom/security/test/general/file_block_toplevel_data_navigation.html create mode 100644 dom/security/test/general/file_block_toplevel_data_navigation2.html create mode 100644 dom/security/test/general/file_block_toplevel_data_navigation3.html create mode 100644 dom/security/test/general/test_block_toplevel_data_navigation.html diff --git a/dom/security/test/general/file_block_toplevel_data_navigation.html b/dom/security/test/general/file_block_toplevel_data_navigation.html new file mode 100644 index 0000000000000..5fbfdfdef4d6f --- /dev/null +++ b/dom/security/test/general/file_block_toplevel_data_navigation.html @@ -0,0 +1,14 @@ + + + + + Toplevel data navigation + + +test1: clicking data: URI tries to navigate window
+click me + + + diff --git a/dom/security/test/general/file_block_toplevel_data_navigation2.html b/dom/security/test/general/file_block_toplevel_data_navigation2.html new file mode 100644 index 0000000000000..e0308e1ae60ec --- /dev/null +++ b/dom/security/test/general/file_block_toplevel_data_navigation2.html @@ -0,0 +1,29 @@ + + + + + Toplevel data navigation + + +test2: data: URI in iframe tries to window.open(data:, _blank);
+ + + + diff --git a/dom/security/test/general/file_block_toplevel_data_navigation3.html b/dom/security/test/general/file_block_toplevel_data_navigation3.html new file mode 100644 index 0000000000000..34aeddab3650b --- /dev/null +++ b/dom/security/test/general/file_block_toplevel_data_navigation3.html @@ -0,0 +1,13 @@ + + + + + Toplevel data navigation + + +test3: performing data: URI navigation through win.loc.href
+ + + diff --git a/dom/security/test/general/mochitest.ini b/dom/security/test/general/mochitest.ini index 70c0c9fb6037b..ec81e55df45e3 100644 --- a/dom/security/test/general/mochitest.ini +++ b/dom/security/test/general/mochitest.ini @@ -3,7 +3,11 @@ support-files = file_contentpolicytype_targeted_link_iframe.sjs file_nosniff_testserver.sjs file_block_script_wrong_mime_server.sjs + file_block_toplevel_data_navigation.html + file_block_toplevel_data_navigation2.html + file_block_toplevel_data_navigation3.html [test_contentpolicytype_targeted_link_iframe.html] [test_nosniff.html] [test_block_script_wrong_mime.html] +[test_block_toplevel_data_navigation.html] diff --git a/dom/security/test/general/test_block_toplevel_data_navigation.html b/dom/security/test/general/test_block_toplevel_data_navigation.html new file mode 100644 index 0000000000000..453f3e6db6ad4 --- /dev/null +++ b/dom/security/test/general/test_block_toplevel_data_navigation.html @@ -0,0 +1,78 @@ + + + + + Bug 1331351 - Block top level window data: URI navigations + + + + + + + + From 2d37dad0be6cf443d6e965ae5f3d8dc8dcc63a15 Mon Sep 17 00:00:00 2001 From: Christoph Kerschbaumer Date: Mon, 24 Jul 2017 18:51:39 +0200 Subject: [PATCH 08/56] Bug 1331351: Test allow toplevel window data: URI navigations from system. r=smaug --- dom/security/test/general/browser.ini | 2 ++ .../browser_test_toplevel_data_navigations.js | 16 ++++++++++++++++ dom/security/test/moz.build | 1 + 3 files changed, 19 insertions(+) create mode 100644 dom/security/test/general/browser.ini create mode 100644 dom/security/test/general/browser_test_toplevel_data_navigations.js diff --git a/dom/security/test/general/browser.ini b/dom/security/test/general/browser.ini new file mode 100644 index 0000000000000..8f918d13c4b66 --- /dev/null +++ b/dom/security/test/general/browser.ini @@ -0,0 +1,2 @@ +[DEFAULT] +[browser_test_toplevel_data_navigations.js] diff --git a/dom/security/test/general/browser_test_toplevel_data_navigations.js b/dom/security/test/general/browser_test_toplevel_data_navigations.js new file mode 100644 index 0000000000000..df79055090948 --- /dev/null +++ b/dom/security/test/general/browser_test_toplevel_data_navigations.js @@ -0,0 +1,16 @@ +"use strict"; + +const kDataBody = "toplevel navigation to data: URI allowed"; +const kDataURI = "data:text/html," + kDataBody + ""; + +add_task(async function test_nav_data_uri_click() { + await SpecialPowers.pushPrefEnv({ + "set": [["security.data_uri.block_toplevel_data_uri_navigations", true]], + }); + await BrowserTestUtils.withNewTab(kDataURI, async function(browser) { + await ContentTask.spawn(gBrowser.selectedBrowser, {kDataBody}, async function({kDataBody}) { // eslint-disable-line + is(content.document.body.innerHTML, kDataBody, + "data: URI navigation from system should be allowed"); + }); + }); +}); diff --git a/dom/security/test/moz.build b/dom/security/test/moz.build index c0defb0bc15e4..c21ca74dff96c 100644 --- a/dom/security/test/moz.build +++ b/dom/security/test/moz.build @@ -29,5 +29,6 @@ MOCHITEST_CHROME_MANIFESTS += [ BROWSER_CHROME_MANIFESTS += [ 'csp/browser.ini', + 'general/browser.ini', 'hsts/browser.ini', ] From 43f7abdf4a449743715b40d3ce6c312a30d83e21 Mon Sep 17 00:00:00 2001 From: Andrea Marchesini Date: Mon, 24 Jul 2017 19:58:32 +0200 Subject: [PATCH 09/56] Bug 1338339 - dom/xhr/tests/browser_blobFromFile.js must use a separate file for testing instead of prefs.js, r=smaug --- dom/xhr/tests/browser_blobFromFile.js | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/dom/xhr/tests/browser_blobFromFile.js b/dom/xhr/tests/browser_blobFromFile.js index 7ff4073509aa8..d3dcdbc47e3a3 100644 --- a/dom/xhr/tests/browser_blobFromFile.js +++ b/dom/xhr/tests/browser_blobFromFile.js @@ -5,10 +5,24 @@ add_task(async function test() { {set: [["browser.tabs.remote.separateFileUriProcess", true]]} ); + let fileData = ""; + for (var i = 0; i < 100; ++i) { + fileData += "hello world!"; + } + let file = Cc["@mozilla.org/file/directory_service;1"] .getService(Ci.nsIDirectoryService) .QueryInterface(Ci.nsIProperties) .get("ProfD", Ci.nsIFile); + file.append('file.txt'); + file.createUnique(Components.interfaces.nsIFile.FILE_TYPE, 0o600); + + let outStream = Components.classes["@mozilla.org/network/file-output-stream;1"] + .createInstance(Components.interfaces.nsIFileOutputStream); + outStream.init(file, 0x02 | 0x08 | 0x20, // write, create, truncate + 0666, 0); + outStream.write(fileData, fileData.length); + outStream.close(); let fileHandler = Cc["@mozilla.org/network/io-service;1"] .getService(Ci.nsIIOService) @@ -23,11 +37,11 @@ add_task(async function test() { let browser = gBrowser.getBrowserForTab(tab); await BrowserTestUtils.browserLoaded(browser); - let blob = await ContentTask.spawn(browser, null, function() { + let blob = await ContentTask.spawn(browser, file.leafName, function(fileName) { return new content.window.Promise(resolve => { let xhr = new content.window.XMLHttpRequest(); xhr.responseType = "blob"; - xhr.open("GET", "prefs.js"); + xhr.open("GET", fileName); xhr.send(); xhr.onload = function() { resolve(xhr.response); @@ -37,9 +51,10 @@ add_task(async function test() { ok(blob instanceof File, "We have a file"); - file.append("prefs.js"); is(blob.size, file.fileSize, "The size matches"); - is(blob.name, "prefs.js", "The name is correct"); + is(blob.name, file.leafName, "The name is correct"); + + file.remove(false); gBrowser.removeTab(tab); }); From 7ea4601b8b77544c0e50144f3a9c08459838647a Mon Sep 17 00:00:00 2001 From: Gabriel Luong Date: Mon, 24 Jul 2017 14:11:26 -0400 Subject: [PATCH 10/56] Bug 1383745 - Upgrade to CodeMirror 5.28.0. r=bgrins --- .../client/sourceeditor/codemirror/README | 2 +- .../codemirror/addon/fold/foldcode.js | 2 + .../codemirror/addon/search/searchcursor.js | 12 ++-- .../codemirror/addon/tern/tern.js | 36 ++++++++---- .../codemirror/codemirror.bundle.js | 58 +++++++++++-------- .../sourceeditor/codemirror/keymap/sublime.js | 17 ++++++ .../sourceeditor/codemirror/lib/codemirror.js | 27 +++------ .../test/codemirror/search_test.js | 8 +++ .../sourceeditor/test/codemirror/test.js | 4 +- 9 files changed, 105 insertions(+), 61 deletions(-) diff --git a/devtools/client/sourceeditor/codemirror/README b/devtools/client/sourceeditor/codemirror/README index a7a883375b841..8417a67ae1090 100644 --- a/devtools/client/sourceeditor/codemirror/README +++ b/devtools/client/sourceeditor/codemirror/README @@ -5,7 +5,7 @@ code, and optionally help with indentation. # Upgrade -Currently used version is 5.27.4. To upgrade: download a new version of +Currently used version is 5.28.0. To upgrade: download a new version of CodeMirror from the project's page [1] and replace all JavaScript and CSS files inside the codemirror directory [2]. diff --git a/devtools/client/sourceeditor/codemirror/addon/fold/foldcode.js b/devtools/client/sourceeditor/codemirror/addon/fold/foldcode.js index 78b36c4641985..826766b69fc1c 100644 --- a/devtools/client/sourceeditor/codemirror/addon/fold/foldcode.js +++ b/devtools/client/sourceeditor/codemirror/addon/fold/foldcode.js @@ -65,6 +65,8 @@ widget = document.createElement("span"); widget.appendChild(text); widget.className = "CodeMirror-foldmarker"; + } else if (widget) { + widget = widget.cloneNode(true) } return widget; } diff --git a/devtools/client/sourceeditor/codemirror/addon/search/searchcursor.js b/devtools/client/sourceeditor/codemirror/addon/search/searchcursor.js index 5feedd2677dee..eccd81aab69c1 100644 --- a/devtools/client/sourceeditor/codemirror/addon/search/searchcursor.js +++ b/devtools/client/sourceeditor/codemirror/addon/search/searchcursor.js @@ -128,11 +128,13 @@ // (compensating for codepoints increasing in number during folding) function adjustPos(orig, folded, pos, foldFunc) { if (orig.length == folded.length) return pos - for (var pos1 = Math.min(pos, orig.length);;) { - var len1 = foldFunc(orig.slice(0, pos1)).length - if (len1 < pos) ++pos1 - else if (len1 > pos) --pos1 - else return pos1 + for (var min = 0, max = pos + Math.max(0, orig.length - folded.length);;) { + if (min == max) return min + var mid = (min + max) >> 1 + var len = foldFunc(orig.slice(0, mid)).length + if (len == pos) return mid + else if (len > pos) max = mid + else min = mid + 1 } } diff --git a/devtools/client/sourceeditor/codemirror/addon/tern/tern.js b/devtools/client/sourceeditor/codemirror/addon/tern/tern.js index efdf2ed628a48..644e495f6504f 100644 --- a/devtools/client/sourceeditor/codemirror/addon/tern/tern.js +++ b/devtools/client/sourceeditor/codemirror/addon/tern/tern.js @@ -334,7 +334,11 @@ tip.appendChild(document.createTextNode(tp.rettype ? ") ->\u00a0" : ")")); if (tp.rettype) tip.appendChild(elt("span", cls + "type", tp.rettype)); var place = cm.cursorCoords(null, "page"); - ts.activeArgHints = makeTooltip(place.right + 1, place.bottom, tip); + var tooltip = ts.activeArgHints = makeTooltip(place.right + 1, place.bottom, tip) + setTimeout(function() { + tooltip.clear = onEditorActivity(cm, function() { + if (ts.activeArgHints == tooltip) closeArgHints(ts) }) + }, 20) } function parseFnType(text) { @@ -604,11 +608,8 @@ } function clear() { cm.state.ternTooltip = null; - if (!tip.parentNode) return; - cm.off("cursorActivity", clear); - cm.off('blur', clear); - cm.off('scroll', clear); - fadeOut(tip); + if (tip.parentNode) fadeOut(tip) + clearActivity() } var mouseOnTip = false, old = false; CodeMirror.on(tip, "mousemove", function() { mouseOnTip = true; }); @@ -619,9 +620,20 @@ } }); setTimeout(maybeClear, ts.options.hintDelay ? ts.options.hintDelay : 1700); - cm.on("cursorActivity", clear); - cm.on('blur', clear); - cm.on('scroll', clear); + var clearActivity = onEditorActivity(cm, clear) + } + + function onEditorActivity(cm, f) { + cm.on("cursorActivity", f) + cm.on("blur", f) + cm.on("scroll", f) + cm.on("setDoc", f) + return function() { + cm.off("cursorActivity", f) + cm.off("blur", f) + cm.off("scroll", f) + cm.off("setDoc", f) + } } function makeTooltip(x, y, content) { @@ -650,7 +662,11 @@ } function closeArgHints(ts) { - if (ts.activeArgHints) { remove(ts.activeArgHints); ts.activeArgHints = null; } + if (ts.activeArgHints) { + if (ts.activeArgHints.clear) ts.activeArgHints.clear() + remove(ts.activeArgHints) + ts.activeArgHints = null + } } function docValue(ts, doc) { diff --git a/devtools/client/sourceeditor/codemirror/codemirror.bundle.js b/devtools/client/sourceeditor/codemirror/codemirror.bundle.js index 6995793e2dcc3..67c8eaa620e5d 100644 --- a/devtools/client/sourceeditor/codemirror/codemirror.bundle.js +++ b/devtools/client/sourceeditor/codemirror/codemirror.bundle.js @@ -7022,15 +7022,15 @@ var CodeMirror = {origin: "+move", bias: -1} ); }, goLineRight: function (cm) { return cm.extendSelectionsBy(function (range) { - var top = cm.charCoords(range.head, "div").top + 5 + var top = cm.cursorCoords(range.head, "div").top + 5 return cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div") }, sel_move); }, goLineLeft: function (cm) { return cm.extendSelectionsBy(function (range) { - var top = cm.charCoords(range.head, "div").top + 5 + var top = cm.cursorCoords(range.head, "div").top + 5 return cm.coordsChar({left: 0, top: top}, "div") }, sel_move); }, goLineLeftSmart: function (cm) { return cm.extendSelectionsBy(function (range) { - var top = cm.charCoords(range.head, "div").top + 5 + var top = cm.cursorCoords(range.head, "div").top + 5 var pos = cm.coordsChar({left: 0, top: top}, "div") if (pos.ch < cm.getLine(pos.line).search(/\S/)) { return lineStartSmart(cm, range.head) } return pos @@ -8585,6 +8585,8 @@ var CodeMirror = }), operation: function(f){return runInOp(this, f)}, + startOperation: function(){return startOperation(this)}, + endOperation: function(){return endOperation(this)}, refresh: methodOp(function() { var oldHeight = this.display.cachedTextHeight @@ -9237,9 +9239,6 @@ var CodeMirror = this.pollingFast = false // Self-resetting timeout for the poller this.polling = new Delayed() - // Tracks when input.reset has punted to just putting a short - // string into the textarea instead of the full selection. - this.inaccurateSelection = false // Used to work around IE issue with selection being forgotten when focus moves away from textarea this.hasSelection = false this.composing = null @@ -9276,12 +9275,6 @@ var CodeMirror = if (signalDOMEvent(cm, e)) { return } if (cm.somethingSelected()) { setLastCopied({lineWise: false, text: cm.getSelections()}) - if (input.inaccurateSelection) { - input.prevInput = "" - input.inaccurateSelection = false - te.value = lastCopied.text.join("\n") - selectInput(te) - } } else if (!cm.options.lineWiseCopyCut) { return } else { @@ -9360,13 +9353,10 @@ var CodeMirror = // when not typing and nothing is selected) TextareaInput.prototype.reset = function (typing) { if (this.contextMenuPending || this.composing) { return } - var minimal, selected, cm = this.cm, doc = cm.doc + var cm = this.cm if (cm.somethingSelected()) { this.prevInput = "" - var range = doc.sel.primary() - minimal = hasCopyEvent && - (range.to().line - range.from().line > 100 || (selected = cm.getSelection()).length > 1000) - var content = minimal ? "-" : selected || cm.getSelection() + var content = cm.getSelection() this.textarea.value = content if (cm.state.focused) { selectInput(this.textarea) } if (ie && ie_version >= 9) { this.hasSelection = content } @@ -9374,7 +9364,6 @@ var CodeMirror = this.prevInput = this.textarea.value = "" if (ie && ie_version >= 9) { this.hasSelection = null } } - this.inaccurateSelection = minimal }; TextareaInput.prototype.getField = function () { return this.textarea }; @@ -9725,7 +9714,7 @@ var CodeMirror = addLegacyProps(CodeMirror) - CodeMirror.version = "5.27.4" + CodeMirror.version = "5.28.0" return CodeMirror; @@ -9865,11 +9854,13 @@ var CodeMirror = // (compensating for codepoints increasing in number during folding) function adjustPos(orig, folded, pos, foldFunc) { if (orig.length == folded.length) return pos - for (var pos1 = Math.min(pos, orig.length);;) { - var len1 = foldFunc(orig.slice(0, pos1)).length - if (len1 < pos) ++pos1 - else if (len1 > pos) --pos1 - else return pos1 + for (var min = 0, max = pos + Math.max(0, orig.length - folded.length);;) { + if (min == max) return min + var mid = (min + max) >> 1 + var len = foldFunc(orig.slice(0, mid)).length + if (len == pos) return mid + else if (len > pos) max = mid + else min = mid + 1 } } @@ -20682,6 +20673,23 @@ var CodeMirror = cm.state.sublimeFindFullWord = cm.doc.sel; }; + function addCursorToSelection(cm, dir) { + var ranges = cm.listSelections(), newRanges = []; + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i]; + var newAnchor = cm.findPosV(range.anchor, dir, "line"); + var newHead = cm.findPosV(range.head, dir, "line"); + var newRange = {anchor: newAnchor, head: newHead}; + newRanges.push(range); + newRanges.push(newRange); + } + cm.setSelections(newRanges); + } + + var addCursorToLineCombo = mac ? "Shift-Cmd" : 'Alt-Ctrl'; + cmds[map[addCursorToLineCombo + "Up"] = "addCursorToPrevLine"] = function(cm) { addCursorToSelection(cm, -1); }; + cmds[map[addCursorToLineCombo + "Down"] = "addCursorToNextLine"] = function(cm) { addCursorToSelection(cm, 1); }; + function isSelectedRange(ranges, from, to) { for (var i = 0; i < ranges.length; i++) if (ranges[i].from() == from && ranges[i].to() == to) return true @@ -21184,6 +21192,8 @@ var CodeMirror = widget = document.createElement("span"); widget.appendChild(text); widget.className = "CodeMirror-foldmarker"; + } else if (widget) { + widget = widget.cloneNode(true) } return widget; } diff --git a/devtools/client/sourceeditor/codemirror/keymap/sublime.js b/devtools/client/sourceeditor/codemirror/keymap/sublime.js index 0ce89558750c1..98266e44f07cf 100644 --- a/devtools/client/sourceeditor/codemirror/keymap/sublime.js +++ b/devtools/client/sourceeditor/codemirror/keymap/sublime.js @@ -165,6 +165,23 @@ cm.state.sublimeFindFullWord = cm.doc.sel; }; + function addCursorToSelection(cm, dir) { + var ranges = cm.listSelections(), newRanges = []; + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i]; + var newAnchor = cm.findPosV(range.anchor, dir, "line"); + var newHead = cm.findPosV(range.head, dir, "line"); + var newRange = {anchor: newAnchor, head: newHead}; + newRanges.push(range); + newRanges.push(newRange); + } + cm.setSelections(newRanges); + } + + var addCursorToLineCombo = mac ? "Shift-Cmd" : 'Alt-Ctrl'; + cmds[map[addCursorToLineCombo + "Up"] = "addCursorToPrevLine"] = function(cm) { addCursorToSelection(cm, -1); }; + cmds[map[addCursorToLineCombo + "Down"] = "addCursorToNextLine"] = function(cm) { addCursorToSelection(cm, 1); }; + function isSelectedRange(ranges, from, to) { for (var i = 0; i < ranges.length; i++) if (ranges[i].from() == from && ranges[i].to() == to) return true diff --git a/devtools/client/sourceeditor/codemirror/lib/codemirror.js b/devtools/client/sourceeditor/codemirror/lib/codemirror.js index 9e084ffb7ae4d..aba145d878b7e 100644 --- a/devtools/client/sourceeditor/codemirror/lib/codemirror.js +++ b/devtools/client/sourceeditor/codemirror/lib/codemirror.js @@ -6780,15 +6780,15 @@ var commands = { {origin: "+move", bias: -1} ); }, goLineRight: function (cm) { return cm.extendSelectionsBy(function (range) { - var top = cm.charCoords(range.head, "div").top + 5 + var top = cm.cursorCoords(range.head, "div").top + 5 return cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div") }, sel_move); }, goLineLeft: function (cm) { return cm.extendSelectionsBy(function (range) { - var top = cm.charCoords(range.head, "div").top + 5 + var top = cm.cursorCoords(range.head, "div").top + 5 return cm.coordsChar({left: 0, top: top}, "div") }, sel_move); }, goLineLeftSmart: function (cm) { return cm.extendSelectionsBy(function (range) { - var top = cm.charCoords(range.head, "div").top + 5 + var top = cm.cursorCoords(range.head, "div").top + 5 var pos = cm.coordsChar({left: 0, top: top}, "div") if (pos.ch < cm.getLine(pos.line).search(/\S/)) { return lineStartSmart(cm, range.head) } return pos @@ -8343,6 +8343,8 @@ function addEditorMethods(CodeMirror) { }), operation: function(f){return runInOp(this, f)}, + startOperation: function(){return startOperation(this)}, + endOperation: function(){return endOperation(this)}, refresh: methodOp(function() { var oldHeight = this.display.cachedTextHeight @@ -8995,9 +8997,6 @@ var TextareaInput = function(cm) { this.pollingFast = false // Self-resetting timeout for the poller this.polling = new Delayed() - // Tracks when input.reset has punted to just putting a short - // string into the textarea instead of the full selection. - this.inaccurateSelection = false // Used to work around IE issue with selection being forgotten when focus moves away from textarea this.hasSelection = false this.composing = null @@ -9034,12 +9033,6 @@ TextareaInput.prototype.init = function (display) { if (signalDOMEvent(cm, e)) { return } if (cm.somethingSelected()) { setLastCopied({lineWise: false, text: cm.getSelections()}) - if (input.inaccurateSelection) { - input.prevInput = "" - input.inaccurateSelection = false - te.value = lastCopied.text.join("\n") - selectInput(te) - } } else if (!cm.options.lineWiseCopyCut) { return } else { @@ -9118,13 +9111,10 @@ TextareaInput.prototype.showSelection = function (drawn) { // when not typing and nothing is selected) TextareaInput.prototype.reset = function (typing) { if (this.contextMenuPending || this.composing) { return } - var minimal, selected, cm = this.cm, doc = cm.doc + var cm = this.cm if (cm.somethingSelected()) { this.prevInput = "" - var range = doc.sel.primary() - minimal = hasCopyEvent && - (range.to().line - range.from().line > 100 || (selected = cm.getSelection()).length > 1000) - var content = minimal ? "-" : selected || cm.getSelection() + var content = cm.getSelection() this.textarea.value = content if (cm.state.focused) { selectInput(this.textarea) } if (ie && ie_version >= 9) { this.hasSelection = content } @@ -9132,7 +9122,6 @@ TextareaInput.prototype.reset = function (typing) { this.prevInput = this.textarea.value = "" if (ie && ie_version >= 9) { this.hasSelection = null } } - this.inaccurateSelection = minimal }; TextareaInput.prototype.getField = function () { return this.textarea }; @@ -9483,7 +9472,7 @@ CodeMirror.fromTextArea = fromTextArea addLegacyProps(CodeMirror) -CodeMirror.version = "5.27.4" +CodeMirror.version = "5.28.0" return CodeMirror; diff --git a/devtools/client/sourceeditor/test/codemirror/search_test.js b/devtools/client/sourceeditor/test/codemirror/search_test.js index 7a6a6604614c9..e3188de529882 100644 --- a/devtools/client/sourceeditor/test/codemirror/search_test.js +++ b/devtools/client/sourceeditor/test/codemirror/search_test.js @@ -74,4 +74,12 @@ run(doc, "", true, 0, 8, 0, 12, 1, 8, 1, 12); run(doc, "İİ", true, 0, 3, 0, 5, 0, 6, 0, 8); }); + + test("normalize", function() { + if (!String.prototype.normalize) return + var doc = new CodeMirror.Doc("yılbaşı\n수 있을까\nLe taux d'humidité à London") + run(doc, "s", false, 0, 5, 0, 6) + run(doc, "이", false, 1, 2, 1, 3) + run(doc, "a", false, 0, 4, 0, 5, 2, 4, 2, 5, 2, 19, 2, 20) + }) })(); diff --git a/devtools/client/sourceeditor/test/codemirror/test.js b/devtools/client/sourceeditor/test/codemirror/test.js index a3b5d63c55b76..59b760d5ae0ea 100644 --- a/devtools/client/sourceeditor/test/codemirror/test.js +++ b/devtools/client/sourceeditor/test/codemirror/test.js @@ -1592,7 +1592,7 @@ testCM("lineWidgetChanged", function(cm) { // Good: // | ------------- display width ------------- | // | ------- widget-width when measured ------ | - // | | -- under-half -- | | -- under-half -- | | + // | | -- under-half -- | | -- under-half -- | | // | | --- over-half --- | | // | | --- over-half --- | | // Height: measured as 3 lines, same as it will be when actually displayed @@ -1609,7 +1609,7 @@ testCM("lineWidgetChanged", function(cm) { // Bad (too wide): // | ------------- display width ------------- | // | -------- widget-width when measured ------- | < -- uh oh - // | | -- under-half -- | | -- under-half -- | | + // | | -- under-half -- | | -- under-half -- | | // | | --- over-half --- | | --- over-half --- | | < -- when measured, combined on one line // Height: measured as 2 lines, less than expected. Will be displayed as 3 lines! From 68f83ebade1c29fa3b3fddd732305ddba10facce Mon Sep 17 00:00:00 2001 From: Lee Salzman Date: Mon, 24 Jul 2017 14:12:26 -0400 Subject: [PATCH 11/56] Bug 1383817 - clamp gamma/contrast for ScaledFontDWrite when creating SkTypeface. r=mchang MozReview-Commit-ID: LSSffVooDCI --- gfx/2d/ScaledFontDWrite.cpp | 14 +++++++++++++- gfx/thebes/gfxWindowsPlatform.cpp | 8 -------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/gfx/2d/ScaledFontDWrite.cpp b/gfx/2d/ScaledFontDWrite.cpp index 7d2e6901efde3..a1d91b17f871c 100644 --- a/gfx/2d/ScaledFontDWrite.cpp +++ b/gfx/2d/ScaledFontDWrite.cpp @@ -156,7 +156,19 @@ ScaledFontDWrite::GetSkTypeface() return nullptr; } - mTypeface = SkCreateTypefaceFromDWriteFont(factory, mFontFace, mStyle, mForceGDIMode, mGamma, mContrast); + Float gamma = mGamma; + // Skia doesn't support a gamma value outside of 0-4, so default to 2.2 + if (gamma < 0.0f || gamma > 4.0f) { + gamma = 2.2f; + } + + Float contrast = mContrast; + // Skia doesn't support a contrast value outside of 0-1, so default to 1.0 + if (contrast < 0.0f || contrast > 1.0f) { + contrast = 1.0f; + } + + mTypeface = SkCreateTypefaceFromDWriteFont(factory, mFontFace, mStyle, mForceGDIMode, gamma, contrast); } return mTypeface; } diff --git a/gfx/thebes/gfxWindowsPlatform.cpp b/gfx/thebes/gfxWindowsPlatform.cpp index 57c5a18ea0751..b0d00b103dd88 100755 --- a/gfx/thebes/gfxWindowsPlatform.cpp +++ b/gfx/thebes/gfxWindowsPlatform.cpp @@ -1193,14 +1193,6 @@ gfxWindowsPlatform::SetupClearTypeParams() } } - if (GetDefaultContentBackend() == BackendType::SKIA) { - // Skia doesn't support a contrast value outside of 0-1, so default to 1.0 - if (contrast < 0.0 || contrast > 1.0) { - NS_WARNING("Custom dwrite contrast not supported in Skia. Defaulting to 1.0."); - contrast = 1.0; - } - } - // For parameters that have not been explicitly set, // we copy values from default params (or our overridden value for contrast) if (gamma < 1.0 || gamma > 2.2) { From ed40ba307a56967f88f80bced7ad1fd269e9ee83 Mon Sep 17 00:00:00 2001 From: Mike Park Date: Wed, 5 Jul 2017 10:57:42 -0400 Subject: [PATCH 12/56] Bug 1282717 - Highlight CSS shapes points in the page from the rule-view and vice versa. r=pbro MozReview-Commit-ID: 9pXkbAwgcXO --- devtools/client/inspector/rules/rules.js | 61 ++ .../rules/views/text-property-editor.js | 68 ++- .../inspector/shared/highlighters-overlay.js | 135 ++++- .../client/inspector/shared/node-types.js | 1 + devtools/client/inspector/test/browser.ini | 1 + ...owser_inspector_highlighter-cssshape_05.js | 110 ++++ devtools/client/inspector/test/head.js | 30 + devtools/client/shared/output-parser.js | 526 +++++++++++++++++- .../shared/test/browser_outputparser.js | 120 ++++ devtools/client/themes/rules.css | 4 + devtools/server/actors/highlighters.css | 4 + devtools/server/actors/highlighters.js | 13 + devtools/server/actors/highlighters/shapes.js | 191 ++++++- devtools/shared/specs/highlighters.js | 7 + 14 files changed, 1240 insertions(+), 31 deletions(-) create mode 100644 devtools/client/inspector/test/browser_inspector_highlighter-cssshape_05.js diff --git a/devtools/client/inspector/rules/rules.js b/devtools/client/inspector/rules/rules.js index 9a65157f53b2a..cd93e9cb9aaea 100644 --- a/devtools/client/inspector/rules/rules.js +++ b/devtools/client/inspector/rules/rules.js @@ -26,6 +26,7 @@ const { VIEW_NODE_VALUE_TYPE, VIEW_NODE_IMAGE_URL_TYPE, VIEW_NODE_LOCATION_TYPE, + VIEW_NODE_SHAPE_POINT_TYPE, } = require("devtools/client/inspector/shared/node-types"); const StyleInspectorMenu = require("devtools/client/inspector/shared/style-inspector-menu"); const TooltipsOverlay = require("devtools/client/inspector/shared/tooltips-overlay"); @@ -48,6 +49,7 @@ const FILTER_PROP_RE = /\s*([^:\s]*)\s*:\s*(.*?)\s*;?$/; // This is used to parse the filter search value to see if the filter // should be strict or not const FILTER_STRICT_RE = /\s*`(.*?)`\s*$/; +const INSET_POINT_TYPES = ["top", "right", "bottom", "left"]; /** * Our model looks like this: @@ -333,6 +335,19 @@ CssRuleView.prototype = { sheetHref: prop.rule.domRule.href, textProperty: prop }; + } else if (classes.contains("ruleview-shape-point") && prop) { + type = VIEW_NODE_SHAPE_POINT_TYPE; + value = { + property: getPropertyNameAndValue(node).name, + value: node.textContent, + enabled: prop.enabled, + overridden: prop.overridden, + pseudoElement: prop.rule.pseudoElement, + sheetHref: prop.rule.domRule.href, + textProperty: prop, + toggleActive: getShapeToggleActive(node), + point: getShapePoint(node) + }; } else if (classes.contains("theme-link") && !classes.contains("ruleview-rule-source") && prop) { type = VIEW_NODE_IMAGE_URL_TYPE; @@ -1539,6 +1554,52 @@ function getPropertyNameAndValue(node) { } } +/** + * Walk up the DOM from a given node until a parent property holder is found, + * and return an active shape toggle if one exists. + * + * @param {DOMNode} node + * The node to start from + * @returns {DOMNode} The active shape toggle node, if one exists. + */ +function getShapeToggleActive(node) { + while (true) { + if (!node || !node.classList) { + return null; + } + // Check first for ruleview-computed since it's the deepest + if (node.classList.contains("ruleview-computed") || + node.classList.contains("ruleview-property")) { + return node.querySelector(".ruleview-shape.active"); + } + node = node.parentNode; + } +} + +/** + * Get the point associated with a shape point node. + * + * @param {DOMNode} node + * A shape point node + * @returns {String} The point associated with the given node. + */ +function getShapePoint(node) { + let classList = node.classList; + let point = node.dataset.point; + // Inset points use classes instead of data because a single span can represent + // multiple points. + let insetClasses = []; + classList.forEach(className => { + if (INSET_POINT_TYPES.includes(className)) { + insetClasses.push(className); + } + }); + if (insetClasses.length > 0) { + point = insetClasses.join(","); + } + return point; +} + function RuleViewTool(inspector, window) { this.inspector = inspector; this.document = window.document; diff --git a/devtools/client/inspector/rules/views/text-property-editor.js b/devtools/client/inspector/rules/views/text-property-editor.js index 532ec7b873536..ae3de6e272ce2 100644 --- a/devtools/client/inspector/rules/views/text-property-editor.js +++ b/devtools/client/inspector/rules/views/text-property-editor.js @@ -27,6 +27,7 @@ const COLOR_SWATCH_CLASS = "ruleview-colorswatch"; const BEZIER_SWATCH_CLASS = "ruleview-bezierswatch"; const FILTER_SWATCH_CLASS = "ruleview-filterswatch"; const ANGLE_SWATCH_CLASS = "ruleview-angleswatch"; +const INSET_POINT_TYPES = ["top", "right", "bottom", "left"]; /* * An actionable element is an element which on click triggers a specific action @@ -78,6 +79,7 @@ function TextPropertyEditor(ruleEditor, property) { this._onValidate = this.ruleView.debounce(this._previewValue, 10, this); this.update = this.update.bind(this); this.updatePropertyState = this.updatePropertyState.bind(this); + this._onHoverShapePoint = this._onHoverShapePoint.bind(this); this._create(); this.update(); @@ -300,6 +302,8 @@ TextPropertyEditor.prototype = { cssProperties: this.cssProperties, contextMenu: this.ruleView.inspector.onTextBoxContextMenu }); + + this.ruleView.highlighters.on("hover-shape-point", this._onHoverShapePoint); } }, @@ -454,6 +458,7 @@ TextPropertyEditor.prototype = { if (highlighters.shapesHighlighterShown === inspector.selection.nodeFront && highlighters.state.shapes.options.mode === mode) { shapeToggle.classList.add("active"); + highlighters.highlightRuleViewShapePoint(highlighters.state.shapes.hoverPoint); } } @@ -944,7 +949,68 @@ TextPropertyEditor.prototype = { return this.prop.name === "display" && (this.prop.value === "grid" || this.prop.value === "inline-grid"); - } + }, + + /** + * Highlight the given shape point in the rule view. Called when "hover-shape-point" + * event is emitted. + * + * @param {Event} event + * The "hover-shape-point" event. + * @param {String} point + * The point to highlight. + */ + _onHoverShapePoint: function (event, point) { + // If there is no shape toggle, or it is not active, return. + let shapeToggle = this.valueSpan.querySelector(".ruleview-shape.active"); + if (!shapeToggle) { + return; + } + + let view = this.ruleView; + let { highlighters } = view; + let ruleViewEl = view.element; + let selector = `.ruleview-shape-point.active`; + for (let pointNode of ruleViewEl.querySelectorAll(selector)) { + this._toggleShapePointActive(pointNode, false); + } + + if (typeof point === "string") { + if (point.includes(",")) { + point = point.split(",")[0]; + } + // Because one inset value can represent multiple points, inset points use classes + // instead of data. + selector = (INSET_POINT_TYPES.includes(point)) ? + `.ruleview-shape-point.${point}` : + `.ruleview-shape-point[data-point='${point}']`; + for (let pointNode of this.valueSpan.querySelectorAll(selector)) { + let nodeInfo = view.getNodeInfo(pointNode); + if (highlighters.isRuleViewShapePoint(nodeInfo)) { + this._toggleShapePointActive(pointNode, true); + } + } + } + }, + + /** + * Toggle the class "active" on the given shape point in the rule view if the current + * inspector selection is highlighted by the shapes highlighter. + * + * @param {NodeFront} node + * The NodeFront of the shape point to toggle + * @param {Boolean} active + * Whether the shape point should be active + */ + _toggleShapePointActive: function (node, active) { + let { highlighters } = this.ruleView; + if (highlighters.inspector.selection.nodeFront != + highlighters.shapesHighlighterShown) { + return; + } + + node.classList.toggle("active", active); + }, }; exports.TextPropertyEditor = TextPropertyEditor; diff --git a/devtools/client/inspector/shared/highlighters-overlay.js b/devtools/client/inspector/shared/highlighters-overlay.js index e1a8335960103..a64ee97e92d0e 100644 --- a/devtools/client/inspector/shared/highlighters-overlay.js +++ b/devtools/client/inspector/shared/highlighters-overlay.js @@ -9,9 +9,13 @@ const Services = require("Services"); const {Task} = require("devtools/shared/task"); const EventEmitter = require("devtools/shared/event-emitter"); -const { VIEW_NODE_VALUE_TYPE } = require("devtools/client/inspector/shared/node-types"); +const { + VIEW_NODE_VALUE_TYPE, + VIEW_NODE_SHAPE_POINT_TYPE +} = require("devtools/client/inspector/shared/node-types"); const DEFAULT_GRID_COLOR = "#4B0082"; +const INSET_POINT_TYPES = ["top", "right", "bottom", "left"]; /** * Highlighters overlay is a singleton managing all highlighters in the Inspector. @@ -52,6 +56,7 @@ function HighlightersOverlay(inspector) { this.showGridHighlighter = this.showGridHighlighter.bind(this); this.showShapesHighlighter = this.showShapesHighlighter.bind(this); this._handleRejection = this._handleRejection.bind(this); + this._onHighlighterEvent = this._onHighlighterEvent.bind(this); // Add inspector events, not specific to a given view. this.inspector.on("markupmutation", this.onMarkupMutation); @@ -180,6 +185,51 @@ HighlightersOverlay.prototype = { this.state.shapes = {}; }), + /** + * Show the shapes highlighter for the given element, with the given point highlighted. + * + * @param {NodeFront} node + * The NodeFront of the element to highlight. + * @param {String} point + * The point to highlight in the shapes highlighter. + */ + hoverPointShapesHighlighter: Task.async(function* (node, point) { + if (node == this.shapesHighlighterShown) { + let options = Object.assign({}, this.state.shapes.options); + options.hoverPoint = point; + yield this.showShapesHighlighter(node, options); + } + }), + + /** + * Highlight the given shape point in the rule view. + * + * @param {String} point + * The point to highlight. + */ + highlightRuleViewShapePoint: function (point) { + let view = this.inspector.getPanel("ruleview").view; + let ruleViewEl = view.element; + let selector = `.ruleview-shape-point.active`; + for (let pointNode of ruleViewEl.querySelectorAll(selector)) { + this._toggleShapePointActive(pointNode, false); + } + + if (point !== null && point !== undefined) { + // Because one inset value can represent multiple points, inset points use classes + // instead of data. + selector = (INSET_POINT_TYPES.includes(point)) ? + `.ruleview-shape-point.${point}` : + `.ruleview-shape-point[data-point='${point}']`; + for (let pointNode of ruleViewEl.querySelectorAll(selector)) { + let nodeInfo = view.getNodeInfo(pointNode); + if (this.isRuleViewShapePoint(nodeInfo)) { + this._toggleShapePointActive(pointNode, true); + } + } + } + }, + /** * Toggle the grid highlighter for the given grid container element. * @@ -319,22 +369,36 @@ HighlightersOverlay.prototype = { this.geometryEditorHighlighterShown = null; }), + /** + * Handle events emitted by the highlighter. + * + * @param {Object} data + * The data object sent in the event. + */ + _onHighlighterEvent: function (data) { + if (data.type === "shape-hover-on") { + this.state.shapes.hoverPoint = data.point; + this.emit("hover-shape-point", data.point); + } else if (data.type === "shape-hover-off") { + this.state.shapes.hoverPoint = null; + this.emit("hover-shape-point", null); + } + this.emit("highlighter-event-handled"); + }, + /** * Restore the saved highlighter states. * @param {String} name * The name of the highlighter to be restored - * @param {String} selector - * The selector of the node that was previously highlighted - * @param {Object} options - * The options previously supplied to the highlighter - * @param {String} url - * The URL of the page the highlighter was active on + * @param {Object} state + * The state of the highlighter to be restored * @param {Function} showFunction * The function that shows the highlighter * @return {Promise} that resolves when the highlighter state was restored, and the * expected highlighters are displayed. */ - restoreState: Task.async(function* (name, {selector, options, url}, showFunction) { + restoreState: Task.async(function* (name, state, showFunction) { + let { selector, options, url } = state; if (!selector || url !== this.inspector.target.url) { // Bail out if no selector was saved, or if we are on a different page. this.emit(`${name}-state-restored`, { restored: false }); @@ -349,6 +413,9 @@ HighlightersOverlay.prototype = { let nodeFront = yield walker.querySelector(rootNode, selector); if (nodeFront) { + if (options.hoverPoint) { + options.hoverPoint = null; + } yield showFunction(nodeFront, options); this.emit(`${name}-state-restored`, { restored: true }); } @@ -382,6 +449,7 @@ HighlightersOverlay.prototype = { return null; } + highlighter.on("highlighter-event", this._onHighlighterEvent); this.highlighters[type] = highlighter; return highlighter; }), @@ -415,6 +483,23 @@ HighlightersOverlay.prototype = { } }, + /** + * Toggle the class "active" on the given shape point in the rule view if the current + * inspector selection is highlighted by the shapes highlighter. + * + * @param {NodeFront} node + * The NodeFront of the shape point to toggle + * @param {Boolean} active + * Whether the shape point should be active + */ + _toggleShapePointActive: function (node, active) { + if (this.inspector.selection.nodeFront != this.shapesHighlighterShown) { + return; + } + + node.classList.toggle("active", active); + }, + /** * Hide the currently shown hovered highlighter. */ @@ -487,6 +572,22 @@ HighlightersOverlay.prototype = { return this.isRuleView && isTransform && isEnabled; }, + /** + * Is the current hovered node a highlightable shape point in the rule-view. + * + * @param {Object} nodeInfo + * @return {Boolean} + */ + isRuleViewShapePoint: function (nodeInfo) { + let isShape = nodeInfo.type === VIEW_NODE_SHAPE_POINT_TYPE && + (nodeInfo.value.property === "clip-path" || + nodeInfo.value.property === "shape-outside"); + let isEnabled = nodeInfo.value.enabled && + !nodeInfo.value.overridden && + !nodeInfo.value.pseudoElement; + return this.isRuleView && isShape && isEnabled && nodeInfo.value.toggleActive; + }, + onClick: function (event) { if (this._isRuleViewDisplayGrid(event.target)) { event.stopPropagation(); @@ -526,6 +627,13 @@ HighlightersOverlay.prototype = { return; } + if (this.isRuleViewShapePoint(nodeInfo)) { + let { point } = nodeInfo.value; + this.hoverPointShapesHighlighter(this.inspector.selection.nodeFront, point); + this.emit("hover-shape-point", point); + return; + } + // Choose the type of highlighter required for the hovered node. let type; if (this._isRuleViewTransform(nodeInfo) || @@ -554,6 +662,14 @@ HighlightersOverlay.prototype = { } // Otherwise, hide the highlighter. + let view = this.isRuleView ? + this.inspector.getPanel("ruleview").view : + this.inspector.getPanel("computedview").computedView; + let nodeInfo = view.getNodeInfo(this._lastHovered); + if (nodeInfo && this.isRuleViewShapePoint(nodeInfo)) { + this.hoverPointShapesHighlighter(this.inspector.selection.nodeFront, null); + this.emit("hover-shape-point", null); + } this._lastHovered = null; this._hideHoveredHighlighter(); }, @@ -630,6 +746,9 @@ HighlightersOverlay.prototype = { destroy: function () { for (let type in this.highlighters) { if (this.highlighters[type]) { + if (this.highlighters[type].off) { + this.highlighters[type].off("highlighter-event", this._onHighlighterEvent); + } this.highlighters[type].finalize(); this.highlighters[type] = null; } diff --git a/devtools/client/inspector/shared/node-types.js b/devtools/client/inspector/shared/node-types.js index 4f31ee9fe254a..919d96b4b0de7 100644 --- a/devtools/client/inspector/shared/node-types.js +++ b/devtools/client/inspector/shared/node-types.js @@ -15,3 +15,4 @@ exports.VIEW_NODE_PROPERTY_TYPE = 2; exports.VIEW_NODE_VALUE_TYPE = 3; exports.VIEW_NODE_IMAGE_URL_TYPE = 4; exports.VIEW_NODE_LOCATION_TYPE = 5; +exports.VIEW_NODE_SHAPE_POINT_TYPE = 6; diff --git a/devtools/client/inspector/test/browser.ini b/devtools/client/inspector/test/browser.ini index da2e9002d478a..09d35b78d5317 100644 --- a/devtools/client/inspector/test/browser.ini +++ b/devtools/client/inspector/test/browser.ini @@ -79,6 +79,7 @@ skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keybo [browser_inspector_highlighter-cssshape_02.js] [browser_inspector_highlighter-cssshape_03.js] [browser_inspector_highlighter-cssshape_04.js] +[browser_inspector_highlighter-cssshape_05.js] [browser_inspector_highlighter-csstransform_01.js] [browser_inspector_highlighter-csstransform_02.js] [browser_inspector_highlighter-embed.js] diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_05.js b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_05.js new file mode 100644 index 0000000000000..58a3ed21a4cdf --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_05.js @@ -0,0 +1,110 @@ +/* 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"; + +// Test hovering over shape points in the rule-view and shapes highlighter. + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter_cssshapes.html"; + +const HIGHLIGHTER_TYPE = "ShapesHighlighter"; +const CSS_SHAPES_ENABLED_PREF = "devtools.inspector.shapesHighlighter.enabled"; + +add_task(function* () { + yield pushPref(CSS_SHAPES_ENABLED_PREF, true); + let env = yield openInspectorForURL(TEST_URL); + let helper = yield getHighlighterHelperFor(HIGHLIGHTER_TYPE)(env); + let { testActor, inspector } = env; + let view = selectRuleView(inspector); + let highlighters = view.highlighters; + + yield highlightFromRuleView(inspector, view, highlighters, testActor); + yield highlightFromHighlighter(view, highlighters, testActor, helper); +}); + +function* highlightFromRuleView(inspector, view, highlighters, testActor) { + yield selectNode("#polygon", inspector); + yield toggleShapesHighlighter(view, highlighters, "#polygon", "clip-path", true); + let container = getRuleViewProperty(view, "#polygon", "clip-path").valueSpan; + let shapesToggle = container.querySelector(".ruleview-shape"); + + let highlighterFront = highlighters.highlighters[HIGHLIGHTER_TYPE]; + let markerHidden = yield testActor.getHighlighterNodeAttribute( + "shapes-marker-hover", "hidden", highlighterFront); + ok(markerHidden, "Hover marker on highlighter is not visible"); + + info("Hover over point 0 in rule view"); + let pointSpan = container.querySelector(".ruleview-shape-point[data-point='0']"); + let onHighlighterShown = highlighters.once("shapes-highlighter-shown"); + EventUtils.synthesizeMouseAtCenter(pointSpan, {type: "mousemove"}, view.styleWindow); + yield onHighlighterShown; + + ok(pointSpan.classList.contains("active"), "Hovered span is active"); + is(highlighters.state.shapes.options.hoverPoint, "0", + "Hovered point is saved to state"); + + markerHidden = yield testActor.getHighlighterNodeAttribute( + "shapes-marker-hover", "hidden", highlighterFront); + ok(!markerHidden, "Marker on highlighter is visible"); + + info("Move mouse off point"); + onHighlighterShown = highlighters.once("shapes-highlighter-shown"); + EventUtils.synthesizeMouseAtCenter(shapesToggle, {type: "mousemove"}, view.styleWindow); + yield onHighlighterShown; + + ok(!pointSpan.classList.contains("active"), "Hovered span is no longer active"); + is(highlighters.state.shapes.options.hoverPoint, null, "Hovered point is null"); + + markerHidden = yield testActor.getHighlighterNodeAttribute( + "shapes-marker-hover", "hidden", highlighterFront); + ok(markerHidden, "Marker on highlighter is not visible"); + + info("Hide shapes highlighter"); + yield toggleShapesHighlighter(view, highlighters, "#polygon", "clip-path", false); +} + +function* highlightFromHighlighter(view, highlighters, testActor, helper) { + let highlighterFront = highlighters.highlighters[HIGHLIGHTER_TYPE]; + let { mouse } = helper; + + yield toggleShapesHighlighter(view, highlighters, "#polygon", "clip-path", true); + let container = getRuleViewProperty(view, "#polygon", "clip-path").valueSpan; + + info("Hover over first point in highlighter"); + let onEventHandled = highlighters.once("highlighter-event-handled"); + yield mouse.move(0, 0); + yield onEventHandled; + let markerHidden = yield testActor.getHighlighterNodeAttribute( + "shapes-marker-hover", "hidden", highlighterFront); + ok(!markerHidden, "Marker on highlighter is visible"); + + let pointSpan = container.querySelector(".ruleview-shape-point[data-point='0']"); + ok(pointSpan.classList.contains("active"), "Span for point 0 is active"); + is(highlighters.state.shapes.hoverPoint, "0", "Hovered point is saved to state"); + + info("Check that point is still highlighted after moving it"); + yield mouse.down(0, 0); + yield mouse.move(10, 10); + yield mouse.up(10, 10); + markerHidden = yield testActor.getHighlighterNodeAttribute( + "shapes-marker-hover", "hidden", highlighterFront); + ok(!markerHidden, "Marker on highlighter is visible after moving point"); + + container = getRuleViewProperty(view, "element", "clip-path").valueSpan; + pointSpan = container.querySelector(".ruleview-shape-point[data-point='0']"); + ok(pointSpan.classList.contains("active"), + "Span for point 0 is active after moving point"); + is(highlighters.state.shapes.hoverPoint, "0", + "Hovered point is saved to state after moving point"); + + info("Move mouse off point"); + onEventHandled = highlighters.once("highlighter-event-handled"); + yield mouse.move(100, 100); + yield onEventHandled; + markerHidden = yield testActor.getHighlighterNodeAttribute( + "shapes-marker-hover", "hidden", highlighterFront); + ok(markerHidden, "Marker on highlighter is no longer visible"); + ok(!pointSpan.classList.contains("active"), "Span for point 0 is no longer active"); + is(highlighters.state.shapes.hoverPoint, null, "Hovered point is null"); +} diff --git a/devtools/client/inspector/test/head.js b/devtools/client/inspector/test/head.js index 03974581b7273..e6d6758c96165 100644 --- a/devtools/client/inspector/test/head.js +++ b/devtools/client/inspector/test/head.js @@ -803,3 +803,33 @@ function* getDisplayedNodeTextContent(selector, inspector) { } return null; } + +/** + * Toggle the shapes highlighter by simulating a click on the toggle + * in the rules view with the given selector and property + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {Object} highlighters + * The highlighters instance of the rule-view panel + * @param {String} selector + * The selector in the rule-view to look for the property in + * @param {String} property + * The name of the property + * @param {Boolean} show + * If true, the shapes highlighter is being shown. If false, it is being hidden + */ +function* toggleShapesHighlighter(view, highlighters, selector, property, show) { + info("Toggle shapes highlighter"); + let container = getRuleViewProperty(view, selector, property).valueSpan; + let shapesToggle = container.querySelector(".ruleview-shape"); + if (show) { + let onHighlighterShown = highlighters.once("shapes-highlighter-shown"); + shapesToggle.click(); + yield onHighlighterShown; + } else { + let onHighlighterHidden = highlighters.once("shapes-highlighter-hidden"); + shapesToggle.click(); + yield onHighlighterHidden; + } +} diff --git a/devtools/client/shared/output-parser.js b/devtools/client/shared/output-parser.js index b5db9637ed73c..64b86808a3778 100644 --- a/devtools/client/shared/output-parser.js +++ b/devtools/client/shared/output-parser.js @@ -15,6 +15,7 @@ const { COLOR_TAKING_FUNCTIONS, CSS_TYPES } = require("devtools/shared/css/properties-db"); +const {appendText} = require("devtools/client/inspector/shared/utils"); const Services = require("Services"); const HTML_NS = "http://www.w3.org/1999/xhtml"; @@ -360,21 +361,538 @@ OutputParser.prototype = { this.parsed.push(container); }, + /** + * Append a CSS shapes highlighter toggle next to the value, and parse the value + * into spans, each containing a point that can be hovered over. + * + * @param {String} shape + * The shape text value to append + * @param {Object} options + * Options object. For valid options and default values see + * _mergeOptions() + */ _appendShape: function (shape, options) { + const shapeTypes = [{ + prefix: "polygon(", + coordParser: this._addPolygonPointNodes.bind(this) + }, { + prefix: "circle(", + coordParser: this._addCirclePointNodes.bind(this) + }, { + prefix: "ellipse(", + coordParser: this._addEllipsePointNodes.bind(this) + }, { + prefix: "inset(", + coordParser: this._addInsetPointNodes.bind(this) + }]; + let container = this._createNode("span", {}); let toggle = this._createNode("span", { class: options.shapeClass }); - let value = this._createNode("span", {}); - value.textContent = shape; + for (let { prefix, coordParser } of shapeTypes) { + if (shape.includes(prefix)) { + let coordsBegin = prefix.length; + let coordsEnd = shape.lastIndexOf(")"); + let valContainer = this._createNode("span", {}); + + container.appendChild(toggle); + + appendText(valContainer, shape.substring(0, coordsBegin)); + + let coordsString = shape.substring(coordsBegin, coordsEnd); + valContainer = coordParser(coordsString, valContainer); + + appendText(valContainer, shape.substring(coordsEnd)); + container.appendChild(valContainer); + } + } - container.appendChild(toggle); - container.appendChild(value); this.parsed.push(container); }, + /** + * Parse the given polygon coordinates and create a span for each coordinate pair, + * adding it to the given container node. + * + * @param {String} coords + * The string of coordinate pairs. + * @param {Node} container + * The node to which spans containing points are added. + * @returns {Node} The container to which spans have been added. + */ + _addPolygonPointNodes: function (coords, container) { + let tokenStream = getCSSLexer(coords); + let token = tokenStream.nextToken(); + let coord = ""; + let i = 0; + let depth = 0; + let isXCoord = true; + let fillRule = false; + let coordNode = this._createNode("span", { + class: "ruleview-shape-point", + "data-point": `${i}`, + }); + + while (token) { + if (token.tokenType === "symbol" && token.text === ",") { + // Comma separating coordinate pairs; add coordNode to container and reset vars + if (!isXCoord) { + // Y coord not added to coordNode yet + let node = this._createNode("span", { + class: "ruleview-shape-point", + "data-point": `${i}`, + "data-pair": (isXCoord) ? "x" : "y" + }, coord); + coordNode.appendChild(node); + coord = ""; + isXCoord = !isXCoord; + } + + if (fillRule) { + // If the last text added was a fill-rule, do not increment i. + fillRule = false; + } else { + container.appendChild(coordNode); + i++; + } + appendText(container, coords.substring(token.startOffset, token.endOffset)); + coord = ""; + depth = 0; + isXCoord = true; + coordNode = this._createNode("span", { + class: "ruleview-shape-point", + "data-point": `${i}`, + }); + } else if (token.tokenType === "symbol" && token.text === "(") { + depth++; + coord += coords.substring(token.startOffset, token.endOffset); + } else if (token.tokenType === "symbol" && token.text === ")") { + depth--; + coord += coords.substring(token.startOffset, token.endOffset); + } else if (token.tokenType === "whitespace" && coord === "") { + // Whitespace at beginning of coord; add to container + appendText(container, coords.substring(token.startOffset, token.endOffset)); + } else if (token.tokenType === "whitespace" && depth === 0) { + // Whitespace signifying end of coord + let node = this._createNode("span", { + class: "ruleview-shape-point", + "data-point": `${i}`, + "data-pair": (isXCoord) ? "x" : "y" + }, coord); + coordNode.appendChild(node); + appendText(coordNode, coords.substring(token.startOffset, token.endOffset)); + coord = ""; + isXCoord = !isXCoord; + } else if ((token.tokenType === "number" || token.tokenType === "dimension" || + token.tokenType === "percentage" || token.tokenType === "function")) { + if (isXCoord && coord && depth === 0) { + // Whitespace is not necessary between x/y coords. + let node = this._createNode("span", { + class: "ruleview-shape-point", + "data-point": `${i}`, + "data-pair": "x" + }, coord); + coordNode.appendChild(node); + isXCoord = false; + coord = ""; + } + + coord += coords.substring(token.startOffset, token.endOffset); + if (token.tokenType === "function") { + depth++; + } + } else if (token.tokenType === "ident" && + (token.text === "nonzero" || token.text === "evenodd")) { + // A fill-rule (nonzero or evenodd). + appendText(container, coords.substring(token.startOffset, token.endOffset)); + fillRule = true; + } else { + coord += coords.substring(token.startOffset, token.endOffset); + } + token = tokenStream.nextToken(); + } + + // Add coords if any are left over + if (coord) { + let node = this._createNode("span", { + class: "ruleview-shape-point", + "data-point": `${i}`, + "data-pair": (isXCoord) ? "x" : "y" + }, coord); + coordNode.appendChild(node); + container.appendChild(coordNode); + } + return container; + }, + + /** + * Parse the given circle coordinates and populate the given container appropriately + * with a separate span for the center point. + * + * @param {String} coords + * The circle definition. + * @param {Node} container + * The node to which the definition is added. + * @returns {Node} The container to which the definition has been added. + */ + _addCirclePointNodes: function (coords, container) { + let tokenStream = getCSSLexer(coords); + let token = tokenStream.nextToken(); + let depth = 0; + let coord = ""; + let point = "radius"; + let centerNode = this._createNode("span", { + class: "ruleview-shape-point", + "data-point": "center" + }); + while (token) { + if (token.tokenType === "symbol" && token.text === "(") { + depth++; + coord += coords.substring(token.startOffset, token.endOffset); + } else if (token.tokenType === "symbol" && token.text === ")") { + depth--; + coord += coords.substring(token.startOffset, token.endOffset); + } else if (token.tokenType === "whitespace" && coord === "") { + // Whitespace at beginning of coord; add to container + appendText(container, coords.substring(token.startOffset, token.endOffset)); + } else if (token.tokenType === "whitespace" && point === "radius" && depth === 0) { + // Whitespace signifying end of radius + let node = this._createNode("span", { + class: "ruleview-shape-point", + "data-point": "radius" + }, coord); + container.appendChild(node); + appendText(container, coords.substring(token.startOffset, token.endOffset)); + point = "cx"; + coord = ""; + depth = 0; + } else if (token.tokenType === "whitespace" && depth === 0) { + // Whitespace signifying end of cx/cy + let node = this._createNode("span", { + class: "ruleview-shape-point", + "data-point": "center", + "data-pair": (point === "cx") ? "x" : "y" + }, coord); + centerNode.appendChild(node); + appendText(centerNode, coords.substring(token.startOffset, token.endOffset)); + point = (point === "cx") ? "cy" : "cx"; + coord = ""; + depth = 0; + } else if (token.tokenType === "ident" && token.text === "at") { + // "at"; Add radius to container if not already done so + if (point === "radius" && coord) { + let node = this._createNode("span", { + class: "ruleview-shape-point", + "data-point": "radius" + }, coord); + container.appendChild(node); + } + appendText(container, coords.substring(token.startOffset, token.endOffset)); + point = "cx"; + coord = ""; + depth = 0; + } else if ((token.tokenType === "number" || token.tokenType === "dimension" || + token.tokenType === "percentage" || token.tokenType === "function")) { + if (point === "cx" && coord && depth === 0) { + // Center coords don't require whitespace between x/y. So if current point is + // cx, we have the cx coord, and depth is 0, then this token is actually cy. + // Add cx to centerNode and set point to cy. + let node = this._createNode("span", { + class: "ruleview-shape-point", + "data-point": "center", + "data-pair": "x" + }, coord); + centerNode.appendChild(node); + point = "cy"; + coord = ""; + } + + coord += coords.substring(token.startOffset, token.endOffset); + if (token.tokenType === "function") { + depth++; + } + } else { + coord += coords.substring(token.startOffset, token.endOffset); + } + token = tokenStream.nextToken(); + } + + // Add coords if any are left over. + if (coord) { + if (point === "radius") { + let node = this._createNode("span", { + class: "ruleview-shape-point", + "data-point": "radius" + }, coord); + container.appendChild(node); + } else { + let node = this._createNode("span", { + class: "ruleview-shape-point", + "data-point": "center", + "data-pair": (point === "cx") ? "x" : "y" + }, coord); + centerNode.appendChild(node); + } + } + + if (centerNode.textContent) { + container.appendChild(centerNode); + } + return container; + }, + + /** + * Parse the given ellipse coordinates and populate the given container appropriately + * with a separate span for each point + * + * @param {String} coords + * The ellipse definition. + * @param {Node} container + * The node to which the definition is added. + * @returns {Node} The container to which the definition has been added. + */ + _addEllipsePointNodes: function (coords, container) { + let tokenStream = getCSSLexer(coords); + let token = tokenStream.nextToken(); + let depth = 0; + let coord = ""; + let point = "rx"; + let centerNode = this._createNode("span", { + class: "ruleview-shape-point", + "data-point": "center" + }); + while (token) { + if (token.tokenType === "symbol" && token.text === "(") { + depth++; + coord += coords.substring(token.startOffset, token.endOffset); + } else if (token.tokenType === "symbol" && token.text === ")") { + depth--; + coord += coords.substring(token.startOffset, token.endOffset); + } else if (token.tokenType === "whitespace" && coord === "") { + // Whitespace at beginning of coord; add to container + appendText(container, coords.substring(token.startOffset, token.endOffset)); + } else if (token.tokenType === "whitespace" && depth === 0) { + if (point === "rx" || point === "ry") { + // Whitespace signifying end of rx/ry + let node = this._createNode("span", { + class: "ruleview-shape-point", + "data-point": point, + }, coord); + container.appendChild(node); + appendText(container, coords.substring(token.startOffset, token.endOffset)); + point = (point === "rx") ? "ry" : "cx"; + coord = ""; + depth = 0; + } else { + // Whitespace signifying end of cx/cy + let node = this._createNode("span", { + class: "ruleview-shape-point", + "data-point": "center", + "data-pair": (point === "cx") ? "x" : "y" + }, coord); + centerNode.appendChild(node); + appendText(centerNode, coords.substring(token.startOffset, token.endOffset)); + point = (point === "cx") ? "cy" : "cx"; + coord = ""; + depth = 0; + } + } else if (token.tokenType === "ident" && token.text === "at") { + // "at"; Add radius to container if not already done so + if (point === "ry" && coord) { + let node = this._createNode("span", { + class: "ruleview-shape-point", + "data-point": "ry" + }, coord); + container.appendChild(node); + } + appendText(container, coords.substring(token.startOffset, token.endOffset)); + point = "cx"; + coord = ""; + depth = 0; + } else if ((token.tokenType === "number" || token.tokenType === "dimension" || + token.tokenType === "percentage" || token.tokenType === "function")) { + if (point === "rx" && coord && depth === 0) { + // Radius coords don't require whitespace between x/y. + let node = this._createNode("span", { + class: "ruleview-shape-point", + "data-point": "rx", + }, coord); + container.appendChild(node); + point = "ry"; + coord = ""; + } + if (point === "cx" && coord && depth === 0) { + // Center coords don't require whitespace between x/y. + let node = this._createNode("span", { + class: "ruleview-shape-point", + "data-point": "center", + "data-pair": "x" + }, coord); + centerNode.appendChild(node); + point = "cy"; + coord = ""; + } + + coord += coords.substring(token.startOffset, token.endOffset); + if (token.tokenType === "function") { + depth++; + } + } else { + coord += coords.substring(token.startOffset, token.endOffset); + } + token = tokenStream.nextToken(); + } + + // Add coords if any are left over. + if (coord) { + if (point === "rx" || point === "ry") { + let node = this._createNode("span", { + class: "ruleview-shape-point", + "data-point": point + }, coord); + container.appendChild(node); + } else { + let node = this._createNode("span", { + class: "ruleview-shape-point", + "data-point": "center", + "data-pair": (point === "cx") ? "x" : "y" + }, coord); + centerNode.appendChild(node); + } + } + + if (centerNode.textContent) { + container.appendChild(centerNode); + } + return container; + }, + + /** + * Parse the given inset coordinates and populate the given container appropriately. + * + * @param {String} coords + * The inset definition. + * @param {Node} container + * The node to which the definition is added. + * @returns {Node} The container to which the definition has been added. + */ + _addInsetPointNodes: function (coords, container) { + const insetPoints = ["top", "right", "bottom", "left"]; + let tokenStream = getCSSLexer(coords); + let token = tokenStream.nextToken(); + let depth = 0; + let coord = ""; + let i = 0; + let round = false; + // nodes is an array containing all the coordinate spans. otherText is an array of + // arrays, each containing the text that should be inserted into container before + // the node with the same index. i.e. all elements of otherText[i] is inserted + // into container before nodes[i]. + let nodes = []; + let otherText = [[]]; + + while (token) { + if (round) { + // Everything that comes after "round" should just be plain text + otherText[i].push(coords.substring(token.startOffset, token.endOffset)); + } else if (token.tokenType === "symbol" && token.text === "(") { + depth++; + coord += coords.substring(token.startOffset, token.endOffset); + } else if (token.tokenType === "symbol" && token.text === ")") { + depth--; + coord += coords.substring(token.startOffset, token.endOffset); + } else if (token.tokenType === "whitespace" && coord === "") { + // Whitespace at beginning of coord; add to container + otherText[i].push(coords.substring(token.startOffset, token.endOffset)); + } else if (token.tokenType === "whitespace" && depth === 0) { + // Whitespace signifying end of coord; create node and push to nodes + let node = this._createNode("span", { + class: "ruleview-shape-point" + }, coord); + nodes.push(node); + i++; + coord = ""; + otherText[i] = [coords.substring(token.startOffset, token.endOffset)]; + depth = 0; + } else if ((token.tokenType === "number" || token.tokenType === "dimension" || + token.tokenType === "percentage" || token.tokenType === "function")) { + if (coord && depth === 0) { + // Inset coords don't require whitespace between each coord. + let node = this._createNode("span", { + class: "ruleview-shape-point", + }, coord); + nodes.push(node); + i++; + coord = ""; + otherText[i] = []; + } + + coord += coords.substring(token.startOffset, token.endOffset); + if (token.tokenType === "function") { + depth++; + } + } else if (token.tokenType === "ident" && token.text === "round") { + if (coord && depth === 0) { + // Whitespace is not necessary before "round"; create a new node for the coord + let node = this._createNode("span", { + class: "ruleview-shape-point", + }, coord); + nodes.push(node); + i++; + coord = ""; + otherText[i] = []; + } + round = true; + otherText[i].push(coords.substring(token.startOffset, token.endOffset)); + } else { + coord += coords.substring(token.startOffset, token.endOffset); + } + token = tokenStream.nextToken(); + } + + // Take care of any leftover text + if (coord) { + if (round) { + otherText[i].push(coord); + } else { + let node = this._createNode("span", { + class: "ruleview-shape-point", + }, coord); + nodes.push(node); + } + } + + // insetPoints contains the 4 different possible inset points in the order they are + // defined. By taking the modulo of the index in insetPoints with the number of nodes, + // we can get which node represents each point (e.g. if there is only 1 node, it + // represents all 4 points). The exception is "left" when there are 3 nodes. In that + // case, it is nodes[1] that represents the left point rather than nodes[0]. + for (let j = 0; j < 4; j++) { + let point = insetPoints[j]; + let nodeIndex = (point === "left" && nodes.length === 3) ? 1 : j % nodes.length; + nodes[nodeIndex].classList.add(point); + } + + nodes.forEach((node, j, array) => { + for (let text of otherText[j]) { + appendText(container, text); + } + container.appendChild(node); + }); + + // Add text that comes after the last node, if any exists + if (otherText[nodes.length]) { + for (let text of otherText[nodes.length]) { + appendText(container, text); + } + } + + return container; + }, + /** * Append a angle value to the output * diff --git a/devtools/client/shared/test/browser_outputparser.js b/devtools/client/shared/test/browser_outputparser.js index fe87d623c2add..d892ba312a207 100644 --- a/devtools/client/shared/test/browser_outputparser.js +++ b/devtools/client/shared/test/browser_outputparser.js @@ -5,6 +5,7 @@ const OutputParser = require("devtools/client/shared/output-parser"); const {initCssProperties, getCssProperties} = require("devtools/shared/fronts/css-properties"); +const CSS_SHAPES_ENABLED_PREF = "devtools.inspector.shapesHighlighter.enabled"; add_task(function* () { yield addTab("about:blank"); @@ -27,6 +28,7 @@ function* performTest() { testParseURL(doc, parser); testParseFilter(doc, parser); testParseAngle(doc, parser); + testParseShape(doc, parser); host.destroy(); } @@ -293,3 +295,121 @@ function testParseAngle(doc, parser) { swatchCount = frag.querySelectorAll(".test-angleswatch").length; is(swatchCount, 1, "angle swatch was created"); } + +function testParseShape(doc, parser) { + info("Test shape parsing"); + pushPref(CSS_SHAPES_ENABLED_PREF, true); + const tests = [ + { + desc: "Polygon shape", + definition: "polygon(evenodd, 0px 0px, 10%200px,30%30% , calc(250px - 10px) 0 ,\n " + + "12em var(--variable), 100% 100%) margin-box", + spanCount: 18 + }, + { + desc: "Invalid polygon shape", + definition: "polygon(0px 0px 100px 20px, 20% 20%)", + spanCount: 0 + }, + { + desc: "Circle shape with all arguments", + definition: "circle(25% at\n 30% 200px) border-box", + spanCount: 4 + }, + { + desc: "Circle shape with only one center", + definition: "circle(25em at 40%)", + spanCount: 3 + }, + { + desc: "Circle shape with no radius", + definition: "circle(at 30% 40%)", + spanCount: 3 + }, + { + desc: "Circle shape with no center", + definition: "circle(12em)", + spanCount: 1 + }, + { + desc: "Circle shape with no arguments", + definition: "circle()", + spanCount: 0 + }, + { + desc: "Circle shape with no space before at", + definition: "circle(25%at 30% 30%)", + spanCount: 4 + }, + { + desc: "Invalid circle shape", + definition: "circle(25%at30%30%)", + spanCount: 0 + }, + { + desc: "Ellipse shape with all arguments", + definition: "ellipse(200px 10em at 25% 120px) content-box", + spanCount: 5 + }, + { + desc: "Ellipse shape with only one center", + definition: "ellipse(200px 10% at 120px)", + spanCount: 4 + }, + { + desc: "Ellipse shape with no radius", + definition: "ellipse(at 25% 120px)", + spanCount: 3 + }, + { + desc: "Ellipse shape with no center", + definition: "ellipse(200px\n10em)", + spanCount: 2 + }, + { + desc: "Ellipse shape with no arguments", + definition: "ellipse()", + spanCount: 0 + }, + { + desc: "Invalid ellipse shape", + definition: "ellipse(200px100px at 30$ 20%)", + spanCount: 0 + }, + { + desc: "Inset shape with 4 arguments", + definition: "inset(200px 100px\n 30%15%)", + spanCount: 4 + }, + { + desc: "Inset shape with 3 arguments", + definition: "inset(200px 100px 15%)", + spanCount: 3 + }, + { + desc: "Inset shape with 2 arguments", + definition: "inset(200px 100px)", + spanCount: 2 + }, + { + desc: "Inset shape with 1 argument", + definition: "inset(200px)", + spanCount: 1 + }, + { + desc: "Inset shape with 0 arguments", + definition: "inset()", + spanCount: 0 + } + ]; + + for (let {desc, definition, spanCount} of tests) { + info(desc); + let frag = parser.parseCssProperty("clip-path", definition, { + shapeClass: "ruleview-shape" + }); + let spans = frag.querySelectorAll(".ruleview-shape-point"); + is(spans.length, spanCount, desc + " span count"); + is(frag.textContent, definition, desc + " text content"); + } +} diff --git a/devtools/client/themes/rules.css b/devtools/client/themes/rules.css index 75a880d658466..a6db24899092c 100644 --- a/devtools/client/themes/rules.css +++ b/devtools/client/themes/rules.css @@ -477,6 +477,10 @@ background-size: 1em; } +.ruleview-shape-point.active { + background-color: var(--rule-highlight-background-color); +} + .ruleview-colorswatch::before { content: ''; background-color: #eee; diff --git a/devtools/server/actors/highlighters.css b/devtools/server/actors/highlighters.css index 3456b51d3bb80..ff9fcc9203451 100644 --- a/devtools/server/actors/highlighters.css +++ b/devtools/server/actors/highlighters.css @@ -613,3 +613,7 @@ :-moz-native-anonymous .shapes-markers { fill: var(--highlighter-marker-color); } + +:-moz-native-anonymous .shapes-marker-hover { + fill: var(--highlighter-guide-color); +} diff --git a/devtools/server/actors/highlighters.js b/devtools/server/actors/highlighters.js index 7f5f08469590f..f427fb6fc7826 100644 --- a/devtools/server/actors/highlighters.js +++ b/devtools/server/actors/highlighters.js @@ -459,6 +459,9 @@ exports.CustomHighlighterActor = protocol.ActorClassWithSpec(customHighlighterSp this._highlighterEnv = new HighlighterEnvironment(); this._highlighterEnv.initFromTabActor(inspector.tabActor); this._highlighter = new constructor(this._highlighterEnv); + if (this._highlighter.on) { + this._highlighter.on("highlighter-event", this._onHighlighterEvent.bind(this)); + } } else { throw new Error("Custom " + typeName + "highlighter cannot be created in a XUL window"); @@ -511,12 +514,22 @@ exports.CustomHighlighterActor = protocol.ActorClassWithSpec(customHighlighterSp } }, + /** + * Upon receiving an event from the highlighter, forward it to the client. + */ + _onHighlighterEvent: function (type, data) { + events.emit(this, "highlighter-event", data); + }, + /** * Kill this actor. This method is called automatically just before the actor * is destroyed. */ finalize: function () { if (this._highlighter) { + if (this._highlighter.off) { + this._highlighter.off("highlighter-event", this._onHighlighterEvent.bind(this)); + } this._highlighter.destroy(); this._highlighter = null; } diff --git a/devtools/server/actors/highlighters/shapes.js b/devtools/server/actors/highlighters/shapes.js index 64e231b964c9f..387468231ab18 100644 --- a/devtools/server/actors/highlighters/shapes.js +++ b/devtools/server/actors/highlighters/shapes.js @@ -15,6 +15,7 @@ const { projection, clickedOnPoint } = require("devtools/server/actors/utils/shapes-geometry-utils"); +const EventEmitter = require("devtools/shared/event-emitter"); const BASE_MARKER_SIZE = 10; // the width of the area around highlighter lines that can be clicked, in px @@ -30,12 +31,15 @@ const _dragging = Symbol("shapes/dragging"); class ShapesHighlighter extends AutoRefreshHighlighter { constructor(highlighterEnv) { super(highlighterEnv); + EventEmitter.decorate(this); this.ID_CLASS_PREFIX = "shapes-"; this.referenceBox = "border"; this.useStrokeBox = false; this.geometryBox = ""; + this.hoveredPoint = null; + this.fillRule = ""; this.markup = new CanvasFrameAnonymousContentHelper(this.highlighterEnv, this._buildMarkup.bind(this)); @@ -122,6 +126,17 @@ class ShapesHighlighter extends AutoRefreshHighlighter { prefix: this.ID_CLASS_PREFIX }); + createSVGNode(this.win, { + nodeType: "path", + parent: mainSvg, + attributes: { + "id": "marker-hover", + "class": "marker-hover", + "hidden": true + }, + prefix: this.ID_CLASS_PREFIX + }); + return container; } @@ -201,6 +216,7 @@ class ShapesHighlighter extends AutoRefreshHighlighter { break; case "mousemove": if (!this[_dragging]) { + this._handleMouseMoveNotDragging(pageX, pageY); return; } event.stopPropagation(); @@ -220,7 +236,7 @@ class ShapesHighlighter extends AutoRefreshHighlighter { case "dblclick": if (this.shapeType === "polygon") { let { percentX, percentY } = this.convertPageCoordsToPercent(pageX, pageY); - let index = this.getPolygonClickedPoint(percentX, percentY); + let index = this.getPolygonPointAt(percentX, percentY); if (index === -1) { this.getPolygonClickedLine(percentX, percentY); return; @@ -240,7 +256,7 @@ class ShapesHighlighter extends AutoRefreshHighlighter { _handlePolygonClick(pageX, pageY) { let { width, height } = this.zoomAdjustedDimensions; let { percentX, percentY } = this.convertPageCoordsToPercent(pageX, pageY); - let point = this.getPolygonClickedPoint(percentX, percentY); + let point = this.getPolygonPointAt(percentX, percentY); if (point === -1) { return; } @@ -273,7 +289,8 @@ class ShapesHighlighter extends AutoRefreshHighlighter { let newX = `${valueX + deltaX}${unitX}`; let newY = `${valueY + deltaY}${unitY}`; - let polygonDef = this.coordUnits.map((coords, i) => { + let polygonDef = (this.fillRule) ? `${this.fillRule}, ` : ""; + polygonDef += this.coordUnits.map((coords, i) => { return (i === point) ? `${newX} ${newY}` : `${coords[0]} ${coords[1]}`; }).join(", "); polygonDef = (this.geometryBox) ? `polygon(${polygonDef}) ${this.geometryBox}` : @@ -289,13 +306,16 @@ class ShapesHighlighter extends AutoRefreshHighlighter { * @param {Number} y the y coordinate of the new point */ _addPolygonPoint(after, x, y) { - let polygonDef = this.coordUnits.map((coords, i) => { + let polygonDef = (this.fillRule) ? `${this.fillRule}, ` : ""; + polygonDef += this.coordUnits.map((coords, i) => { return (i === after) ? `${coords[0]} ${coords[1]}, ${x}% ${y}%` : `${coords[0]} ${coords[1]}`; }).join(", "); polygonDef = (this.geometryBox) ? `polygon(${polygonDef}) ${this.geometryBox}` : `polygon(${polygonDef})`; + this.hoveredPoint = after + 1; + this._emitHoverEvent(this.hoveredPoint); this.currentNode.style.setProperty(this.property, polygonDef, "important"); } @@ -306,12 +326,15 @@ class ShapesHighlighter extends AutoRefreshHighlighter { _deletePolygonPoint(point) { let coordinates = this.coordUnits.slice(); coordinates.splice(point, 1); - let polygonDef = coordinates.map((coords, i) => { + let polygonDef = (this.fillRule) ? `${this.fillRule}, ` : ""; + polygonDef += coordinates.map((coords, i) => { return `${coords[0]} ${coords[1]}`; }).join(", "); polygonDef = (this.geometryBox) ? `polygon(${polygonDef}) ${this.geometryBox}` : `polygon(${polygonDef})`; + this.hoveredPoint = null; + this._emitHoverEvent(this.hoveredPoint); this.currentNode.style.setProperty(this.property, polygonDef, "important"); } /** @@ -322,7 +345,7 @@ class ShapesHighlighter extends AutoRefreshHighlighter { _handleCircleClick(pageX, pageY) { let { width, height } = this.zoomAdjustedDimensions; let { percentX, percentY } = this.convertPageCoordsToPercent(pageX, pageY); - let point = this.getCircleClickedPoint(percentX, percentY); + let point = this.getCirclePointAt(percentX, percentY); if (!point) { return; } @@ -403,7 +426,7 @@ class ShapesHighlighter extends AutoRefreshHighlighter { _handleEllipseClick(pageX, pageY) { let { width, height } = this.zoomAdjustedDimensions; let { percentX, percentY } = this.convertPageCoordsToPercent(pageX, pageY); - let point = this.getEllipseClickedPoint(percentX, percentY); + let point = this.getEllipsePointAt(percentX, percentY); if (!point) { return; } @@ -502,7 +525,7 @@ class ShapesHighlighter extends AutoRefreshHighlighter { _handleInsetClick(pageX, pageY) { let { width, height } = this.zoomAdjustedDimensions; let { percentX, percentY } = this.convertPageCoordsToPercent(pageX, pageY); - let point = this.getInsetClickedPoint(percentX, percentY); + let point = this.getInsetPointAt(percentX, percentY); if (!point) { return; } @@ -555,6 +578,124 @@ class ShapesHighlighter extends AutoRefreshHighlighter { this.currentNode.style.setProperty(this.property, insetDef, "important"); } + _handleMouseMoveNotDragging(pageX, pageY) { + let { percentX, percentY } = this.convertPageCoordsToPercent(pageX, pageY); + if (this.shapeType === "polygon") { + let point = this.getPolygonPointAt(percentX, percentY); + let oldHoveredPoint = this.hoveredPoint; + this.hoveredPoint = (point !== -1) ? point : null; + if (this.hoveredPoint !== oldHoveredPoint) { + this._emitHoverEvent(this.hoveredPoint); + } + this._handleMarkerHover(point); + } else if (this.shapeType === "circle") { + let point = this.getCirclePointAt(percentX, percentY); + let oldHoveredPoint = this.hoveredPoint; + this.hoveredPoint = point ? point : null; + if (this.hoveredPoint !== oldHoveredPoint) { + this._emitHoverEvent(this.hoveredPoint); + } + this._handleMarkerHover(point); + } else if (this.shapeType === "ellipse") { + let point = this.getEllipsePointAt(percentX, percentY); + let oldHoveredPoint = this.hoveredPoint; + this.hoveredPoint = point ? point : null; + if (this.hoveredPoint !== oldHoveredPoint) { + this._emitHoverEvent(this.hoveredPoint); + } + this._handleMarkerHover(point); + } else if (this.shapeType === "inset") { + let point = this.getInsetPointAt(percentX, percentY); + let oldHoveredPoint = this.hoveredPoint; + this.hoveredPoint = point ? point : null; + if (this.hoveredPoint !== oldHoveredPoint) { + this._emitHoverEvent(this.hoveredPoint); + } + this._handleMarkerHover(point); + } + } + + _handleMarkerHover(point) { + // Hide hover marker for now, will be shown if point is a valid hover target + this.getElement("marker-hover").setAttribute("hidden", true); + if (point === null || point === undefined) { + return; + } + + if (this.shapeType === "polygon") { + if (point === -1) { + return; + } + this._drawHoverMarker([this.coordinates[point]]); + } else if (this.shapeType === "circle") { + let { cx, cy, rx } = this.coordinates; + if (point === "radius") { + this._drawHoverMarker([[cx + rx, cy]]); + } else if (point === "center") { + this._drawHoverMarker([[cx, cy]]); + } + } else if (this.shapeType === "ellipse") { + if (point === "center") { + let { cx, cy } = this.coordinates; + this._drawHoverMarker([[cx, cy]]); + } else if (point === "rx") { + let { cx, cy, rx } = this.coordinates; + this._drawHoverMarker([[cx + rx, cy]]); + } else if (point === "ry") { + let { cx, cy, ry } = this.coordinates; + this._drawHoverMarker([[cx, cy + ry]]); + } + } else if (this.shapeType === "inset") { + if (!point) { + return; + } + + let { top, right, bottom, left } = this.coordinates; + let centerX = (left + (100 - right)) / 2; + let centerY = (top + (100 - bottom)) / 2; + let points = point.split(","); + let coords = points.map(side => { + if (side === "top") { + return [centerX, top]; + } else if (side === "right") { + return [100 - right, centerY]; + } else if (side === "bottom") { + return [centerX, 100 - bottom]; + } else if (side === "left") { + return [left, centerY]; + } + return null; + }); + + this._drawHoverMarker(coords); + } + } + + _drawHoverMarker(points) { + let { width, height } = this.zoomAdjustedDimensions; + let zoom = getCurrentZoom(this.win); + let path = points.map(([x, y]) => { + return getCirclePath(x, y, width, height, zoom); + }).join(" "); + + let markerHover = this.getElement("marker-hover"); + markerHover.setAttribute("d", path); + markerHover.removeAttribute("hidden"); + } + + _emitHoverEvent(point) { + if (point === null || point === undefined) { + this.emit("highlighter-event", { + type: "shape-hover-off" + }); + } else { + this.emit("highlighter-event", { + type: "shape-hover-on", + point: point.toString() + }); + } + } + /** * Convert the given coordinates on the page to percentages relative to the current * element. @@ -592,13 +733,13 @@ class ShapesHighlighter extends AutoRefreshHighlighter { } /** - * Get the id of the point clicked on the polygon highlighter. + * Get the id of the point on the polygon highlighter at the given coordinate. * @param {Number} pageX the x coordinate on the page, in % relative to the element * @param {Number} pageY the y coordinate on the page, in % relative to the element * @returns {Number} the index of the point that was clicked on in this.coordinates, * or -1 if none of the points were clicked on. */ - getPolygonClickedPoint(pageX, pageY) { + getPolygonPointAt(pageX, pageY) { let { coordinates } = this; let { width, height } = this.zoomAdjustedDimensions; let zoom = getCurrentZoom(this.win); @@ -647,13 +788,13 @@ class ShapesHighlighter extends AutoRefreshHighlighter { } /** - * Check if the center point or radius of the circle highlighter was clicked + * Check if the center point or radius of the circle highlighter is at given coords * @param {Number} pageX the x coordinate on the page, in % relative to the element * @param {Number} pageY the y coordinate on the page, in % relative to the element * @returns {String} "center" if the center point was clicked, "radius" if the radius * was clicked, "" if neither was clicked. */ - getCircleClickedPoint(pageX, pageY) { + getCirclePointAt(pageX, pageY) { let { cx, cy, rx, ry } = this.coordinates; let { width, height } = this.zoomAdjustedDimensions; let zoom = getCurrentZoom(this.win); @@ -675,14 +816,14 @@ class ShapesHighlighter extends AutoRefreshHighlighter { } /** - * Check if the center point or rx/ry points of the ellipse highlighter was clicked + * Check if the center or rx/ry points of the ellipse highlighter is at given point * @param {Number} pageX the x coordinate on the page, in % relative to the element * @param {Number} pageY the y coordinate on the page, in % relative to the element * @returns {String} "center" if the center point was clicked, "rx" if the x-radius * point was clicked, "ry" if the y-radius point was clicked, * "" if none was clicked. */ - getEllipseClickedPoint(pageX, pageY) { + getEllipsePointAt(pageX, pageY) { let { cx, cy, rx, ry } = this.coordinates; let { width, height } = this.zoomAdjustedDimensions; let zoom = getCurrentZoom(this.win); @@ -705,13 +846,13 @@ class ShapesHighlighter extends AutoRefreshHighlighter { } /** - * Check if the edges of the inset highlighter was clicked + * Check if the edges of the inset highlighter is at given coords * @param {Number} pageX the x coordinate on the page, in % relative to the element * @param {Number} pageY the y coordinate on the page, in % relative to the element * @returns {String} "top", "left", "right", or "bottom" if any of those edges were * clicked. "" if none were clicked. */ - getInsetClickedPoint(pageX, pageY) { + getInsetPointAt(pageX, pageY) { let { top, left, right, bottom } = this.coordinates; let zoom = getCurrentZoom(this.win); let { width, height } = this.zoomAdjustedDimensions; @@ -813,7 +954,11 @@ class ShapesHighlighter extends AutoRefreshHighlighter { */ polygonPoints(definition) { this.coordUnits = this.polygonRawPoints(); - return definition.split(", ").map(coords => { + let splitDef = definition.split(", "); + if (splitDef[0] === "evenodd" || splitDef[0] === "nonzero") { + splitDef.shift(); + } + return splitDef.map(coords => { return splitCoords(coords).map(this.convertCoordsToPercent.bind(this)); }); } @@ -829,7 +974,14 @@ class ShapesHighlighter extends AutoRefreshHighlighter { } this.rawDefinition = definition; definition = definition.substring(8, definition.lastIndexOf(")")); - return definition.split(", ").map(coords => { + let splitDef = definition.split(", "); + if (splitDef[0].includes("evenodd") || splitDef[0].includes("nonzero")) { + this.fillRule = splitDef[0].trim(); + splitDef.shift(); + } else { + this.fillRule = ""; + } + return splitDef.map(coords => { return splitCoords(coords).map(coord => { // Undo the insertion of   that was done in splitCoords. return coord.replace(/\u00a0/g, " "); @@ -1076,6 +1228,7 @@ class ShapesHighlighter extends AutoRefreshHighlighter { * Show the highlighter on a given node */ _show() { + this.hoveredPoint = this.options.hoverPoint; return this._update(); } @@ -1152,6 +1305,8 @@ class ShapesHighlighter extends AutoRefreshHighlighter { this._updateInsetShape(width, height, zoom); } + this._handleMarkerHover(this.hoveredPoint); + let { width: winWidth, height: winHeight } = this._winDimensions; root.removeAttribute("hidden"); root.setAttribute("style", diff --git a/devtools/shared/specs/highlighters.js b/devtools/shared/specs/highlighters.js index 8335b22d93244..3b6ae371b6e63 100644 --- a/devtools/shared/specs/highlighters.js +++ b/devtools/shared/specs/highlighters.js @@ -38,6 +38,13 @@ exports.highlighterSpec = highlighterSpec; const customHighlighterSpec = generateActorSpec({ typeName: "customhighlighter", + events: { + "highlighter-event": { + type: "highlighter-event", + data: Arg(0, "json") + } + }, + methods: { release: { release: true From 9370303ce30aa03023376bf9c67bc92e83605ccf Mon Sep 17 00:00:00 2001 From: Michal Novotny Date: Mon, 24 Jul 2017 20:35:36 +0200 Subject: [PATCH 13/56] Bug 1377223 - RCWN - Should we revalidate when racing and the cache wins, r=valentin, data-r=bsmedberg This patch adds telemetry probes to find out how often the cache wins the race but the entry cannot be used because it needs to be revalidated and we cannot send a conditional request. --- netwerk/protocol/http/nsHttpChannel.cpp | 36 +++++++++++++++++--- toolkit/components/telemetry/Histograms.json | 9 +++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/netwerk/protocol/http/nsHttpChannel.cpp b/netwerk/protocol/http/nsHttpChannel.cpp index b802c08859822..d36e8335ca589 100644 --- a/netwerk/protocol/http/nsHttpChannel.cpp +++ b/netwerk/protocol/http/nsHttpChannel.cpp @@ -530,11 +530,18 @@ nsHttpChannel::Connect() // otherwise, let's just proceed without using the cache. } + if (mRaceCacheWithNetwork && mCacheEntry && !mCachedContentIsValid && + (mDidReval || mCachedContentIsPartial)) { + // We won't send the conditional request because the unconditional + // request was already sent (see bug 1377223). + AccumulateCategorical(Telemetry::LABELS_NETWORK_RACE_CACHE_VALIDATION::NotSent); + } + // When racing, if OnCacheEntryAvailable is called before AsyncOpenURI // returns, then we may not have started reading from the cache. // If the content is valid, we should attempt to do so, as technically the // cache has won the race. - if (sRCWNEnabled && mCachedContentIsValid && mNetworkTriggered) { + if (mRaceCacheWithNetwork && mCachedContentIsValid) { Unused << ReadFromCache(true); } @@ -2402,6 +2409,7 @@ nsHttpChannel::ContinueProcessResponse2(nsresult rv) uint32_t httpStatus = mResponseHead->Status(); bool successfulReval = false; + bool partialContentUsed = false; // handle different server response categories. Note that we handle // caching or not caching of error pages in @@ -2425,9 +2433,12 @@ nsHttpChannel::ContinueProcessResponse2(nsresult rv) MaybeInvalidateCacheEntryForSubsequentGet(); break; case 206: - if (mCachedContentIsPartial) // an internal byte range request... + if (mCachedContentIsPartial) { // an internal byte range request... rv = ProcessPartialContent(); - else { + if (NS_SUCCEEDED(rv)) { + partialContentUsed = true; + } + } else { mCacheInputStream.CloseAndRelease(); rv = ProcessNormal(); } @@ -2545,6 +2556,16 @@ nsHttpChannel::ContinueProcessResponse2(nsresult rv) break; } + if (mRaceDelay && !mRaceCacheWithNetwork && + (mCachedContentIsPartial || mDidReval)) { + if (successfulReval || partialContentUsed) { + AccumulateCategorical(Telemetry::LABELS_NETWORK_RACE_CACHE_VALIDATION::CachedContentUsed); + } else { + AccumulateCategorical(Telemetry::LABELS_NETWORK_RACE_CACHE_VALIDATION::CachedContentNotUsed); + } + } + + if (gHttpHandler->IsTelemetryEnabled()) { CacheDisposition cacheDisposition; if (!mDidReval) { @@ -4530,7 +4551,14 @@ nsHttpChannel::OnCacheEntryAvailableInternal(nsICacheEntry *entry, return NS_OK; } - if (mCachedContentIsValid && mNetworkTriggered) { + if (mRaceCacheWithNetwork && mCacheEntry && !mCachedContentIsValid && + (mDidReval || mCachedContentIsPartial)) { + // We won't send the conditional request because the unconditional + // request was already sent (see bug 1377223). + AccumulateCategorical(Telemetry::LABELS_NETWORK_RACE_CACHE_VALIDATION::NotSent); + } + + if (mRaceCacheWithNetwork && mCachedContentIsValid) { Unused << ReadFromCache(true); } diff --git a/toolkit/components/telemetry/Histograms.json b/toolkit/components/telemetry/Histograms.json index fddcc3ebc6ec3..8c73a53411b20 100644 --- a/toolkit/components/telemetry/Histograms.json +++ b/toolkit/components/telemetry/Histograms.json @@ -2491,6 +2491,15 @@ "alert_emails": ["necko@mozilla.com", "bsmedberg@mozilla.com"], "bug_numbers": [1354405] }, + "NETWORK_RACE_CACHE_VALIDATION": { + "record_in_processes": ["main"], + "expires_in_version": "62", + "alert_emails": ["necko@mozilla.com"], + "bug_numbers": [1377223], + "kind": "categorical", + "labels": ["NotSent", "CachedContentUsed", "CachedContentNotUsed"], + "description": "Stats for validation requests when cache won the race." + }, "HTTP_AUTH_DIALOG_STATS_2": { "record_in_processes": ["main", "content"], "expires_in_version": "61", From 459f9f007de07bb1022acade3a42a965936c2b78 Mon Sep 17 00:00:00 2001 From: Bob Owen Date: Mon, 24 Jul 2017 20:01:06 +0100 Subject: [PATCH 14/56] Bug 1383611: Pre-load psapi.dll for widevine CDM as it needs it for GetMappedFileNameW. r=cpearce --- dom/media/gmp/GMPChild.cpp | 1 + dom/media/gmp/GMPParent.cpp | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/dom/media/gmp/GMPChild.cpp b/dom/media/gmp/GMPChild.cpp index 03e9a28e10bdc..5d6b6a84ee3ce 100644 --- a/dom/media/gmp/GMPChild.cpp +++ b/dom/media/gmp/GMPChild.cpp @@ -278,6 +278,7 @@ GMPChild::RecvPreloadLibs(const nsCString& aLibs) "evr.dll", // MFGetStrideForBitmapInfoHeader "mfplat.dll", // MFCreateSample, MFCreateAlignedMemoryBuffer, MFCreateMediaType "msmpeg2vdec.dll", // H.264 decoder + "psapi.dll", // For GetMappedFileNameW, see bug 1383611 }; nsTArray libs; diff --git a/dom/media/gmp/GMPParent.cpp b/dom/media/gmp/GMPParent.cpp index fdd070ce46ca8..d2be52a79523f 100644 --- a/dom/media/gmp/GMPParent.cpp +++ b/dom/media/gmp/GMPParent.cpp @@ -791,7 +791,9 @@ GMPParent::ParseChromiumManifest(const nsAString& aJSON) } else if (mDisplayName.EqualsASCII("WidevineCdm")) { kEMEKeySystem = kEMEKeySystemWidevine; #if XP_WIN - mLibs = NS_LITERAL_CSTRING("dxva2.dll"); + // psapi.dll added for GetMappedFileNameW, which could possibly be avoided + // in future versions, see bug 1383611 for details. + mLibs = NS_LITERAL_CSTRING("dxva2.dll, psapi.dll"); #endif } else { return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); From 2a5a5dd13b984198dbbce74adff75d80ae375f9c Mon Sep 17 00:00:00 2001 From: Joel Maher Date: Mon, 24 Jul 2017 15:06:11 -0400 Subject: [PATCH 15/56] Bug 1382830 - annotate more of web-platform-tests moz.build files for BUGZILLA_COMPONENTS. r=overholt MozReview-Commit-ID: 9sMNQntYd35 --- testing/web-platform/moz.build | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/testing/web-platform/moz.build b/testing/web-platform/moz.build index 25d595e433116..ba88ad04344b9 100644 --- a/testing/web-platform/moz.build +++ b/testing/web-platform/moz.build @@ -35,6 +35,32 @@ with Files("mach*"): with Files("*.py"): BUG_COMPONENT = ("Testing", "web-platform-tests") +# usb and payments ? +with Files("tests/feature-policy/**"): + BUG_COMPONENT = ("Core", "DOM") + +with Files("tests/fetch/**"): + BUG_COMPONENT = ("Core", "DOM") + +with Files("tests/fonts/math/**"): + BUG_COMPONENT = ("Core", "MathML") + +with Files("tests/fullscreen/**"): + BUG_COMPONENT = ("Core", "DOM") + +with Files("tests/gamepad/**"): + BUG_COMPONENT = ("Core", "DOM: Device Interfaces") + +# Bug 1359076 - Deleting this feature due to security +with Files("tests/generic-sensor/**"): + BUG_COMPONENT = ("Core", "DOM: Device Interfaces") + +with Files("tests/geolocation-API/**"): + BUG_COMPONENT = ("Core", "Geolocation") + +with Files("tests/gyroscope/**"): + BUG_COMPONENT = ("Core", "DOM: Device Interfaces") + with Files("tests/media/**"): BUG_COMPONENT = ("Core", "Audio/Video: Playback") From 3ea39fbf96ab5d33bbea0374d06227619f032395 Mon Sep 17 00:00:00 2001 From: Sean Stangl Date: Fri, 21 Jul 2017 14:52:00 -0400 Subject: [PATCH 16/56] Bug 1308193 - Reorganize code that looks like it could return a stack address. r=nbp --- js/src/jit/IonBuilder.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/js/src/jit/IonBuilder.cpp b/js/src/jit/IonBuilder.cpp index 3f2f95027d362..91341c39881d0 100644 --- a/js/src/jit/IonBuilder.cpp +++ b/js/src/jit/IonBuilder.cpp @@ -8539,8 +8539,7 @@ IonBuilder::computeHeapType(const TemporaryTypeSet* objTypes, const jsid id) if (objTypes->unknownObject() || objTypes->getObjectCount() == 0) return nullptr; - TemporaryTypeSet empty; - TemporaryTypeSet* acc = ∅ + TemporaryTypeSet* acc = nullptr; LifoAlloc* lifoAlloc = alloc().lifoAlloc(); Vector properties; @@ -8560,7 +8559,14 @@ IonBuilder::computeHeapType(const TemporaryTypeSet* objTypes, const jsid id) return nullptr; properties.infallibleAppend(property); - acc = TypeSet::unionSets(acc, currentSet, lifoAlloc); + + if (acc) { + acc = TypeSet::unionSets(acc, currentSet, lifoAlloc); + } else { + TemporaryTypeSet empty; + acc = TypeSet::unionSets(&empty, currentSet, lifoAlloc); + } + if (!acc) return nullptr; } From 8bc55108f202afab5a4d84ab1f3b6289df9b0301 Mon Sep 17 00:00:00 2001 From: Gian-Carlo Pascutto Date: Mon, 24 Jul 2017 16:32:22 +0200 Subject: [PATCH 17/56] Bug 1308400 - Support file process, whitelist path prefs. r=jld MozReview-Commit-ID: 3eX06AioPZL --HG-- extra : rebase_source : 56bcfaad3360fe92ce605a0413bb3a9cacb4446d --- browser/app/profile/firefox.js | 6 +- dom/ipc/ContentChild.cpp | 5 +- dom/ipc/ContentParent.cpp | 3 +- security/sandbox/linux/Sandbox.cpp | 6 +- security/sandbox/linux/Sandbox.h | 2 + .../broker/SandboxBrokerPolicyFactory.cpp | 163 +++++++++++++++++- .../linux/broker/SandboxBrokerPolicyFactory.h | 5 +- .../linux/reporter/SandboxReporterCommon.h | 1 + .../test/browser_content_sandbox_fs.js | 37 ++-- 9 files changed, 194 insertions(+), 34 deletions(-) diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index ed0ea1aa89bf7..c9a0490944601 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1105,7 +1105,8 @@ pref("security.sandbox.content.level", 1); // its Windows/Mac counterpart, but on Linux it's an integer which means: // 0 -> "no sandbox" // 1 -> "content sandbox using seccomp-bpf when available" -// 2 -> "seccomp-bpf + file broker" +// 2 -> "seccomp-bpf + write file broker" +// 3 -> "seccomp-bpf + read/write file brokering" // Content sandboxing on Linux is currently in the stage of // 'just getting it enabled', which includes a very permissive whitelist. We // enable seccomp-bpf on nightly to see if everything is running, or if we need @@ -1117,8 +1118,9 @@ pref("security.sandbox.content.level", 1); // // This setting may not be required anymore once we decide to permanently // enable the content sandbox. -pref("security.sandbox.content.level", 2); +pref("security.sandbox.content.level", 3); pref("security.sandbox.content.write_path_whitelist", ""); +pref("security.sandbox.content.read_path_whitelist", ""); pref("security.sandbox.content.syscall_whitelist", ""); #endif diff --git a/dom/ipc/ContentChild.cpp b/dom/ipc/ContentChild.cpp index 94203d9450e5d..670036a82ce0e 100644 --- a/dom/ipc/ContentChild.cpp +++ b/dom/ipc/ContentChild.cpp @@ -1645,7 +1645,10 @@ ContentChild::RecvSetProcessSandbox(const MaybeFileDesc& aBroker) } } } - sandboxEnabled = SetContentProcessSandbox(brokerFd, syscallWhitelist); + ContentChild* cc = ContentChild::GetSingleton(); + bool isFileProcess = cc->GetRemoteType().EqualsLiteral(FILE_REMOTE_TYPE); + sandboxEnabled = SetContentProcessSandbox(brokerFd, isFileProcess, + syscallWhitelist); } #elif defined(XP_WIN) mozilla::SandboxTarget::Instance()->StartSandbox(); diff --git a/dom/ipc/ContentParent.cpp b/dom/ipc/ContentParent.cpp index 5fb517c96b03e..ad59bb6d5934f 100644 --- a/dom/ipc/ContentParent.cpp +++ b/dom/ipc/ContentParent.cpp @@ -2435,8 +2435,9 @@ ContentParent::InitInternal(ProcessPriority aInitialPriority, #ifdef XP_LINUX if (shouldSandbox) { MOZ_ASSERT(!mSandboxBroker); + bool isFileProcess = mRemoteType.EqualsLiteral(FILE_REMOTE_TYPE); UniquePtr policy = - sSandboxBrokerPolicyFactory->GetContentPolicy(Pid()); + sSandboxBrokerPolicyFactory->GetContentPolicy(Pid(), isFileProcess); if (policy) { brokerFd = FileDescriptor(); mSandboxBroker = SandboxBroker::Create(Move(policy), Pid(), brokerFd); diff --git a/security/sandbox/linux/Sandbox.cpp b/security/sandbox/linux/Sandbox.cpp index 2b473bf408d37..6aa0e1681cbb4 100644 --- a/security/sandbox/linux/Sandbox.cpp +++ b/security/sandbox/linux/Sandbox.cpp @@ -685,7 +685,8 @@ SandboxEarlyInit(GeckoProcessType aType) * Will normally make the process exit on failure. */ bool -SetContentProcessSandbox(int aBrokerFd, std::vector& aSyscallWhitelist) +SetContentProcessSandbox(int aBrokerFd, bool aFileProcess, + std::vector& aSyscallWhitelist) { if (!SandboxInfo::Get().Test(SandboxInfo::kEnabledForContent)) { if (aBrokerFd >= 0) { @@ -694,7 +695,8 @@ SetContentProcessSandbox(int aBrokerFd, std::vector& aSyscallWhitelist) return false; } - gSandboxReporterClient.emplace(SandboxReport::ProcType::CONTENT); + gSandboxReporterClient.emplace(aFileProcess ? SandboxReport::ProcType::FILE + : SandboxReport::ProcType::CONTENT); // This needs to live until the process exits. static Maybe sBroker; diff --git a/security/sandbox/linux/Sandbox.h b/security/sandbox/linux/Sandbox.h index a65cc2dd944f2..8e2763bf50d70 100644 --- a/security/sandbox/linux/Sandbox.h +++ b/security/sandbox/linux/Sandbox.h @@ -25,7 +25,9 @@ MOZ_EXPORT void SandboxEarlyInit(GeckoProcessType aType); // (No-op if MOZ_DISABLE_CONTENT_SANDBOX is set.) // aBrokerFd is the filesystem broker client file descriptor, // or -1 to allow direct filesystem access. +// isFileProcess determines whether we allow system wide file reads. MOZ_EXPORT bool SetContentProcessSandbox(int aBrokerFd, + bool aFileProcess, std::vector& aSyscallWhitelist); #endif diff --git a/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp b/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp index 50f8e8ec7eb60..fe7e4752e259b 100644 --- a/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp +++ b/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp @@ -11,10 +11,13 @@ #include "mozilla/ClearOnShutdown.h" #include "mozilla/Preferences.h" #include "mozilla/SandboxSettings.h" +#include "mozilla/dom/ContentChild.h" #include "nsPrintfCString.h" #include "nsString.h" #include "nsThreadUtils.h" #include "nsXULAppAPI.h" +#include "nsDirectoryServiceDefs.h" +#include "nsAppDirectoryServiceDefs.h" #include "SpecialSystemDirectory.h" #ifdef ANDROID @@ -42,14 +45,16 @@ SandboxBrokerPolicyFactory::SandboxBrokerPolicyFactory() // are cached over the lifetime of the factory. #if defined(MOZ_CONTENT_SANDBOX) SandboxBroker::Policy* policy = new SandboxBroker::Policy; - policy->AddDir(rdonly, "/"); policy->AddDir(rdwrcr, "/dev/shm"); + // Write permssions + // // Add write permissions on the temporary directory. This can come // from various environment variables (TMPDIR,TMP,TEMP,...) so // make sure to use the full logic. nsCOMPtr tmpDir; nsresult rv = GetSpecialSystemDirectory(OS_TemporaryDirectory, getter_AddRefs(tmpDir)); + if (NS_SUCCEEDED(rv)) { nsAutoCString tmpPath; rv = tmpDir->GetNativePath(tmpPath); @@ -82,13 +87,131 @@ SandboxBrokerPolicyFactory::SandboxBrokerPolicyFactory() } #endif + // Read permissions + // No read blocking at level 2 and below + if (Preferences::GetInt("security.sandbox.content.level") <= 2) { + policy->AddDir(rdonly, "/"); + mCommonContentPolicy.reset(policy); + return; + } + policy->AddPath(rdonly, "/dev/urandom"); + policy->AddPath(rdonly, "/proc/cpuinfo"); + policy->AddPath(rdonly, "/proc/meminfo"); + policy->AddDir(rdonly, "/lib"); + policy->AddDir(rdonly, "/etc"); + policy->AddDir(rdonly, "/usr/share"); + policy->AddDir(rdonly, "/usr/local/share"); + policy->AddDir(rdonly, "/usr/lib"); + policy->AddDir(rdonly, "/usr/lib32"); + policy->AddDir(rdonly, "/usr/lib64"); + policy->AddDir(rdonly, "/usr/X11R6/lib/X11/fonts"); + policy->AddDir(rdonly, "/usr/tmp"); + policy->AddDir(rdonly, "/var/tmp"); + policy->AddDir(rdonly, "/sys/devices/cpu"); + policy->AddDir(rdonly, "/sys/devices/system/cpu"); + + // Configuration dirs in the homedir that we want to allow read + // access to. + mozilla::Array confDirs = { + ".config", + ".themes", + ".fonts", + }; + + nsCOMPtr homeDir; + rv = GetSpecialSystemDirectory(Unix_HomeDirectory, getter_AddRefs(homeDir)); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr confDir; + + for (auto dir : confDirs) { + rv = homeDir->Clone(getter_AddRefs(confDir)); + if (NS_SUCCEEDED(rv)) { + rv = confDir->AppendNative(nsDependentCString(dir)); + if (NS_SUCCEEDED(rv)) { + nsAutoCString tmpPath; + rv = confDir->GetNativePath(tmpPath); + if (NS_SUCCEEDED(rv)) { + policy->AddDir(rdonly, tmpPath.get()); + } + } + } + } + + // ~/.local/share (for themes) + rv = homeDir->Clone(getter_AddRefs(confDir)); + if (NS_SUCCEEDED(rv)) { + rv = confDir->AppendNative(NS_LITERAL_CSTRING(".local")); + if (NS_SUCCEEDED(rv)) { + rv = confDir->AppendNative(NS_LITERAL_CSTRING("share")); + } + if (NS_SUCCEEDED(rv)) { + nsAutoCString tmpPath; + rv = confDir->GetNativePath(tmpPath); + if (NS_SUCCEEDED(rv)) { + policy->AddDir(rdonly, tmpPath.get()); + } + } + } + + // ~/.fonts.conf (Fontconfig) + rv = homeDir->Clone(getter_AddRefs(confDir)); + if (NS_SUCCEEDED(rv)) { + rv = confDir->AppendNative(NS_LITERAL_CSTRING(".fonts.conf")); + if (NS_SUCCEEDED(rv)) { + nsAutoCString tmpPath; + rv = confDir->GetNativePath(tmpPath); + if (NS_SUCCEEDED(rv)) { + policy->AddPath(rdonly, tmpPath.get()); + } + } + } + + // .pangorc + rv = homeDir->Clone(getter_AddRefs(confDir)); + if (NS_SUCCEEDED(rv)) { + rv = confDir->AppendNative(NS_LITERAL_CSTRING(".pangorc")); + if (NS_SUCCEEDED(rv)) { + nsAutoCString tmpPath; + rv = confDir->GetNativePath(tmpPath); + if (NS_SUCCEEDED(rv)) { + policy->AddPath(rdonly, tmpPath.get()); + } + } + } + } + + // Firefox binary dir. + // Note that unlike the previous cases, we use NS_GetSpecialDirectory + // instead of GetSpecialSystemDirectory. The former requires a working XPCOM + // system, which may not be the case for some tests. For quering for the + // location of XPCOM things, we can use it anyway. + nsCOMPtr ffDir; + rv = NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(ffDir)); + if (NS_SUCCEEDED(rv)) { + nsAutoCString tmpPath; + rv = ffDir->GetNativePath(tmpPath); + if (NS_SUCCEEDED(rv)) { + policy->AddDir(rdonly, tmpPath.get()); + } + } + + if (mozilla::IsDevelopmentBuild()) { + // If this is a developer build the resources are symlinks to outside the binary dir. + // Therefore in non-release builds we allow reads from the whole repository. + // MOZ_DEVELOPER_REPO_DIR is set by mach run. + const char *developer_repo_dir = PR_GetEnv("MOZ_DEVELOPER_REPO_DIR"); + if (developer_repo_dir) { + policy->AddDir(rdonly, developer_repo_dir); + } + } + mCommonContentPolicy.reset(policy); #endif } #ifdef MOZ_CONTENT_SANDBOX UniquePtr -SandboxBrokerPolicyFactory::GetContentPolicy(int aPid) +SandboxBrokerPolicyFactory::GetContentPolicy(int aPid, bool aFileProcess) { // Policy entries that vary per-process (currently the only reason // that can happen is because they contain the pid) are added here. @@ -103,21 +226,43 @@ SandboxBrokerPolicyFactory::GetContentPolicy(int aPid) UniquePtr policy(new SandboxBroker::Policy(*mCommonContentPolicy)); + // Bug 1198550: the profiler's replacement for dl_iterate_phdr + policy->AddPath(rdonly, nsPrintfCString("/proc/%d/maps", aPid).get()); + + // Bug 1198552: memory reporting. + policy->AddPath(rdonly, nsPrintfCString("/proc/%d/statm", aPid).get()); + policy->AddPath(rdonly, nsPrintfCString("/proc/%d/smaps", aPid).get()); // Now read any extra paths, this requires accessing user preferences // so we can only do it now. Our constructor is initialized before // user preferences are read in. - nsAdoptingCString extraPathString = + nsAdoptingCString extraReadPathString = + Preferences::GetCString("security.sandbox.content.read_path_whitelist"); + AddDynamicPathList(policy.get(), extraReadPathString, rdonly); + nsAdoptingCString extraWritePathString = Preferences::GetCString("security.sandbox.content.write_path_whitelist"); - if (extraPathString) { - for (const nsACString& path : extraPathString.Split(',')) { - nsCString trimPath(path); - trimPath.Trim(" ", true, true); - policy->AddDynamic(rdwr, trimPath.get()); - } + AddDynamicPathList(policy.get(), extraWritePathString, rdwr); + + // file:// processes get global read permissions + if (aFileProcess) { + policy->AddDir(rdonly, "/"); } // Return the common policy. return policy; + +} + +void +SandboxBrokerPolicyFactory::AddDynamicPathList(SandboxBroker::Policy *policy, + nsAdoptingCString& pathList, + int perms) { + if (pathList) { + for (const nsACString& path : pathList.Split(',')) { + nsCString trimPath(path); + trimPath.Trim(" ", true, true); + policy->AddDynamic(perms, trimPath.get()); + } + } } #endif // MOZ_CONTENT_SANDBOX diff --git a/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.h b/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.h index 0581fba565526..8c6052f080d54 100644 --- a/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.h +++ b/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.h @@ -16,11 +16,14 @@ class SandboxBrokerPolicyFactory { SandboxBrokerPolicyFactory(); #ifdef MOZ_CONTENT_SANDBOX - UniquePtr GetContentPolicy(int aPid); + UniquePtr GetContentPolicy(int aPid, bool aFileProcess); #endif private: UniquePtr mCommonContentPolicy; + static void AddDynamicPathList(SandboxBroker::Policy *policy, + nsAdoptingCString& paths, + int perms); }; } // namespace mozilla diff --git a/security/sandbox/linux/reporter/SandboxReporterCommon.h b/security/sandbox/linux/reporter/SandboxReporterCommon.h index c17c1c72a9359..8b955f9300817 100644 --- a/security/sandbox/linux/reporter/SandboxReporterCommon.h +++ b/security/sandbox/linux/reporter/SandboxReporterCommon.h @@ -31,6 +31,7 @@ struct SandboxReport { // user-granted permissions. enum class ProcType : uint8_t { CONTENT, + FILE, MEDIA_PLUGIN, }; diff --git a/security/sandbox/test/browser_content_sandbox_fs.js b/security/sandbox/test/browser_content_sandbox_fs.js index 0f3a2263f7421..8d7b790725726 100644 --- a/security/sandbox/test/browser_content_sandbox_fs.js +++ b/security/sandbox/test/browser_content_sandbox_fs.js @@ -276,14 +276,19 @@ async function testFileAccess() { // that will be read from either a web or file process. let tests = []; + // The Linux test runners create the temporary profile in the same + // system temp dir we give write access to, so this gives a false + // positive. let profileDir = GetProfileDir(); - tests.push({ - desc: "profile dir", // description - ok: false, // expected to succeed? - browser: webBrowser, // browser to run test in - file: profileDir, // nsIFile object - minLevel: minProfileReadSandboxLevel(), // min level to enable test - }); + if (!isLinux()) { + tests.push({ + desc: "profile dir", // description + ok: false, // expected to succeed? + browser: webBrowser, // browser to run test in + file: profileDir, // nsIFile object + minLevel: minProfileReadSandboxLevel(), // min level to enable test + }); + } if (fileContentProcessEnabled) { tests.push({ desc: "profile dir", @@ -336,19 +341,15 @@ async function testFileAccess() { } } - // Should we enable this /var test on Linux? Once we are running - // with read access restrictions on Linux, this todo will fail and - // should then be removed. - if (isLinux()) { - todo(level >= minHomeReadSandboxLevel(), "enable /var test on Linux?"); - } - if (isMac()) { + if (isMac() || isLinux()) { let varDir = GetDir("/var"); - // Mac sandbox rules use /private/var because /var is a symlink - // to /private/var on OS X. Make sure that hasn't changed. - varDir.normalize(); - Assert.ok(varDir.path === "/private/var", "/var resolves to /private/var"); + if (isMac()) { + // Mac sandbox rules use /private/var because /var is a symlink + // to /private/var on OS X. Make sure that hasn't changed. + varDir.normalize(); + Assert.ok(varDir.path === "/private/var", "/var resolves to /private/var"); + } tests.push({ desc: "/var", From d791c78487b829d3a84c6716d8caa3bb5a3f8ca5 Mon Sep 17 00:00:00 2001 From: Gian-Carlo Pascutto Date: Thu, 6 Jul 2017 15:31:13 +0200 Subject: [PATCH 18/56] Bug 1308400 - Symlink handling for read brokering. r=jld MozReview-Commit-ID: BP1gFdDbqXD --HG-- extra : rebase_source : 5db26ad21e40ab19228ac8a978215b97cf8b3b28 --- .../sandbox/linux/SandboxBrokerClient.cpp | 2 +- .../sandbox/linux/broker/SandboxBroker.cpp | 122 ++++++- security/sandbox/linux/broker/SandboxBroker.h | 13 +- .../linux/broker/SandboxBrokerCommon.cpp | 14 + .../linux/broker/SandboxBrokerCommon.h | 2 + .../linux/broker/SandboxBrokerRealpath.cpp | 302 ++++++++++++++++++ security/sandbox/linux/broker/moz.build | 1 + 7 files changed, 444 insertions(+), 12 deletions(-) create mode 100644 security/sandbox/linux/broker/SandboxBrokerRealpath.cpp diff --git a/security/sandbox/linux/SandboxBrokerClient.cpp b/security/sandbox/linux/SandboxBrokerClient.cpp index d9fcbfd1cba3b..9adac30a51770 100644 --- a/security/sandbox/linux/SandboxBrokerClient.cpp +++ b/security/sandbox/linux/SandboxBrokerClient.cpp @@ -143,7 +143,7 @@ SandboxBrokerClient::DoCall(const Request* aReq, const char* aPath, // actually exist, if it's something that's optional or part of a // search path (e.g., shared libraries). In those cases, this // error message is expected. - SANDBOX_LOG_ERROR("Rejected errno %d op %d flags 0%o path %s", + SANDBOX_LOG_ERROR("Failed errno %d op %d flags 0%o path %s", resp.mError, aReq->mOp, aReq->mFlags, path); } if (openedFd >= 0) { diff --git a/security/sandbox/linux/broker/SandboxBroker.cpp b/security/sandbox/linux/broker/SandboxBroker.cpp index 0225234d9b5b9..969cb313410f1 100644 --- a/security/sandbox/linux/broker/SandboxBroker.cpp +++ b/security/sandbox/linux/broker/SandboxBroker.cpp @@ -21,6 +21,7 @@ #include #endif +#include "base/string_util.h" #include "mozilla/Assertions.h" #include "mozilla/DebugOnly.h" #include "mozilla/Move.h" @@ -200,14 +201,20 @@ SandboxBroker::Policy::AddDir(int aPerms, const char* aPath) return; } + // Add a Prefix permission on things inside the dir. nsDependentCString path(aPath); MOZ_ASSERT(path.Length() <= kMaxPathLen - 1); // Enforce trailing / on aPath - if (path[path.Length() - 1] != '/') { + if (path.Last() != '/') { path.Append('/'); } - Policy::AddPrefixInternal(aPerms, path); + + // Add a path permission on the dir itself so it can + // be opened. We're guaranteed to have a trailing / now, + // so just cut that. + path.Truncate(path.Length() - 1); + Policy::AddPath(aPerms, path.get(), AddAlways); } void @@ -423,8 +430,7 @@ SandboxBroker::ConvertToRealPath(char* aPath, size_t aBufSize, size_t aPathLen) if (strstr(aPath, "..") != nullptr) { char* result = realpath(aPath, nullptr); if (result != nullptr) { - strncpy(aPath, result, aBufSize); - aPath[aBufSize - 1] = '\0'; + base::strlcpy(aPath, result, aBufSize); free(result); // Size changed, but guaranteed to be 0 terminated aPathLen = strlen(aPath); @@ -434,6 +440,64 @@ SandboxBroker::ConvertToRealPath(char* aPath, size_t aBufSize, size_t aPathLen) return aPathLen; } +nsCString +SandboxBroker::ReverseSymlinks(const nsACString& aPath) +{ + // Revert any symlinks we previously resolved. + int32_t cutLength = aPath.Length(); + nsCString cutPath(Substring(aPath, 0, cutLength)); + + for (;;) { + nsCString orig; + bool found = mSymlinkMap.Get(cutPath, &orig); + if (found) { + orig.Append(Substring(aPath, cutLength, aPath.Length() - cutLength)); + return orig; + } + // Not found? Remove a path component and try again. + int32_t pos = cutPath.RFindChar('/'); + if (pos == kNotFound || pos <= 0) { + // will be empty + return orig; + } else { + // Cut until just before the / + cutLength = pos; + cutPath.Assign(Substring(cutPath, 0, cutLength)); + } + } +} + +int +SandboxBroker::SymlinkPermissions(const char* aPath, const size_t aPathLen) +{ + // Work on a temporary copy, so we can reverse it. + // Because we bail on a writable dir, SymlinkPath + // might not restore the callers' path exactly. + char pathBufSymlink[kMaxPathLen + 1]; + strcpy(pathBufSymlink, aPath); + + nsCString orig = ReverseSymlinks(nsDependentCString(pathBufSymlink, aPathLen)); + if (!orig.IsEmpty()) { + if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) { + SANDBOX_LOG_ERROR("Reversing %s -> %s", aPath, orig.get()); + } + base::strlcpy(pathBufSymlink, orig.get(), sizeof(pathBufSymlink)); + } + + int perms = 0; + // Resolve relative paths, propagate permissions and + // fail if a symlink is in a writable path. The output is in perms. + char* result = SandboxBroker::SymlinkPath(mPolicy.get(), pathBufSymlink, NULL, &perms); + if (result != NULL) { + free(result); + // We finished the translation, so we have a usable return in "perms". + return perms; + } else { + // Empty path means we got a writable dir in the chain. + return 0; + } +} + void SandboxBroker::ThreadMain(void) { @@ -452,8 +516,8 @@ SandboxBroker::ThreadMain(void) char recvBuf[2 * (kMaxPathLen + 1)]; char pathBuf[kMaxPathLen + 1]; char pathBuf2[kMaxPathLen + 1]; - size_t pathLen; - size_t pathLen2; + size_t pathLen = 0; + size_t pathLen2 = 0; char respBuf[kMaxPathLen + 1]; // Also serves as struct stat Request req; Response resp; @@ -538,6 +602,18 @@ SandboxBroker::ThreadMain(void) pathLen = ConvertToRealPath(pathBuf, sizeof(pathBuf), pathLen); perms = mPolicy->Lookup(nsDependentCString(pathBuf, pathLen)); + // We don't have read permissions on the requested dir. + // Did we arrive from a symlink in a path that is not writable? + // Then try to figure out the original path and see if that is readable. + if (!(perms & MAY_READ)) { + // Work on the original path, + // this reverses ConvertToRealPath above. + int symlinkPerms = SymlinkPermissions(recvBuf, first_len); + if (symlinkPerms > 0) { + perms = symlinkPerms; + } + } + // Same for the second path. pathLen2 = strnlen(pathBuf2, kMaxPathLen); if (pathLen2 > 0) { @@ -698,6 +774,34 @@ SandboxBroker::ThreadMain(void) if (permissive || AllowOperation(R_OK, perms)) { ssize_t respSize = readlink(pathBuf, (char*)&respBuf, sizeof(respBuf)); if (respSize >= 0) { + if (respSize > 0) { + // Record the mapping so we can invert the file to the original + // symlink. + nsDependentCString orig(pathBuf, pathLen); + nsDependentCString xlat(respBuf, respSize); + if (!orig.Equals(xlat) && xlat[0] == '/') { + if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) { + SANDBOX_LOG_ERROR("Recording mapping %s -> %s", + xlat.get(), orig.get()); + } + mSymlinkMap.Put(xlat, orig); + } + // Make sure we can invert a fully resolved mapping too. If our + // caller is realpath, and there's a relative path involved, the + // client side will try to open this one. + char *resolvedBuf = realpath(pathBuf, nullptr); + if (resolvedBuf) { + nsDependentCString resolvedXlat(resolvedBuf); + if (!orig.Equals(resolvedXlat) && !xlat.Equals(resolvedXlat)) { + if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) { + SANDBOX_LOG_ERROR("Recording mapping %s -> %s", + resolvedXlat.get(), orig.get()); + } + mSymlinkMap.Put(resolvedXlat, orig); + } + free(resolvedBuf); + } + } resp.mError = respSize; ios[1].iov_base = &respBuf; ios[1].iov_len = respSize; @@ -747,9 +851,9 @@ void SandboxBroker::AuditDenial(int aOp, int aFlags, int aPerms, const char* aPath) { if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) { - SANDBOX_LOG_ERROR("SandboxBroker: denied op=%d rflags=%o perms=%d path=%s for pid=%d" \ - " error=\"%s\"", aOp, aFlags, aPerms, aPath, mChildPid, - strerror(errno)); + SANDBOX_LOG_ERROR("SandboxBroker: denied op=%s rflags=%o perms=%d path=%s for pid=%d", + OperationDescription[aOp], aFlags, + aPerms, aPath, mChildPid); } } diff --git a/security/sandbox/linux/broker/SandboxBroker.h b/security/sandbox/linux/broker/SandboxBroker.h index 7012451175adb..18853be856cee 100644 --- a/security/sandbox/linux/broker/SandboxBroker.h +++ b/security/sandbox/linux/broker/SandboxBroker.h @@ -56,10 +56,10 @@ class SandboxBroker final }; // Bitwise operations on enum values return ints, so just use int in // the hash table type (and below) to avoid cluttering code with casts. - typedef nsDataHashtable PathMap; + typedef nsDataHashtable PathPermissionMap; class Policy { - PathMap mMap; + PathPermissionMap mMap; public: Policy(); Policy(const Policy& aOther); @@ -120,6 +120,9 @@ class SandboxBroker final const int mChildPid; const UniquePtr mPolicy; + typedef nsDataHashtable PathMap; + PathMap mSymlinkMap; + SandboxBroker(UniquePtr aPolicy, int aChildPid, int& aClientFd); void ThreadMain(void) override; @@ -127,6 +130,12 @@ class SandboxBroker final void AuditDenial(int aOp, int aFlags, int aPerms, const char* aPath); // Remap relative paths to absolute paths. size_t ConvertToRealPath(char* aPath, size_t aBufSize, size_t aPathLen); + nsCString ReverseSymlinks(const nsACString& aPath); + // Retrieves permissions for the path the original symlink sits in. + int SymlinkPermissions(const char* aPath, const size_t aPathLen); + // In SandboxBrokerRealPath.cpp + char* SymlinkPath(const Policy* aPolicy, const char* __restrict aPath, + char* __restrict aResolved, int* aPermission); // Holding a UniquePtr should disallow copying, but to make that explicit: SandboxBroker(const SandboxBroker&) = delete; diff --git a/security/sandbox/linux/broker/SandboxBrokerCommon.cpp b/security/sandbox/linux/broker/SandboxBrokerCommon.cpp index 1141fb87ab5f5..58262b5389853 100644 --- a/security/sandbox/linux/broker/SandboxBrokerCommon.cpp +++ b/security/sandbox/linux/broker/SandboxBrokerCommon.cpp @@ -30,6 +30,20 @@ namespace mozilla { +const char* SandboxBrokerCommon::OperationDescription[] = { + "open", + "access", + "stat", + "chmod", + "link", + "symlink", + "mkdir", + "rename", + "rmdir", + "unlink", + "readlink" +}; + /* static */ ssize_t SandboxBrokerCommon::RecvWithFd(int aFd, const iovec* aIO, size_t aNumIO, int* aPassedFdPtr) diff --git a/security/sandbox/linux/broker/SandboxBrokerCommon.h b/security/sandbox/linux/broker/SandboxBrokerCommon.h index dbd17e0b957bd..3769539075035 100644 --- a/security/sandbox/linux/broker/SandboxBrokerCommon.h +++ b/security/sandbox/linux/broker/SandboxBrokerCommon.h @@ -38,6 +38,8 @@ class SandboxBrokerCommon { SANDBOX_FILE_UNLINK, SANDBOX_FILE_READLINK, }; + // String versions of the above + static const char* OperationDescription[]; struct Request { Operation mOp; diff --git a/security/sandbox/linux/broker/SandboxBrokerRealpath.cpp b/security/sandbox/linux/broker/SandboxBrokerRealpath.cpp new file mode 100644 index 0000000000000..59ac2a93d0b59 --- /dev/null +++ b/security/sandbox/linux/broker/SandboxBrokerRealpath.cpp @@ -0,0 +1,302 @@ +/* + * Copyright (c) 2003 Constantin S. Svintsoff + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The names of the authors may not be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +/* + * This is originally from: + * android-n-mr2-preview-1-303-gccec0f4c1 + * libc/upstream-freebsd/lib/libc/stdlib/realpath.c + */ + +#if defined(LIBC_SCCS) && !defined(lint) +static char sccsid[] = "@(#)realpath.c 8.1 (Berkeley) 2/16/94"; +#endif /* LIBC_SCCS and not lint */ +#include + +#include +#include + +#include +#include +#include +#include + +#include "base/string_util.h" +#include "SandboxBroker.h" + +// base::strlcpy +using namespace base; + +// Original copy in, but not usable from here: +// toolkit/crashreporter/google-breakpad/src/common/linux/linux_libc_support.cc +static size_t my_strlcat(char* s1, const char* s2, size_t len) { + size_t pos1 = 0; + + while (pos1 < len && s1[pos1] != '\0') + pos1++; + + if (pos1 == len) + return pos1; + + return pos1 + strlcpy(s1 + pos1, s2, len - pos1); +} + +namespace mozilla { + +/* + * Original: realpath + * Find the real name of path, by removing all ".", ".." and symlink + * components. Returns (resolved) on success, or (NULL) on failure, + * in which case the path which caused trouble is left in (resolved). + * Changes: + * Resolve relative paths, but don't allow backing out of a symlink + * target. Fail with permission error if any dir is writable. + */ +char* SandboxBroker::SymlinkPath(const Policy* policy, + const char* __restrict path, + char* __restrict resolved, + int* perms) +{ + struct stat sb; + char *p, *q, *s; + size_t left_len, resolved_len, backup_allowed; + unsigned symlinks; + int m, slen; + char left[PATH_MAX], next_token[PATH_MAX], symlink[PATH_MAX]; + + if (*perms) { + *perms = 0; + } + if (path == NULL) { + errno = EINVAL; + return (NULL); + } + if (path[0] == '\0') { + errno = ENOENT; + return (NULL); + } + if (resolved == NULL) { + resolved = (char*)malloc(PATH_MAX); + if (resolved == NULL) + return (NULL); + m = 1; + } else + m = 0; + symlinks = 0; + backup_allowed = PATH_MAX; + if (path[0] == '/') { + resolved[0] = '/'; + resolved[1] = '\0'; + if (path[1] == '\0') + return (resolved); + resolved_len = 1; + left_len = strlcpy(left, path + 1, sizeof(left)); + } else { + if (getcwd(resolved, PATH_MAX) == NULL) { + if (m) + free(resolved); + else { + resolved[0] = '.'; + resolved[1] = '\0'; + } + return (NULL); + } + resolved_len = strlen(resolved); + left_len = strlcpy(left, path, sizeof(left)); + } + if (left_len >= sizeof(left) || resolved_len >= PATH_MAX) { + if (m) + free(resolved); + errno = ENAMETOOLONG; + return (NULL); + } + + /* + * Iterate over path components in `left'. + */ + while (left_len != 0) { + /* + * Extract the next path component and adjust `left' + * and its length. + */ + p = strchr(left, '/'); + s = p ? p : left + left_len; + if (s - left >= (ssize_t)sizeof(next_token)) { + if (m) + free(resolved); + errno = ENAMETOOLONG; + return (NULL); + } + memcpy(next_token, left, s - left); + next_token[s - left] = '\0'; + left_len -= s - left; + if (p != NULL) + memmove(left, s + 1, left_len + 1); + if (resolved[resolved_len - 1] != '/') { + if (resolved_len + 1 >= PATH_MAX) { + if (m) + free(resolved); + errno = ENAMETOOLONG; + return (NULL); + } + resolved[resolved_len++] = '/'; + resolved[resolved_len] = '\0'; + } + if (next_token[0] == '\0') { + /* Handle consequential slashes. */ + continue; + } + else if (strcmp(next_token, ".") == 0) + continue; + else if (strcmp(next_token, "..") == 0) { + /* + * Strip the last path component except when we have + * single "/" + */ + if (resolved_len > 1) { + if (backup_allowed > 0) { + resolved[resolved_len - 1] = '\0'; + q = strrchr(resolved, '/') + 1; + *q = '\0'; + resolved_len = q - resolved; + backup_allowed--; + } else { + // Backing out past a symlink target. + // We don't allow this, because it can eliminate + // permissions we accumulated while descending. + if (m) + free(resolved); + errno = EPERM; + return (NULL); + } + } + continue; + } + + /* + * Append the next path component and lstat() it. + */ + resolved_len = my_strlcat(resolved, next_token, PATH_MAX); + backup_allowed++; + if (resolved_len >= PATH_MAX) { + if (m) + free(resolved); + errno = ENAMETOOLONG; + return (NULL); + } + if (lstat(resolved, &sb) != 0) { + if (m) + free(resolved); + return (NULL); + } + if (S_ISLNK(sb.st_mode)) { + if (symlinks++ > MAXSYMLINKS) { + if (m) + free(resolved); + errno = ELOOP; + return (NULL); + } + /* Our changes start here: + * It's a symlink, check for write permissions on the path where + * it sits in, in which case we won't resolve and just error out. */ + int link_path_perms = policy->Lookup(resolved); + if (link_path_perms & MAY_WRITE) { + if (m) + free(resolved); + errno = EPERM; + return (NULL); + } else { + /* Accumulate permissions so far */ + *perms |= link_path_perms; + } + /* Original symlink lookup code */ + slen = readlink(resolved, symlink, sizeof(symlink) - 1); + if (slen < 0) { + if (m) + free(resolved); + return (NULL); + } + symlink[slen] = '\0'; + if (symlink[0] == '/') { + resolved[1] = 0; + resolved_len = 1; + } else if (resolved_len > 1) { + /* Strip the last path component. */ + resolved[resolved_len - 1] = '\0'; + q = strrchr(resolved, '/') + 1; + *q = '\0'; + resolved_len = q - resolved; + } + + /* + * If there are any path components left, then + * append them to symlink. The result is placed + * in `left'. + */ + if (p != NULL) { + if (symlink[slen - 1] != '/') { + if (slen + 1 >= (ssize_t)sizeof(symlink)) { + if (m) + free(resolved); + errno = ENAMETOOLONG; + return (NULL); + } + symlink[slen] = '/'; + symlink[slen + 1] = 0; + } + left_len = my_strlcat(symlink, left, sizeof(symlink)); + if (left_len >= sizeof(left)) { + if (m) + free(resolved); + errno = ENAMETOOLONG; + return (NULL); + } + } + left_len = strlcpy(left, symlink, sizeof(left)); + backup_allowed = 0; + } else if (!S_ISDIR(sb.st_mode) && p != NULL) { + if (m) + free(resolved); + errno = ENOTDIR; + return (NULL); + } + } + + /* + * Remove trailing slash except when the resolved pathname + * is a single "/". + */ + if (resolved_len > 1 && resolved[resolved_len - 1] == '/') + resolved[resolved_len - 1] = '\0'; + + /* Accumulate permissions. */ + *perms |= policy->Lookup(resolved); + + return (resolved); +} + +} diff --git a/security/sandbox/linux/broker/moz.build b/security/sandbox/linux/broker/moz.build index 8a1d248d984fd..c2db7584939a1 100644 --- a/security/sandbox/linux/broker/moz.build +++ b/security/sandbox/linux/broker/moz.build @@ -14,6 +14,7 @@ SOURCES += [ 'SandboxBroker.cpp', 'SandboxBrokerCommon.cpp', 'SandboxBrokerPolicyFactory.cpp', + 'SandboxBrokerRealpath.cpp', ] if CONFIG['MOZ_ALSA']: From b4e70523629de3f28a57b3318edb11c6ffc17af7 Mon Sep 17 00:00:00 2001 From: Gian-Carlo Pascutto Date: Mon, 10 Jul 2017 20:29:09 +0200 Subject: [PATCH 19/56] Bug 1308400 - Whitelist paths with test files during testing. r=Alex_Gaynor MozReview-Commit-ID: LMWAv3GP5c4 --HG-- extra : rebase_source : 80ae220e415510bb21c7f184eaaeb02ac27b91d0 --- testing/mochitest/runtests.py | 1 + testing/mozharness/configs/unittests/linux_unittest.py | 2 ++ testing/mozharness/scripts/desktop_unittest.py | 3 ++- testing/xpcshell/runxpcshelltests.py | 4 ++-- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/testing/mochitest/runtests.py b/testing/mochitest/runtests.py index 59d63062f6b86..b258f16ffc902 100644 --- a/testing/mochitest/runtests.py +++ b/testing/mochitest/runtests.py @@ -1757,6 +1757,7 @@ def buildProfile(self, options): options.extraPrefs.append( "browser.tabs.remote.autostart=%s" % ('true' if options.e10s else 'false')) + options.extraPrefs.append( "dom.ipc.tabs.nested.enabled=%s" % ('true' if options.nested_oop else 'false')) diff --git a/testing/mozharness/configs/unittests/linux_unittest.py b/testing/mozharness/configs/unittests/linux_unittest.py index 2dd955da58c3a..2993ab8b9ef4d 100644 --- a/testing/mozharness/configs/unittests/linux_unittest.py +++ b/testing/mozharness/configs/unittests/linux_unittest.py @@ -117,6 +117,7 @@ "--screenshot-on-fail", "--cleanup-crashes", "--marionette-startup-timeout=180", + "--work-path=%(abs_work_dir)s", ], "run_filename": "runtests.py", "testsdir": "mochitest" @@ -148,6 +149,7 @@ "--log-raw=%(raw_log_file)s", "--log-errorsummary=%(error_summary_file)s", "--cleanup-crashes", + "--work-path=%(abs_work_dir)s", ], "run_filename": "runreftest.py", "testsdir": "reftest" diff --git a/testing/mozharness/scripts/desktop_unittest.py b/testing/mozharness/scripts/desktop_unittest.py index a778722550bb1..f0bd0daefc1fb 100755 --- a/testing/mozharness/scripts/desktop_unittest.py +++ b/testing/mozharness/scripts/desktop_unittest.py @@ -250,6 +250,7 @@ def query_abs_dirs(self): c = self.config dirs = {} + dirs['abs_work_dir'] = abs_dirs['abs_work_dir'] dirs['abs_app_install_dir'] = os.path.join(abs_dirs['abs_work_dir'], 'application') dirs['abs_test_install_dir'] = os.path.join(abs_dirs['abs_work_dir'], 'tests') dirs['abs_test_extensions_dir'] = os.path.join(dirs['abs_test_install_dir'], 'extensions') @@ -373,13 +374,13 @@ def _query_abs_base_cmd(self, suite_category, suite): str_format_values = { 'binary_path': self.binary_path, 'symbols_path': self._query_symbols_url(), + 'abs_work_dir' : dirs['abs_work_dir'], 'abs_app_dir': abs_app_dir, 'abs_res_dir': abs_res_dir, 'raw_log_file': raw_log_file, 'error_summary_file': error_summary_file, 'gtest_dir': os.path.join(dirs['abs_test_install_dir'], 'gtest'), - 'abs_work_dir': dirs['abs_work_dir'], } # TestingMixin._download_and_extract_symbols() will set diff --git a/testing/xpcshell/runxpcshelltests.py b/testing/xpcshell/runxpcshelltests.py index 01711d04722e2..3093b72e3b07b 100755 --- a/testing/xpcshell/runxpcshelltests.py +++ b/testing/xpcshell/runxpcshelltests.py @@ -950,8 +950,8 @@ def buildCoreEnvironment(self): self.env.setdefault('MOZ_DISABLE_NONLOCAL_CONNECTIONS', '1') if self.mozInfo.get("topsrcdir") is not None: self.env["MOZ_DEVELOPER_REPO_DIR"] = self.mozInfo["topsrcdir"].encode() - if self.mozInfo.get("topobjdir") is not None: - self.env["MOZ_DEVELOPER_OBJ_DIR"] = self.mozInfo["topobjdir"].encode() + if sys.platform.startswith('linux'): + self.env.setdefault('MOZ_SANDBOX_LOGGING', '1') # Disable the content process sandbox for the xpcshell tests. They # currently attempt to do things like bind() sockets, which is not From 7ee246522d0456f7877f49f5ab9441476b539043 Mon Sep 17 00:00:00 2001 From: Gian-Carlo Pascutto Date: Mon, 10 Jul 2017 20:20:49 +0200 Subject: [PATCH 20/56] Bug 1308400 - Report failures in file processes too. r=jld MozReview-Commit-ID: 549WuWKaJeM --HG-- extra : rebase_source : 22d6348e602f2ceae546502fa0050ab0960ec075 --- security/sandbox/linux/reporter/SandboxReporter.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/security/sandbox/linux/reporter/SandboxReporter.cpp b/security/sandbox/linux/reporter/SandboxReporter.cpp index a9275f00465a2..dd69e0ae7057d 100644 --- a/security/sandbox/linux/reporter/SandboxReporter.cpp +++ b/security/sandbox/linux/reporter/SandboxReporter.cpp @@ -139,6 +139,9 @@ SubmitToTelemetry(const SandboxReport& aReport) case SandboxReport::ProcType::MEDIA_PLUGIN: key.AppendLiteral("gmp"); break; + case SandboxReport::ProcType::FILE: + key.AppendLiteral("file"); + break; default: MOZ_ASSERT(false); } From 2d965a95c3a5cefc6331ee85a700917f9f777119 Mon Sep 17 00:00:00 2001 From: Gian-Carlo Pascutto Date: Mon, 24 Jul 2017 21:20:17 +0200 Subject: [PATCH 21/56] Bug 1308400 - Disable logging again, fix removed object dir. r=bustage --- testing/xpcshell/runxpcshelltests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/xpcshell/runxpcshelltests.py b/testing/xpcshell/runxpcshelltests.py index 3093b72e3b07b..01711d04722e2 100755 --- a/testing/xpcshell/runxpcshelltests.py +++ b/testing/xpcshell/runxpcshelltests.py @@ -950,8 +950,8 @@ def buildCoreEnvironment(self): self.env.setdefault('MOZ_DISABLE_NONLOCAL_CONNECTIONS', '1') if self.mozInfo.get("topsrcdir") is not None: self.env["MOZ_DEVELOPER_REPO_DIR"] = self.mozInfo["topsrcdir"].encode() - if sys.platform.startswith('linux'): - self.env.setdefault('MOZ_SANDBOX_LOGGING', '1') + if self.mozInfo.get("topobjdir") is not None: + self.env["MOZ_DEVELOPER_OBJ_DIR"] = self.mozInfo["topobjdir"].encode() # Disable the content process sandbox for the xpcshell tests. They # currently attempt to do things like bind() sockets, which is not From ee712ab23e18e862390d55b69cf34bb1684c4f5c Mon Sep 17 00:00:00 2001 From: Christoph Kerschbaumer Date: Mon, 24 Jul 2017 20:34:29 +0200 Subject: [PATCH 22/56] Bug 1381755: Updating the data: URI inheritance security model renders test browser_CTP_data_urls.js superfluous. r=bsmedberg --- browser/base/content/test/plugins/browser.ini | 3 - .../test/plugins/browser_CTP_data_urls.js | 255 ------------------ .../content/test/plugins/plugin_data_url.html | 11 - ...ochitest-browser-chrome-e10s.runtimes.json | 1 - .../mochitest-browser-chrome.runtimes.json | 1 - 5 files changed, 271 deletions(-) delete mode 100644 browser/base/content/test/plugins/browser_CTP_data_urls.js delete mode 100644 browser/base/content/test/plugins/plugin_data_url.html diff --git a/browser/base/content/test/plugins/browser.ini b/browser/base/content/test/plugins/browser.ini index 9144f6a1e4fbb..56f5fba1a9d06 100644 --- a/browser/base/content/test/plugins/browser.ini +++ b/browser/base/content/test/plugins/browser.ini @@ -22,7 +22,6 @@ support-files = plugin_bug820497.html plugin_clickToPlayAllow.html plugin_clickToPlayDeny.html - plugin_data_url.html plugin_favorfallback.html plugin_hidden_to_visible.html plugin_iframe.html @@ -58,8 +57,6 @@ tags = blocklist [browser_CTP_crashreporting.js] skip-if = !crashreporter tags = blocklist -[browser_CTP_data_urls.js] -tags = blocklist [browser_CTP_drag_drop.js] tags = blocklist [browser_CTP_favorfallback.js] diff --git a/browser/base/content/test/plugins/browser_CTP_data_urls.js b/browser/base/content/test/plugins/browser_CTP_data_urls.js deleted file mode 100644 index f7a46ca798125..0000000000000 --- a/browser/base/content/test/plugins/browser_CTP_data_urls.js +++ /dev/null @@ -1,255 +0,0 @@ -var rootDir = getRootDirectory(gTestPath); -const gTestRoot = rootDir.replace("chrome://mochitests/content/", "http://127.0.0.1:8888/"); -var gPluginHost = Components.classes["@mozilla.org/plugin/host;1"].getService(Components.interfaces.nsIPluginHost); -var gTestBrowser = null; - -add_task(async function() { - registerCleanupFunction(function() { - clearAllPluginPermissions(); - setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in"); - setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in"); - Services.prefs.clearUserPref("plugins.click_to_play"); - Services.prefs.clearUserPref("extensions.blocklist.suppressUI"); - gBrowser.removeCurrentTab(); - window.focus(); - gTestBrowser = null; - }); - - gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); - gTestBrowser = gBrowser.selectedBrowser; - - Services.prefs.setBoolPref("plugins.click_to_play", true); - Services.prefs.setBoolPref("extensions.blocklist.suppressUI", true); - - setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Test Plug-in"); - setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Second Test Plug-in"); -}); - -// Test that the click-to-play doorhanger still works when navigating to data URLs -add_task(async function() { - await promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_data_url.html"); - - // Work around for delayed PluginBindingAttached - await promiseUpdatePluginBindings(gTestBrowser); - - let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); - ok(popupNotification, "Test 1a, Should have a click-to-play notification"); - - let pluginInfo = await promiseForPluginInfo("test"); - ok(!pluginInfo.activated, "Test 1a, plugin should not be activated"); - - let loadPromise = promiseTabLoadEvent(gBrowser.selectedTab); - await ContentTask.spawn(gTestBrowser, {}, async function() { - // navigate forward to a page with 'test' in it - content.document.getElementById("data-link-1").click(); - }); - await loadPromise; - - // Work around for delayed PluginBindingAttached - await promiseUpdatePluginBindings(gTestBrowser); - - popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); - ok(popupNotification, "Test 1b, Should have a click-to-play notification"); - - pluginInfo = await promiseForPluginInfo("test"); - ok(!pluginInfo.activated, "Test 1b, plugin should not be activated"); - - let promise = promisePopupNotification("click-to-play-plugins"); - await ContentTask.spawn(gTestBrowser, {}, async function() { - let plugin = content.document.getElementById("test"); - let bounds = plugin.getBoundingClientRect(); - let left = (bounds.left + bounds.right) / 2; - let top = (bounds.top + bounds.bottom) / 2; - let utils = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor) - .getInterface(Components.interfaces.nsIDOMWindowUtils); - utils.sendMouseEvent("mousedown", left, top, 0, 1, 0, false, 0, 0); - utils.sendMouseEvent("mouseup", left, top, 0, 1, 0, false, 0, 0); - }); - await promise; - - // Simulate clicking the "Allow Always" button. - let condition = () => !PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed && - PopupNotifications.panel.firstChild; - await promiseForCondition(condition); - PopupNotifications.panel.firstChild._primaryButton.click(); - - // check plugin state - pluginInfo = await promiseForPluginInfo("test"); - ok(pluginInfo.activated, "Test 1b, plugin should be activated"); -}); - -// Test that the click-to-play notification doesn't break when navigating -// to data URLs with multiple plugins. -add_task(async function() { - // We click activated above - clearAllPluginPermissions(); - - await promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_data_url.html"); - - // Work around for delayed PluginBindingAttached - await promiseUpdatePluginBindings(gTestBrowser); - - let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); - ok(notification, "Test 2a, Should have a click-to-play notification"); - - let pluginInfo = await promiseForPluginInfo("test"); - ok(!pluginInfo.activated, "Test 2a, plugin should not be activated"); - - let loadPromise = promiseTabLoadEvent(gBrowser.selectedTab); - await ContentTask.spawn(gTestBrowser, {}, async function() { - // navigate forward to a page with 'test1' & 'test2' in it - content.document.getElementById("data-link-2").click(); - }); - await loadPromise; - - // Work around for delayed PluginBindingAttached - await ContentTask.spawn(gTestBrowser, {}, async function() { - content.document.getElementById("test1").clientTop; - content.document.getElementById("test2").clientTop; - }); - - pluginInfo = await promiseForPluginInfo("test1"); - ok(!pluginInfo.activated, "Test 2a, test1 should not be activated"); - pluginInfo = await promiseForPluginInfo("test2"); - ok(!pluginInfo.activated, "Test 2a, test2 should not be activated"); - - notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); - ok(notification, "Test 2b, Should have a click-to-play notification"); - - await promiseForNotificationShown(notification); - - // Simulate choosing "Allow now" for the test plugin - is(notification.options.pluginData.size, 2, "Test 2b, Should have two types of plugin in the notification"); - - let centerAction = null; - for (let action of notification.options.pluginData.values()) { - if (action.pluginName == "Test") { - centerAction = action; - break; - } - } - ok(centerAction, "Test 2b, found center action for the Test plugin"); - - let centerItem = null; - for (let item of PopupNotifications.panel.firstChild.childNodes) { - is(item.value, "block", "Test 2b, all plugins should start out blocked"); - if (item.action == centerAction) { - centerItem = item; - break; - } - } - ok(centerItem, "Test 2b, found center item for the Test plugin"); - - // "click" the button to activate the Test plugin - centerItem.value = "allownow"; - PopupNotifications.panel.firstChild._primaryButton.click(); - - // Work around for delayed PluginBindingAttached - await promiseUpdatePluginBindings(gTestBrowser); - - // check plugin state - pluginInfo = await promiseForPluginInfo("test1"); - ok(pluginInfo.activated, "Test 2b, plugin should be activated"); -}); - -add_task(async function() { - // We click activated above - clearAllPluginPermissions(); - - await promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_data_url.html"); - - // Work around for delayed PluginBindingAttached - await promiseUpdatePluginBindings(gTestBrowser); -}); - -// Test that when navigating to a data url, the plugin permission is inherited -add_task(async function() { - let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); - ok(notification, "Test 3a, Should have a click-to-play notification"); - - // check plugin state - let pluginInfo = await promiseForPluginInfo("test"); - ok(!pluginInfo.activated, "Test 3a, plugin should not be activated"); - - let promise = promisePopupNotification("click-to-play-plugins"); - await ContentTask.spawn(gTestBrowser, {}, async function() { - let plugin = content.document.getElementById("test"); - let bounds = plugin.getBoundingClientRect(); - let left = (bounds.left + bounds.right) / 2; - let top = (bounds.top + bounds.bottom) / 2; - let utils = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor) - .getInterface(Components.interfaces.nsIDOMWindowUtils); - utils.sendMouseEvent("mousedown", left, top, 0, 1, 0, false, 0, 0); - utils.sendMouseEvent("mouseup", left, top, 0, 1, 0, false, 0, 0); - }); - await promise; - - // Simulate clicking the "Allow Always" button. - let condition = () => !PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed && - PopupNotifications.panel.firstChild; - await promiseForCondition(condition); - PopupNotifications.panel.firstChild._primaryButton.click(); - - // check plugin state - pluginInfo = await promiseForPluginInfo("test"); - ok(pluginInfo.activated, "Test 3a, plugin should be activated"); - - let loadPromise = promiseTabLoadEvent(gBrowser.selectedTab); - await ContentTask.spawn(gTestBrowser, {}, async function() { - // navigate forward to a page with 'test' in it - content.document.getElementById("data-link-1").click(); - }); - await loadPromise; - - // Work around for delayed PluginBindingAttached - await promiseUpdatePluginBindings(gTestBrowser); - - // check plugin state - pluginInfo = await promiseForPluginInfo("test"); - ok(pluginInfo.activated, "Test 3b, plugin should be activated"); - - clearAllPluginPermissions(); -}); - -// Test that the click-to-play doorhanger still works -// when directly navigating to data URLs. -// Fails, bug XXX. Plugins plus a data url don't fire a load event. -/* -add_task(function* () { - yield promiseTabLoadEvent(gBrowser.selectedTab, - "data:text/html,Hi!"); - - // Work around for delayed PluginBindingAttached - yield promiseUpdatePluginBindings(gTestBrowser); - - let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); - ok(notification, "Test 4a, Should have a click-to-play notification"); - - // check plugin state - let pluginInfo = yield promiseForPluginInfo("test"); - ok(!pluginInfo.activated, "Test 4a, plugin should not be activated"); - - let promise = promisePopupNotification("click-to-play-plugins"); - yield ContentTask.spawn(gTestBrowser, {}, function* () { - let plugin = content.document.getElementById("test"); - let bounds = plugin.getBoundingClientRect(); - let left = (bounds.left + bounds.right) / 2; - let top = (bounds.top + bounds.bottom) / 2; - let utils = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor) - .getInterface(Components.interfaces.nsIDOMWindowUtils); - utils.sendMouseEvent("mousedown", left, top, 0, 1, 0, false, 0, 0); - utils.sendMouseEvent("mouseup", left, top, 0, 1, 0, false, 0, 0); - }); - yield promise; - - // Simulate clicking the "Allow Always" button. - let condition = () => !PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed && - PopupNotifications.panel.firstChild; - yield promiseForCondition(condition); - PopupNotifications.panel.firstChild._primaryButton.click(); - - // check plugin state - pluginInfo = yield promiseForPluginInfo("test"); - ok(pluginInfo.activated, "Test 4a, plugin should be activated"); -}); -*/ diff --git a/browser/base/content/test/plugins/plugin_data_url.html b/browser/base/content/test/plugins/plugin_data_url.html deleted file mode 100644 index 77e10114431b0..0000000000000 --- a/browser/base/content/test/plugins/plugin_data_url.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - data: with one plugin -
- - data: with two plugins -
- - - diff --git a/testing/runtimes/mochitest-browser-chrome-e10s.runtimes.json b/testing/runtimes/mochitest-browser-chrome-e10s.runtimes.json index a8b1a0e27f749..38a5f434a30eb 100644 --- a/testing/runtimes/mochitest-browser-chrome-e10s.runtimes.json +++ b/testing/runtimes/mochitest-browser-chrome-e10s.runtimes.json @@ -141,7 +141,6 @@ "browser/base/content/test/newtab/browser_newtab_perwindow_private_browsing.js": 2913, "browser/base/content/test/newtab/browser_newtab_unpin.js": 3032, "browser/base/content/test/plugins/browser_CTP_crashreporting.js": 4878, - "browser/base/content/test/plugins/browser_CTP_data_urls.js": 4053, "browser/base/content/test/plugins/browser_CTP_drag_drop.js": 5737, "browser/base/content/test/plugins/browser_CTP_notificationBar.js": 4802, "browser/base/content/test/plugins/browser_CTP_remove_navigate.js": 3322, diff --git a/testing/runtimes/mochitest-browser-chrome.runtimes.json b/testing/runtimes/mochitest-browser-chrome.runtimes.json index 422f3017d23d8..6eebe09027040 100644 --- a/testing/runtimes/mochitest-browser-chrome.runtimes.json +++ b/testing/runtimes/mochitest-browser-chrome.runtimes.json @@ -157,7 +157,6 @@ "browser/base/content/test/newtab/browser_newtab_perwindow_private_browsing.js": 2984, "browser/base/content/test/newtab/browser_newtab_unpin.js": 2896, "browser/base/content/test/plugins/browser_CTP_crashreporting.js": 4623, - "browser/base/content/test/plugins/browser_CTP_data_urls.js": 3580, "browser/base/content/test/plugins/browser_CTP_drag_drop.js": 5004, "browser/base/content/test/plugins/browser_CTP_notificationBar.js": 4726, "browser/base/content/test/plugins/browser_CTP_outsideScrollArea.js": 2057, From 3e70ba6667771c8e478328610d27a87d8b97521d Mon Sep 17 00:00:00 2001 From: Christoph Kerschbaumer Date: Mon, 24 Jul 2017 20:34:44 +0200 Subject: [PATCH 23/56] Bug 1381755: Test that data: URIs can't activate plugins. r=bsmedberg --- dom/plugins/test/mochitest/browser.ini | 2 ++ .../test/mochitest/browser_data_url_plugin.js | 20 +++++++++++++++++++ .../test/mochitest/plugin_data_url_test.html | 14 +++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 dom/plugins/test/mochitest/browser_data_url_plugin.js create mode 100644 dom/plugins/test/mochitest/plugin_data_url_test.html diff --git a/dom/plugins/test/mochitest/browser.ini b/dom/plugins/test/mochitest/browser.ini index ddcdde8e3f7bf..b98e52155471c 100644 --- a/dom/plugins/test/mochitest/browser.ini +++ b/dom/plugins/test/mochitest/browser.ini @@ -1,6 +1,7 @@ [DEFAULT] support-files = head.js + plugin_data_url_test.html plugin_test.html plugin_subframe_test.html plugin_no_scroll_div.html @@ -14,3 +15,4 @@ skip-if = (!e10s || os != "win") [browser_pluginscroll.js] skip-if = (true || !e10s || os != "win") # Bug 1213631 [browser_bug1335475.js] +[browser_data_url_plugin.js] diff --git a/dom/plugins/test/mochitest/browser_data_url_plugin.js b/dom/plugins/test/mochitest/browser_data_url_plugin.js new file mode 100644 index 0000000000000..f81df7a768d20 --- /dev/null +++ b/dom/plugins/test/mochitest/browser_data_url_plugin.js @@ -0,0 +1,20 @@ +var rootDir = getRootDirectory(gTestPath); +const gTestRoot = rootDir.replace("chrome://mochitests/content/", "http://127.0.0.1:8888/"); + +add_task(async function() { + await SpecialPowers.pushPrefEnv({ + "set": [["security.data_uri.unique_opaque_origin", true]], + }); + is(navigator.plugins.length, 0, + "plugins should not be available to chrome-privilege pages"); + + await BrowserTestUtils.withNewTab({ gBrowser, url: gTestRoot + "plugin_data_url_test.html" }, async function(browser) { + await ContentTask.spawn(browser, null, async function() { + ok(content.window.navigator.plugins.length > 0, + "plugins should be available to HTTP-loaded pages"); + let dataFrameWin = content.document.getElementById("dataFrame").contentWindow; + is(dataFrameWin.navigator.plugins.length, 0, + "plugins should not be available to data: URI in iframe on a site"); + }); + }); +}); diff --git a/dom/plugins/test/mochitest/plugin_data_url_test.html b/dom/plugins/test/mochitest/plugin_data_url_test.html new file mode 100644 index 0000000000000..49ee4b1057ba8 --- /dev/null +++ b/dom/plugins/test/mochitest/plugin_data_url_test.html @@ -0,0 +1,14 @@ + + + + + + + +
+ + + + + From b2294259ada55f0147b27e233e548baaa293d1e6 Mon Sep 17 00:00:00 2001 From: Christoph Kerschbaumer Date: Mon, 24 Jul 2017 20:35:02 +0200 Subject: [PATCH 24/56] Bug 1383649 - Convert more tests within layout/ to comply with new data: URI inheritance model. r=smaug --- layout/base/tests/test_bug399284.html | 2 +- layout/style/test/test_media_queries.html | 5 +++-- layout/style/test/test_namespace_rule.html | 6 +++--- layout/style/test/test_property_syntax_errors.html | 4 +++- layout/style/test/test_selectors.html | 10 ++++++---- layout/style/test/test_value_cloning.html | 6 ++++-- 6 files changed, 20 insertions(+), 13 deletions(-) diff --git a/layout/base/tests/test_bug399284.html b/layout/base/tests/test_bug399284.html index 09d7297427241..6bab3f010133a 100644 --- a/layout/base/tests/test_bug399284.html +++ b/layout/base/tests/test_bug399284.html @@ -102,7 +102,7 @@ function testFontSize(frame) { - var iframeDoc = $(frame).contentDocument; + var iframeDoc = SpecialPowers.wrap($(frame)).contentDocument; var size = parseInt(iframeDoc.defaultView. getComputedStyle(iframeDoc.getElementById("testPara"), null). diff --git a/layout/style/test/test_media_queries.html b/layout/style/test/test_media_queries.html index 4bbaee023d0b7..1e1edb08b2a95 100644 --- a/layout/style/test/test_media_queries.html +++ b/layout/style/test/test_media_queries.html @@ -158,8 +158,9 @@ var htmldoc = "" + link + link + ""; var docurl = "data:text/html," + escape(htmldoc); post_clone_test(docurl, function() { - var clonedoc = iframe.contentDocument; - var clonewin = iframe.contentWindow; + var wrappedFrame = SpecialPowers.wrap(iframe); + var clonedoc = wrappedFrame.contentDocument; + var clonewin = wrappedFrame.contentWindow; var links = clonedoc.getElementsByTagName("link"); // cause a clone var clonedsheet = links[1].sheet; diff --git a/layout/style/test/test_namespace_rule.html b/layout/style/test/test_namespace_rule.html index 105bc12f07754..484ded9ab86cf 100644 --- a/layout/style/test/test_namespace_rule.html +++ b/layout/style/test/test_namespace_rule.html @@ -16,9 +16,9 @@ var style_text; function run() { - var iframe = $("iframe"); - var ifwin = iframe.contentWindow; - var ifdoc = iframe.contentDocument; + var wrappedFrame = SpecialPowers.wrap($("iframe")); + var ifwin = wrappedFrame.contentWindow; + var ifdoc = wrappedFrame.contentDocument; var ifbody = ifdoc.getElementsByTagName("body")[0]; function setup_style_text() { diff --git a/layout/style/test/test_property_syntax_errors.html b/layout/style/test/test_property_syntax_errors.html index be1127deff8ff..a10ad16a6d2f5 100644 --- a/layout/style/test/test_property_syntax_errors.html +++ b/layout/style/test/test_property_syntax_errors.html @@ -84,7 +84,9 @@ function run() { var gDeclaration = document.getElementById("testnode").style; - var gQuirksDeclaration = document.getElementById("quirks").contentDocument + var quirksFrame = document.getElementById("quirks"); + var wrappedFrame = SpecialPowers.wrap(quirksFrame); + var gQuirksDeclaration = wrappedFrame.contentDocument .getElementById("testnode").style; for (var property in gCSSProperties) { diff --git a/layout/style/test/test_selectors.html b/layout/style/test/test_selectors.html index 54f3390d410be..adb170b1fccba 100644 --- a/layout/style/test/test_selectors.html +++ b/layout/style/test/test_selectors.html @@ -134,8 +134,9 @@ } var docurl = "data:text/html," + escape(html_doc); defer_clonedoc_tests(docurl, function() { - var clonedoc = cloneiframe.contentDocument; - var clonewin = cloneiframe.contentWindow; + var wrappedCloneFrame = SpecialPowers.wrap(cloneiframe); + var clonedoc = wrappedCloneFrame.contentDocument; + var clonewin = wrappedCloneFrame.contentWindow; if (typeof(body_contents) != "string") { body_contents(clonedoc.body); @@ -221,8 +222,9 @@ var docurl = "data:text/html," + escape(html_doc); defer_clonedoc_tests(docurl, function() { - var clonedoc = cloneiframe.contentDocument; - var clonewin = cloneiframe.contentWindow; + var wrappedCloneFrame = SpecialPowers.wrap(cloneiframe); + var clonedoc = wrappedCloneFrame.contentDocument; + var clonewin = wrappedCloneFrame.contentWindow; var links = clonedoc.getElementsByTagName("link"); // cause a clone links[1].sheet.insertRule("#nonexistent { color: purple}", 0); diff --git a/layout/style/test/test_value_cloning.html b/layout/style/test/test_value_cloning.html index 8487565b3a7a8..32de7a60f6935 100644 --- a/layout/style/test/test_value_cloning.html +++ b/layout/style/test/test_value_cloning.html @@ -111,14 +111,16 @@ var start_ser = []; var start_compute = []; var test_cs = []; - var ifdoc = iframe.contentDocument; + var wrappedFrame = SpecialPowers.wrap(iframe); + var ifdoc = wrappedFrame.contentDocument; + var ifwin = wrappedFrame.contentWindow; for (var idx = 0; idx < test_queue.length; ++idx) { var current_item = test_queue[idx]; var info = gCSSProperties[current_item.prop]; var test = ifdoc.getElementById("test" + idx); - var cur_cs = iframe.contentWindow.getComputedStyle(test); + var cur_cs = ifwin.getComputedStyle(test); test_cs.push(cur_cs); var cur_ser = ifdoc.styleSheets[0].cssRules[3*idx+2].style.getPropertyValue(current_item.prop); if (cur_ser == "") { From 15ed99965d40cf9d34e526f2e5a223239b96ae39 Mon Sep 17 00:00:00 2001 From: Christoph Kerschbaumer Date: Mon, 24 Jul 2017 20:35:21 +0200 Subject: [PATCH 25/56] Bug 1383732 - Update more reftests to comply with new data: URI inheritance model. r=smaug --- layout/reftests/bugs/381746-1-framea.html | 1 + layout/reftests/bugs/381746-1-frameb.html | 1 + layout/reftests/bugs/381746-1-ref.html | 4 ++-- layout/reftests/bugs/381746-1.html | 20 +++++++++---------- layout/reftests/bugs/537507-1-frame.xul | 1 + layout/reftests/bugs/537507-1-ref.xul | 2 +- layout/reftests/bugs/537507-1.xul | 4 +--- layout/reftests/bugs/537507-2-frame.xul | 1 + layout/reftests/bugs/537507-2-ref.html | 2 +- layout/reftests/bugs/537507-2.html | 4 +--- layout/reftests/bugs/858803-1-frame.xhtml | 2 ++ layout/reftests/bugs/858803-1.html | 3 +-- .../scrolling/iframe-border-radius-frame.html | 1 + .../scrolling/iframe-border-radius-ref.html | 3 +-- .../scrolling/iframe-border-radius.html | 3 +-- 15 files changed, 26 insertions(+), 26 deletions(-) create mode 100644 layout/reftests/bugs/381746-1-framea.html create mode 100644 layout/reftests/bugs/381746-1-frameb.html create mode 100644 layout/reftests/bugs/537507-1-frame.xul create mode 100644 layout/reftests/bugs/537507-2-frame.xul create mode 100644 layout/reftests/bugs/858803-1-frame.xhtml create mode 100644 layout/reftests/scrolling/iframe-border-radius-frame.html diff --git a/layout/reftests/bugs/381746-1-framea.html b/layout/reftests/bugs/381746-1-framea.html new file mode 100644 index 0000000000000..ebd0150cc24f9 --- /dev/null +++ b/layout/reftests/bugs/381746-1-framea.html @@ -0,0 +1 @@ +text diff --git a/layout/reftests/bugs/381746-1-frameb.html b/layout/reftests/bugs/381746-1-frameb.html new file mode 100644 index 0000000000000..8e27be7d6154a --- /dev/null +++ b/layout/reftests/bugs/381746-1-frameb.html @@ -0,0 +1 @@ +text diff --git a/layout/reftests/bugs/381746-1-ref.html b/layout/reftests/bugs/381746-1-ref.html index dabccf6615fdd..02d020e107010 100644 --- a/layout/reftests/bugs/381746-1-ref.html +++ b/layout/reftests/bugs/381746-1-ref.html @@ -9,7 +9,7 @@ - - + + diff --git a/layout/reftests/bugs/381746-1.html b/layout/reftests/bugs/381746-1.html index 427b9cee3efca..0085090387c0e 100644 --- a/layout/reftests/bugs/381746-1.html +++ b/layout/reftests/bugs/381746-1.html @@ -1,17 +1,17 @@ Testcase Bug 381746 - odd and changing border in frameset - diff --git a/layout/reftests/bugs/537507-1-frame.xul b/layout/reftests/bugs/537507-1-frame.xul new file mode 100644 index 0000000000000..20d5b9774a19f --- /dev/null +++ b/layout/reftests/bugs/537507-1-frame.xul @@ -0,0 +1 @@ + diff --git a/layout/reftests/bugs/537507-1-ref.xul b/layout/reftests/bugs/537507-1-ref.xul index ff1678711a5ce..0edf8311cd1ea 100644 --- a/layout/reftests/bugs/537507-1-ref.xul +++ b/layout/reftests/bugs/537507-1-ref.xul @@ -4,5 +4,5 @@ diff --git a/layout/reftests/bugs/537507-1.xul b/layout/reftests/bugs/537507-1.xul index 3c753a7eb3a0e..3cc96944c62b8 100644 --- a/layout/reftests/bugs/537507-1.xul +++ b/layout/reftests/bugs/537507-1.xul @@ -4,7 +4,5 @@ diff --git a/layout/reftests/bugs/537507-2-frame.xul b/layout/reftests/bugs/537507-2-frame.xul new file mode 100644 index 0000000000000..20d5b9774a19f --- /dev/null +++ b/layout/reftests/bugs/537507-2-frame.xul @@ -0,0 +1 @@ + diff --git a/layout/reftests/bugs/537507-2-ref.html b/layout/reftests/bugs/537507-2-ref.html index b8807ca8964df..0469911fc7691 100644 --- a/layout/reftests/bugs/537507-2-ref.html +++ b/layout/reftests/bugs/537507-2-ref.html @@ -1,5 +1,5 @@ The iframe below should show the text 'Here'
- + diff --git a/layout/reftests/bugs/537507-2.html b/layout/reftests/bugs/537507-2.html index e19549c9f5810..3c19a9b4f46ea 100644 --- a/layout/reftests/bugs/537507-2.html +++ b/layout/reftests/bugs/537507-2.html @@ -1,7 +1,5 @@ The iframe below should show the text 'Here'
- + diff --git a/layout/reftests/bugs/858803-1-frame.xhtml b/layout/reftests/bugs/858803-1-frame.xhtml new file mode 100644 index 0000000000000..e56af71b2f57e --- /dev/null +++ b/layout/reftests/bugs/858803-1-frame.xhtml @@ -0,0 +1,2 @@ + + diff --git a/layout/reftests/bugs/858803-1.html b/layout/reftests/bugs/858803-1.html index 5cfed5f4d57c4..caafbb3bcc0db 100644 --- a/layout/reftests/bugs/858803-1.html +++ b/layout/reftests/bugs/858803-1.html @@ -1,8 +1,7 @@ - diff --git a/layout/reftests/scrolling/iframe-border-radius-frame.html b/layout/reftests/scrolling/iframe-border-radius-frame.html new file mode 100644 index 0000000000000..05a60efa137b7 --- /dev/null +++ b/layout/reftests/scrolling/iframe-border-radius-frame.html @@ -0,0 +1 @@ +

Hello

Kitty

Hello

Kitty

Hello

Kitty

Hello

Kitty

Hello

Kitty

Hello

Kitty diff --git a/layout/reftests/scrolling/iframe-border-radius-ref.html b/layout/reftests/scrolling/iframe-border-radius-ref.html index 70686ec26181f..814bcd3c14960 100644 --- a/layout/reftests/scrolling/iframe-border-radius-ref.html +++ b/layout/reftests/scrolling/iframe-border-radius-ref.html @@ -1,8 +1,7 @@ - + - + diff --git a/docshell/base/crashtests/file_432114-2.xhtml b/docshell/base/crashtests/file_432114-2.xhtml new file mode 100644 index 0000000000000..40bf886b8ec2d --- /dev/null +++ b/docshell/base/crashtests/file_432114-2.xhtml @@ -0,0 +1 @@ + diff --git a/dom/base/crashtests/504224.html b/dom/base/crashtests/504224.html index 71e561d7429ad..9ba9fe1a7bb87 100644 --- a/dom/base/crashtests/504224.html +++ b/dom/base/crashtests/504224.html @@ -3,7 +3,7 @@ Crash [@ nsFocusManager::GetCommonAncestor], part 2 - + diff --git a/dom/base/crashtests/593302-1.html b/dom/base/crashtests/593302-1.html index e6abff8081f19..96d731cfb4ad5 100644 --- a/dom/base/crashtests/593302-1.html +++ b/dom/base/crashtests/593302-1.html @@ -24,6 +24,6 @@ - + diff --git a/dom/base/crashtests/851353-1.html b/dom/base/crashtests/851353-1.html index ac5d772b6886c..2af7de97aa6e5 100644 --- a/dom/base/crashtests/851353-1.html +++ b/dom/base/crashtests/851353-1.html @@ -20,6 +20,6 @@ - + diff --git a/dom/base/crashtests/file_504224.html b/dom/base/crashtests/file_504224.html new file mode 100644 index 0000000000000..9031ccbfad82a --- /dev/null +++ b/dom/base/crashtests/file_504224.html @@ -0,0 +1,7 @@ + + + + + diff --git a/dom/smil/crashtests/590425-1.html b/dom/smil/crashtests/590425-1.html index 80452607d26f9..906d348db2552 100644 --- a/dom/smil/crashtests/590425-1.html +++ b/dom/smil/crashtests/590425-1.html @@ -18,7 +18,7 @@ - + diff --git a/editor/libeditor/crashtests/336081-1.xhtml b/editor/libeditor/crashtests/336081-1.xhtml index da653c60147f8..5a499d9f5108f 100644 --- a/editor/libeditor/crashtests/336081-1.xhtml +++ b/editor/libeditor/crashtests/336081-1.xhtml @@ -39,7 +39,7 @@ function init() - + diff --git a/js/xpconnect/crashtests/751995.html b/js/xpconnect/crashtests/751995.html index fce9565ef2031..9f2758faf6198 100644 --- a/js/xpconnect/crashtests/751995.html +++ b/js/xpconnect/crashtests/751995.html @@ -32,5 +32,5 @@ - + diff --git a/js/xpconnect/crashtests/753162.html b/js/xpconnect/crashtests/753162.html index 67a0efeebecfc..c633488ea25fb 100644 --- a/js/xpconnect/crashtests/753162.html +++ b/js/xpconnect/crashtests/753162.html @@ -19,5 +19,5 @@ - + diff --git a/js/xpconnect/crashtests/806751.html b/js/xpconnect/crashtests/806751.html index 4e44b9b829a76..0163cd4ae0d09 100644 --- a/js/xpconnect/crashtests/806751.html +++ b/js/xpconnect/crashtests/806751.html @@ -21,6 +21,6 @@ - + diff --git a/js/xpconnect/crashtests/851418.html b/js/xpconnect/crashtests/851418.html index e39ff83e65f0b..ef2a12bf976ad 100644 --- a/js/xpconnect/crashtests/851418.html +++ b/js/xpconnect/crashtests/851418.html @@ -18,6 +18,6 @@ - + diff --git a/js/xpconnect/crashtests/898939.html b/js/xpconnect/crashtests/898939.html index dd025728358c4..7cb238498a20f 100644 --- a/js/xpconnect/crashtests/898939.html +++ b/js/xpconnect/crashtests/898939.html @@ -13,6 +13,6 @@ - + diff --git a/layout/base/crashtests/606432-1.html b/layout/base/crashtests/606432-1.html index a166c94de211a..4473778723263 100644 --- a/layout/base/crashtests/606432-1.html +++ b/layout/base/crashtests/606432-1.html @@ -20,5 +20,5 @@ - + diff --git a/layout/base/crashtests/645572-1.html b/layout/base/crashtests/645572-1.html index 67d04f68b1e49..c5db4bf8da5c2 100644 --- a/layout/base/crashtests/645572-1.html +++ b/layout/base/crashtests/645572-1.html @@ -18,7 +18,7 @@ o232=o230.ownerDocument.getElementById('ifr42257').contentDocument.documentElement; o234=o196; tmp=o234.ownerDocument.createElement('iframe'); - tmp.src='data:text/html,' + escape("

"); + tmp.srcdoc="
"; tmp.id='ifr22371'; tmp.addEventListener("load", start_dataiframe12); o234.ownerDocument.documentElement.appendChild(tmp); diff --git a/layout/generic/crashtests/324318-1.html b/layout/generic/crashtests/324318-1.html index 2637b6d298d14..c9946caba96b9 100644 --- a/layout/generic/crashtests/324318-1.html +++ b/layout/generic/crashtests/324318-1.html @@ -24,6 +24,6 @@ - - + + diff --git a/layout/generic/crashtests/empty.html b/layout/generic/crashtests/empty.html new file mode 100644 index 0000000000000..18ecdcb795c33 --- /dev/null +++ b/layout/generic/crashtests/empty.html @@ -0,0 +1 @@ + diff --git a/layout/generic/crashtests/file_324318-1.html b/layout/generic/crashtests/file_324318-1.html new file mode 100644 index 0000000000000..90c29362e0e2d --- /dev/null +++ b/layout/generic/crashtests/file_324318-1.html @@ -0,0 +1 @@ +
tdc
diff --git a/layout/style/crashtests/592698-1.html b/layout/style/crashtests/592698-1.html index 9dcab0e32a4e8..b2620b512eafe 100644 --- a/layout/style/crashtests/592698-1.html +++ b/layout/style/crashtests/592698-1.html @@ -1,7 +1,7 @@ + srcdoc="
aaa"> From ab7ac78b9cb0ee78fe748e291cc840e2991481d1 Mon Sep 17 00:00:00 2001 From: Yoshi Huang Date: Mon, 24 Jul 2017 18:15:48 +0800 Subject: [PATCH 46/56] Bug 1382531 - Part 2: add pref for docshell/base/crashtests/914521.html. r=smaug I found that use a seperate document will have differnet behavior than the original test, so configured this test will run with pref off. --- docshell/base/crashtests/914521.html | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docshell/base/crashtests/914521.html b/docshell/base/crashtests/914521.html index 36efbb8af4a49..663388bf75c43 100644 --- a/docshell/base/crashtests/914521.html +++ b/docshell/base/crashtests/914521.html @@ -20,14 +20,22 @@ finish(); } +function init() +{ + SpecialPowers.pushPrefEnv({"set": [["security.data_uri.unique_opaque_origin", false]]}, start); +} + function start() { var html = " - + From 9a241577282238ad4a97ae8f55ba78913f3bc30f Mon Sep 17 00:00:00 2001 From: Bevis Tseng Date: Thu, 20 Jul 2017 17:15:42 +0800 Subject: [PATCH 47/56] Bug 1379414 - Fix ReadCompressedIndexDataValuesFromBlob(). r=asuth --- dom/indexedDB/ActorsParent.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/dom/indexedDB/ActorsParent.cpp b/dom/indexedDB/ActorsParent.cpp index d07dbe14ec8de..785c4c091e55b 100644 --- a/dom/indexedDB/ActorsParent.cpp +++ b/dom/indexedDB/ActorsParent.cpp @@ -7,6 +7,7 @@ #include "ActorsParent.h" #include +#include // UINTPTR_MAX, uintptr_t #include "FileInfo.h" #include "FileManager.h" #include "IDBObjectStore.h" @@ -854,6 +855,11 @@ ReadCompressedIndexDataValuesFromBlob(const uint8_t* aBlobData, AUTO_PROFILER_LABEL("ReadCompressedIndexDataValuesFromBlob", STORAGE); + if (uintptr_t(aBlobData) > UINTPTR_MAX - aBlobDataLength) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_FILE_CORRUPTED; + } + const uint8_t* blobDataIter = aBlobData; const uint8_t* blobDataEnd = aBlobData + aBlobDataLength; @@ -873,7 +879,8 @@ ReadCompressedIndexDataValuesFromBlob(const uint8_t* aBlobData, if (NS_WARN_IF(blobDataIter == blobDataEnd) || NS_WARN_IF(keyBufferLength > uint64_t(UINT32_MAX)) || - NS_WARN_IF(blobDataIter + keyBufferLength > blobDataEnd)) { + NS_WARN_IF(keyBufferLength > uintptr_t(blobDataEnd)) || + NS_WARN_IF(blobDataIter > blobDataEnd - keyBufferLength)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_FILE_CORRUPTED; } @@ -891,7 +898,8 @@ ReadCompressedIndexDataValuesFromBlob(const uint8_t* aBlobData, if (sortKeyBufferLength > 0) { if (NS_WARN_IF(blobDataIter == blobDataEnd) || NS_WARN_IF(sortKeyBufferLength > uint64_t(UINT32_MAX)) || - NS_WARN_IF(blobDataIter + sortKeyBufferLength > blobDataEnd)) { + NS_WARN_IF(sortKeyBufferLength > uintptr_t(blobDataEnd)) || + NS_WARN_IF(blobDataIter > blobDataEnd - sortKeyBufferLength)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_FILE_CORRUPTED; } From 8f6455242f5972c6df1faaa0b628198f04d2bfef Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Fri, 21 Jul 2017 15:11:02 -0700 Subject: [PATCH 48/56] Bug 1383215: Part 1 - Don't resolve module URIs to files when already cached. r=mccr8 MozReview-Commit-ID: KBhXhcJkRjp --HG-- extra : rebase_source : f5d9852dfa0bbda11d7ceee080bcde7e779c6312 extra : histedit_source : 151752f049ff9e6b2b73de51cbcef0d1d4f31906 --- js/xpconnect/loader/mozJSComponentLoader.cpp | 54 ++++++++++---------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/js/xpconnect/loader/mozJSComponentLoader.cpp b/js/xpconnect/loader/mozJSComponentLoader.cpp index d6b4531b72a7e..cb8febc1c3717 100644 --- a/js/xpconnect/loader/mozJSComponentLoader.cpp +++ b/js/xpconnect/loader/mozJSComponentLoader.cpp @@ -904,33 +904,6 @@ mozJSComponentLoader::ImportInto(const nsACString& aLocation, } ComponentLoaderInfo info(aLocation); - rv = info.EnsureResolvedURI(); - NS_ENSURE_SUCCESS(rv, rv); - - // get the JAR if there is one - nsCOMPtr jarURI; - jarURI = do_QueryInterface(info.ResolvedURI(), &rv); - nsCOMPtr baseFileURL; - if (NS_SUCCEEDED(rv)) { - nsCOMPtr baseURI; - while (jarURI) { - jarURI->GetJARFile(getter_AddRefs(baseURI)); - jarURI = do_QueryInterface(baseURI, &rv); - } - baseFileURL = do_QueryInterface(baseURI, &rv); - NS_ENSURE_SUCCESS(rv, rv); - } else { - baseFileURL = do_QueryInterface(info.ResolvedURI(), &rv); - NS_ENSURE_SUCCESS(rv, rv); - } - - nsCOMPtr sourceFile; - rv = baseFileURL->GetFile(getter_AddRefs(sourceFile)); - NS_ENSURE_SUCCESS(rv, rv); - - nsCOMPtr sourceLocalFile; - sourceLocalFile = do_QueryInterface(sourceFile, &rv); - NS_ENSURE_SUCCESS(rv, rv); rv = info.EnsureKey(); NS_ENSURE_SUCCESS(rv, rv); @@ -941,12 +914,37 @@ mozJSComponentLoader::ImportInto(const nsACString& aLocation, newEntry = new ModuleEntry(RootingContext::get(callercx)); if (!newEntry) return NS_ERROR_OUT_OF_MEMORY; + + rv = info.EnsureResolvedURI(); + NS_ENSURE_SUCCESS(rv, rv); + + // get the JAR if there is one + nsCOMPtr jarURI; + jarURI = do_QueryInterface(info.ResolvedURI(), &rv); + nsCOMPtr baseFileURL; + if (NS_SUCCEEDED(rv)) { + nsCOMPtr baseURI; + while (jarURI) { + jarURI->GetJARFile(getter_AddRefs(baseURI)); + jarURI = do_QueryInterface(baseURI, &rv); + } + baseFileURL = do_QueryInterface(baseURI, &rv); + NS_ENSURE_SUCCESS(rv, rv); + } else { + baseFileURL = do_QueryInterface(info.ResolvedURI(), &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr sourceFile; + rv = baseFileURL->GetFile(getter_AddRefs(sourceFile)); + NS_ENSURE_SUCCESS(rv, rv); + mInProgressImports.Put(info.Key(), newEntry); rv = info.EnsureURI(); NS_ENSURE_SUCCESS(rv, rv); RootedValue exception(callercx); - rv = ObjectForLocation(info, sourceLocalFile, &newEntry->obj, + rv = ObjectForLocation(info, sourceFile, &newEntry->obj, &newEntry->thisObjectKey, &newEntry->location, true, &exception); From b136b1f0599e2a18c6e8f986002bd65fb63a96ac Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Fri, 21 Jul 2017 15:12:32 -0700 Subject: [PATCH 49/56] Bug 1383215: Part 2 - Split out URI resolution code into ResolveURI helper. r=mccr8 MozReview-Commit-ID: Bfr67WQPq9l --HG-- extra : rebase_source : bbf9090d03c15d6c541aec12b090ab4dadcdab68 extra : histedit_source : 68a54268e601c1cb7521abb305fad2288ab4a03a --- startupcache/StartupCacheUtils.cpp | 80 ++++++++++++++++++------------ startupcache/StartupCacheUtils.h | 3 ++ 2 files changed, 50 insertions(+), 33 deletions(-) diff --git a/startupcache/StartupCacheUtils.cpp b/startupcache/StartupCacheUtils.cpp index 2cfecbff3c962..02c23a2522bac 100644 --- a/startupcache/StartupCacheUtils.cpp +++ b/startupcache/StartupCacheUtils.cpp @@ -145,6 +145,47 @@ canonicalizeBase(nsAutoCString &spec, return true; } +/** + * ResolveURI transforms a chrome: or resource: URI into the URI for its + * underlying resource, or returns any other URI unchanged. + */ +nsresult +ResolveURI(nsIURI *in, nsIURI **out) +{ + bool equals; + nsresult rv; + + // Resolve resource:// URIs. At the end of this if/else block, we + // have both spec and uri variables identifying the same URI. + if (NS_SUCCEEDED(in->SchemeIs("resource", &equals)) && equals) { + nsCOMPtr ioService = do_GetIOService(&rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr ph; + rv = ioService->GetProtocolHandler("resource", getter_AddRefs(ph)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr irph(do_QueryInterface(ph, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString spec; + rv = irph->ResolveURI(in, spec); + NS_ENSURE_SUCCESS(rv, rv); + + return ioService->NewURI(spec, nullptr, nullptr, out); + } else if (NS_SUCCEEDED(in->SchemeIs("chrome", &equals)) && equals) { + nsCOMPtr chromeReg = + mozilla::services::GetChromeRegistryService(); + if (!chromeReg) + return NS_ERROR_UNEXPECTED; + + return chromeReg->ConvertChromeURL(in, out); + } + + *out = do_AddRef(in).take(); + return NS_OK; +} + /** * PathifyURI transforms uris into useful zip paths * to make it easier to manipulate startup cache entries @@ -175,41 +216,14 @@ PathifyURI(nsIURI *in, nsACString &out) { bool equals; nsresult rv; - nsCOMPtr uri = in; - nsAutoCString spec; - - // Resolve resource:// URIs. At the end of this if/else block, we - // have both spec and uri variables identifying the same URI. - if (NS_SUCCEEDED(in->SchemeIs("resource", &equals)) && equals) { - nsCOMPtr ioService = do_GetIOService(&rv); - NS_ENSURE_SUCCESS(rv, rv); - - nsCOMPtr ph; - rv = ioService->GetProtocolHandler("resource", getter_AddRefs(ph)); - NS_ENSURE_SUCCESS(rv, rv); - nsCOMPtr irph(do_QueryInterface(ph, &rv)); - NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr uri; + rv = ResolveURI(in, getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); - rv = irph->ResolveURI(in, spec); - NS_ENSURE_SUCCESS(rv, rv); - - rv = ioService->NewURI(spec, nullptr, nullptr, getter_AddRefs(uri)); - NS_ENSURE_SUCCESS(rv, rv); - } else { - if (NS_SUCCEEDED(in->SchemeIs("chrome", &equals)) && equals) { - nsCOMPtr chromeReg = - mozilla::services::GetChromeRegistryService(); - if (!chromeReg) - return NS_ERROR_UNEXPECTED; - - rv = chromeReg->ConvertChromeURL(in, getter_AddRefs(uri)); - NS_ENSURE_SUCCESS(rv, rv); - } - - rv = uri->GetSpec(spec); - NS_ENSURE_SUCCESS(rv, rv); - } + nsAutoCString spec; + rv = uri->GetSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); if (!canonicalizeBase(spec, out)) { if (NS_SUCCEEDED(uri->SchemeIs("file", &equals)) && equals) { diff --git a/startupcache/StartupCacheUtils.h b/startupcache/StartupCacheUtils.h index 779ae514a9c17..95f2ae88dcb4f 100644 --- a/startupcache/StartupCacheUtils.h +++ b/startupcache/StartupCacheUtils.h @@ -38,6 +38,9 @@ nsresult NewBufferFromStorageStream(nsIStorageStream *storageStream, UniquePtr* buffer, uint32_t* len); +nsresult +ResolveURI(nsIURI *in, nsIURI **out); + nsresult PathifyURI(nsIURI *in, nsACString &out); } // namespace scache From 5e494303a286a5952380e8fb37c42c20f165d116 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Fri, 21 Jul 2017 15:42:38 -0700 Subject: [PATCH 50/56] Bug 1383215: Part 3 - Use scache::ResolveURI to resolve module URIs. r=mccr8 Since we now usually load modules from one of the startup caches, we usually have no need to ever actually create a channel in order to load them. Resolving the URIs directly is much cheaper in the normal case. MozReview-Commit-ID: 8W8RMHRnyBa --HG-- extra : rebase_source : 073ae92c3dc53e86084c1daa1ccfe720ade634c6 extra : histedit_source : cf6cc2b025a839e39aa48bf412ee3a273b549bbe --- js/xpconnect/loader/mozJSComponentLoader.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/js/xpconnect/loader/mozJSComponentLoader.cpp b/js/xpconnect/loader/mozJSComponentLoader.cpp index cb8febc1c3717..21ee7fed65108 100644 --- a/js/xpconnect/loader/mozJSComponentLoader.cpp +++ b/js/xpconnect/loader/mozJSComponentLoader.cpp @@ -246,8 +246,8 @@ class MOZ_STACK_CLASS ComponentLoaderInfo { nsIURI* ResolvedURI() { MOZ_ASSERT(mResolvedURI); return mResolvedURI; } nsresult EnsureResolvedURI() { - BEGIN_ENSURE(ResolvedURI, ScriptChannel); - return mScriptChannel->GetURI(getter_AddRefs(mResolvedURI)); + BEGIN_ENSURE(ResolvedURI, URI); + return ResolveURI(mURI, getter_AddRefs(mResolvedURI)); } nsAutoCString& Key() { return *mKey; } @@ -613,8 +613,10 @@ mozJSComponentLoader::ObjectForLocation(ComponentLoaderInfo& aInfo, bool writeToCache = false; StartupCache* cache = StartupCache::GetSingleton(); + aInfo.EnsureResolvedURI(); + nsAutoCString cachePath(kJSCachePrefix); - rv = PathifyURI(aInfo.URI(), cachePath); + rv = PathifyURI(aInfo.ResolvedURI(), cachePath); NS_ENSURE_SUCCESS(rv, rv); script = ScriptPreloader::GetSingleton().GetCachedScript(cx, cachePath); From 0fefc0e8c0836a51e0dbff3cc362835e7e61ddce Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Mon, 24 Jul 2017 20:32:42 -0700 Subject: [PATCH 51/56] Bug 1383215: Part 4 - Use location string as key in modules map. r=mccr8 Using the unmolested module location string as the cache key removes a huge chunk of overhead when loading cached modules. This also ensures that multiple URLs are not used to load the same module, which would result in it being loaded more than once in the new regime MozReview-Commit-ID: BAWoOJQSTc1 --HG-- extra : rebase_source : e5b295a498caf76e60efec4d174e558e9e55d77b extra : histedit_source : dd683966b30090b5702264c2903e6050be0e4137 --- js/xpconnect/loader/mozJSComponentLoader.cpp | 20 +++++++++++++++----- js/xpconnect/loader/mozJSComponentLoader.h | 8 +++++++- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/js/xpconnect/loader/mozJSComponentLoader.cpp b/js/xpconnect/loader/mozJSComponentLoader.cpp index 21ee7fed65108..bd36a28de8c83 100644 --- a/js/xpconnect/loader/mozJSComponentLoader.cpp +++ b/js/xpconnect/loader/mozJSComponentLoader.cpp @@ -196,6 +196,7 @@ mozJSComponentLoader::mozJSComponentLoader() : mModules(16), mImports(16), mInProgressImports(16), + mLocations(16), mInitialized(false) { MOZ_ASSERT(!sSelf, "mozJSComponentLoader should be a singleton"); @@ -250,11 +251,9 @@ class MOZ_STACK_CLASS ComponentLoaderInfo { return ResolveURI(mURI, getter_AddRefs(mResolvedURI)); } - nsAutoCString& Key() { return *mKey; } + const nsACString& Key() { return mLocation; } nsresult EnsureKey() { - ENSURE_DEPS(ResolvedURI); - mKey.emplace(); - return mResolvedURI->GetSpec(*mKey); + return NS_OK; } MOZ_MUST_USE nsresult GetLocation(nsCString& aLocation) { @@ -269,7 +268,6 @@ class MOZ_STACK_CLASS ComponentLoaderInfo { nsCOMPtr mURI; nsCOMPtr mScriptChannel; nsCOMPtr mResolvedURI; - Maybe mKey; // This is safe because we're MOZ_STACK_CLASS }; #undef BEGIN_ENSURE @@ -449,6 +447,7 @@ mozJSComponentLoader::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) size_t n = aMallocSizeOf(this); n += SizeOfTableExcludingThis(mModules, aMallocSizeOf); n += SizeOfTableExcludingThis(mImports, aMallocSizeOf); + n += mLocations.ShallowSizeOfExcludingThis(aMallocSizeOf); n += SizeOfTableExcludingThis(mInProgressImports, aMallocSizeOf); return n; } @@ -763,6 +762,7 @@ mozJSComponentLoader::UnloadModules() mInProgressImports.Clear(); mImports.Clear(); + mLocations.Clear(); for (auto iter = mModules.Iter(); !iter.Done(); iter.Next()) { iter.Data()->Clear(); @@ -941,6 +941,15 @@ mozJSComponentLoader::ImportInto(const nsACString& aLocation, rv = baseFileURL->GetFile(getter_AddRefs(sourceFile)); NS_ENSURE_SUCCESS(rv, rv); + rv = info.ResolvedURI()->GetSpec(newEntry->resolvedURL); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString* existingPath; + if (mLocations.Get(newEntry->resolvedURL, &existingPath) && *existingPath != info.Key()) { + return NS_ERROR_UNEXPECTED; + } + + mLocations.Put(newEntry->resolvedURL, new nsCString(info.Key())); mInProgressImports.Put(info.Key(), newEntry); rv = info.EnsureURI(); @@ -1112,6 +1121,7 @@ mozJSComponentLoader::Unload(const nsACString & aLocation) NS_ENSURE_SUCCESS(rv, rv); ModuleEntry* mod; if (mImports.Get(info.Key(), &mod)) { + mLocations.Remove(mod->resolvedURL); mImports.Remove(info.Key()); } diff --git a/js/xpconnect/loader/mozJSComponentLoader.h b/js/xpconnect/loader/mozJSComponentLoader.h index 5548139bfa448..fd69102396980 100644 --- a/js/xpconnect/loader/mozJSComponentLoader.h +++ b/js/xpconnect/loader/mozJSComponentLoader.h @@ -144,7 +144,8 @@ class mozJSComponentLoader : public mozilla::ModuleLoader, nsCOMPtr getfactoryobj; JS::PersistentRootedObject obj; JS::PersistentRootedScript thisObjectKey; - char* location; + char* location; + nsCString resolvedURL; }; friend class ModuleEntry; @@ -161,6 +162,11 @@ class mozJSComponentLoader : public mozilla::ModuleLoader, nsClassHashtable mImports; nsDataHashtable mInProgressImports; + // A map of on-disk file locations which are loaded as modules to the + // pre-resolved URIs they were loaded from. Used to prevent the same file + // from being loaded separately, from multiple URLs. + nsClassHashtable mLocations; + bool mInitialized; }; From cd5c5d6f7ff7614755a92d69bb02e655d4645d71 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Fri, 21 Jul 2017 18:01:42 -0700 Subject: [PATCH 52/56] Bug 1383215: Part 5 - Update tests that relied on loading the same JSM from multiple URLs. MozReview-Commit-ID: KEXGiMrauH7 --HG-- extra : rebase_source : c1b5a1e22c00bdc42cc7cdfae2f4718248c7965d extra : histedit_source : 968e9daffc7505853aac4b892178f27c3386aec1 --- .../source/test/fixtures/native-addon-test/index.js | 7 ++++++- js/xpconnect/loader/XPCOMUtils.jsm | 6 +++--- js/xpconnect/tests/components/js/xpctest_attributes.js | 2 +- js/xpconnect/tests/components/js/xpctest_bug809674.js | 2 +- js/xpconnect/tests/components/js/xpctest_interfaces.js | 2 +- js/xpconnect/tests/components/js/xpctest_params.js | 2 +- .../tests/components/js/xpctest_returncode_child.js | 2 +- js/xpconnect/tests/components/js/xpctest_utils.js | 2 +- js/xpconnect/tests/unit/test_import.js | 7 ++----- js/xpconnect/tests/unit/test_isModuleLoaded.js | 9 --------- js/xpconnect/tests/unit/test_returncode.js | 2 +- xpcom/tests/unit/data/main_process_directive_service.js | 2 +- xpcom/tests/unit/test_process_directives.js | 2 +- 13 files changed, 20 insertions(+), 27 deletions(-) diff --git a/addon-sdk/source/test/fixtures/native-addon-test/index.js b/addon-sdk/source/test/fixtures/native-addon-test/index.js index 68720d40150cf..6bfe2f2beb05f 100644 --- a/addon-sdk/source/test/fixtures/native-addon-test/index.js +++ b/addon-sdk/source/test/fixtures/native-addon-test/index.js @@ -21,7 +21,12 @@ exports.dummyModule = require('./dir/dummy'); exports.eventCore = require('sdk/event/core'); exports.promise = require('sdk/core/promise'); -exports.localJSM = require('./dir/test.jsm'); +if (module.uri.startsWith("file:")) + // We can't load the same file multiple times with different URLs, so + // skip this one. + exports.localJSM = { test: "this is a jsm" }; +else + exports.localJSM = require('./dir/test.jsm'); exports.promisejsm = require('modules/Promise.jsm').Promise; exports.require = require; diff --git a/js/xpconnect/loader/XPCOMUtils.jsm b/js/xpconnect/loader/XPCOMUtils.jsm index bb0b9fcc8fdb6..68135317b39b2 100644 --- a/js/xpconnect/loader/XPCOMUtils.jsm +++ b/js/xpconnect/loader/XPCOMUtils.jsm @@ -457,9 +457,9 @@ this.XPCOMUtils = { if (!("__URI__" in that)) throw Error("importRelative may only be used from a JSM, and its first argument "+ "must be that JSM's global object (hint: use this)"); - let uri = that.__URI__; - let i = uri.lastIndexOf("/"); - Components.utils.import(uri.substring(0, i+1) + path, scope || that); + + Cu.importGlobalProperties(["URL"]); + Components.utils.import(new URL(path, that.__URI__).href, scope || that); }, /** diff --git a/js/xpconnect/tests/components/js/xpctest_attributes.js b/js/xpconnect/tests/components/js/xpctest_attributes.js index 6576f9d558e79..c8dfa6c39417c 100644 --- a/js/xpconnect/tests/components/js/xpctest_attributes.js +++ b/js/xpconnect/tests/components/js/xpctest_attributes.js @@ -1,7 +1,7 @@ /* 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/. */ -Components.utils.import("resource:///modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); function TestObjectReadWrite() {} TestObjectReadWrite.prototype = { diff --git a/js/xpconnect/tests/components/js/xpctest_bug809674.js b/js/xpconnect/tests/components/js/xpctest_bug809674.js index 4f018c128035e..4843b14911aa4 100644 --- a/js/xpconnect/tests/components/js/xpctest_bug809674.js +++ b/js/xpconnect/tests/components/js/xpctest_bug809674.js @@ -1,7 +1,7 @@ /* 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/. */ -Components.utils.import("resource:///modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); function TestBug809674() {} TestBug809674.prototype = { diff --git a/js/xpconnect/tests/components/js/xpctest_interfaces.js b/js/xpconnect/tests/components/js/xpctest_interfaces.js index 37d2eb4071dfa..bfec85c96da08 100644 --- a/js/xpconnect/tests/components/js/xpctest_interfaces.js +++ b/js/xpconnect/tests/components/js/xpctest_interfaces.js @@ -1,7 +1,7 @@ /* 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/. */ -Components.utils.import("resource:///modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); function TestInterfaceA() {} TestInterfaceA.prototype = { diff --git a/js/xpconnect/tests/components/js/xpctest_params.js b/js/xpconnect/tests/components/js/xpctest_params.js index f06e9c2b6bc9a..cd85b33e9a8c2 100644 --- a/js/xpconnect/tests/components/js/xpctest_params.js +++ b/js/xpconnect/tests/components/js/xpctest_params.js @@ -1,7 +1,7 @@ /* 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/. */ -Components.utils.import("resource:///modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); function TestParams() { } diff --git a/js/xpconnect/tests/components/js/xpctest_returncode_child.js b/js/xpconnect/tests/components/js/xpctest_returncode_child.js index ef1e48b2a5f4f..5ef1ae6c98713 100644 --- a/js/xpconnect/tests/components/js/xpctest_returncode_child.js +++ b/js/xpconnect/tests/components/js/xpctest_returncode_child.js @@ -2,7 +2,7 @@ * 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/. */ const {interfaces: Ci, classes: Cc, utils: Cu, results: Cr} = Components; -Cu.import("resource:///modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); function TestReturnCodeChild() {} TestReturnCodeChild.prototype = { diff --git a/js/xpconnect/tests/components/js/xpctest_utils.js b/js/xpconnect/tests/components/js/xpctest_utils.js index 91d7f18396d47..6a57e52e75210 100644 --- a/js/xpconnect/tests/components/js/xpctest_utils.js +++ b/js/xpconnect/tests/components/js/xpctest_utils.js @@ -2,7 +2,7 @@ * 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/. */ -Components.utils.import("resource:///modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); function TestUtils() {} TestUtils.prototype = { diff --git a/js/xpconnect/tests/unit/test_import.js b/js/xpconnect/tests/unit/test_import.js index e5c76b2eccf80..ad558e64c783c 100644 --- a/js/xpconnect/tests/unit/test_import.js +++ b/js/xpconnect/tests/unit/test_import.js @@ -39,11 +39,8 @@ function run_test() { dump("resURI: " + resURI + "\n"); var filePath = res.resolveURI(resURI); var scope3 = {}; - Components.utils.import(filePath, scope3); - do_check_eq(typeof(scope3.XPCOMUtils), "object"); - do_check_eq(typeof(scope3.XPCOMUtils.generateNSGetFactory), "function"); - - do_check_true(scope3.XPCOMUtils == scope.XPCOMUtils); + Assert.throws(() => Components.utils.import(filePath, scope3), + /NS_ERROR_UNEXPECTED/); // make sure we throw when the second arg is bogus var didThrow = false; diff --git a/js/xpconnect/tests/unit/test_isModuleLoaded.js b/js/xpconnect/tests/unit/test_isModuleLoaded.js index 8b1f9eb3d35f1..647f5277f0e90 100644 --- a/js/xpconnect/tests/unit/test_isModuleLoaded.js +++ b/js/xpconnect/tests/unit/test_isModuleLoaded.js @@ -21,13 +21,4 @@ function run_test() { } catch (ex) {} do_check_true(!Cu.isModuleLoaded("resource://gre/modules/ISO8601DateUtils1.jsm"), "isModuleLoaded returned correct value for non-loaded module"); - - // incorrect url - try { - Cu.isModuleLoaded("resource://modules/ISO8601DateUtils1.jsm"); - do_check_true(false, - "Should have thrown while trying to load a non existing file"); - } catch (ex) { - do_check_true(true, "isModuleLoaded threw an exception while loading incorrect uri"); - } } diff --git a/js/xpconnect/tests/unit/test_returncode.js b/js/xpconnect/tests/unit/test_returncode.js index 968c269bceba5..821eae0260608 100644 --- a/js/xpconnect/tests/unit/test_returncode.js +++ b/js/xpconnect/tests/unit/test_returncode.js @@ -4,7 +4,7 @@ const {interfaces: Ci, classes: Cc, utils: Cu, manager: Cm, results: Cr} = Components; -Cu.import("resource:///modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); function getConsoleMessages() { let consoleService = Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService); diff --git a/xpcom/tests/unit/data/main_process_directive_service.js b/xpcom/tests/unit/data/main_process_directive_service.js index f4eed11c9ca59..a5f40b286869c 100644 --- a/xpcom/tests/unit/data/main_process_directive_service.js +++ b/xpcom/tests/unit/data/main_process_directive_service.js @@ -1,7 +1,7 @@ /* 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/. */ -Components.utils.import("resource:///modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); function TestProcessDirective() {} TestProcessDirective.prototype = { diff --git a/xpcom/tests/unit/test_process_directives.js b/xpcom/tests/unit/test_process_directives.js index 807246f464376..0194ce4ac71b9 100644 --- a/xpcom/tests/unit/test_process_directives.js +++ b/xpcom/tests/unit/test_process_directives.js @@ -1,7 +1,7 @@ var Ci = Components.interfaces; var Cc = Components.classes; -Components.utils.import("resource:///modules/Services.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); function run_test() { From 6041274802dc812379c133386ae205c1454a6ef3 Mon Sep 17 00:00:00 2001 From: Nicholas Nethercote Date: Mon, 17 Jul 2017 10:18:50 +1000 Subject: [PATCH 53/56] Bug 1380286 - Introduce ProfilerStackCollector. r=mstange. This allows code outside the profiler to get fully interleaved stack traces containing frames from the pseudo-stack, native stack, and JS stack. --HG-- extra : rebase_source : e21b64e86ffec83a0052947afad1793f3fd62d00 --- tools/profiler/core/ProfileBuffer.cpp | 61 +++++-- tools/profiler/core/ProfileBuffer.h | 15 +- tools/profiler/core/platform.cpp | 161 ++++++++++++++----- tools/profiler/moz.build | 1 + tools/profiler/public/GeckoProfiler.h | 42 +++++ tools/profiler/tests/gtest/GeckoProfiler.cpp | 67 ++++++++ 6 files changed, 287 insertions(+), 60 deletions(-) diff --git a/tools/profiler/core/ProfileBuffer.cpp b/tools/profiler/core/ProfileBuffer.cpp index 5a77da904347a..d0b29062afdd5 100644 --- a/tools/profiler/core/ProfileBuffer.cpp +++ b/tools/profiler/core/ProfileBuffer.cpp @@ -8,6 +8,8 @@ #include "ProfilerMarker.h" +using namespace mozilla; + ProfileBuffer::ProfileBuffer(int aEntrySize) : mEntries(mozilla::MakeUnique(aEntrySize)) , mWritePos(0) @@ -56,28 +58,55 @@ ProfileBuffer::AddThreadIdEntry(int aThreadId, LastSample* aLS) } void -ProfileBuffer::AddDynamicStringEntry(const char* aStr) +ProfileBuffer::AddStoredMarker(ProfilerMarker *aStoredMarker) { - size_t strLen = strlen(aStr) + 1; // +1 for the null terminator - for (size_t j = 0; j < strLen; ) { - // Store up to kNumChars characters in the entry. - char chars[ProfileBufferEntry::kNumChars]; - size_t len = ProfileBufferEntry::kNumChars; - if (j + len >= strLen) { - len = strLen - j; - } - memcpy(chars, &aStr[j], len); - j += ProfileBufferEntry::kNumChars; + aStoredMarker->SetGeneration(mGeneration); + mStoredMarkers.insert(aStoredMarker); +} - AddEntry(ProfileBufferEntry::DynamicStringFragment(chars)); - } +void +ProfileBuffer::CollectNativeLeafAddr(void* aAddr) +{ + AddEntry(ProfileBufferEntry::NativeLeafAddr(aAddr)); } void -ProfileBuffer::AddStoredMarker(ProfilerMarker *aStoredMarker) +ProfileBuffer::CollectJitReturnAddr(void* aAddr) { - aStoredMarker->SetGeneration(mGeneration); - mStoredMarkers.insert(aStoredMarker); + AddEntry(ProfileBufferEntry::JitReturnAddr(aAddr)); +} + +void +ProfileBuffer::CollectCodeLocation( + const char* aLabel, const char* aStr, int aLineNumber, + const Maybe& aCategory) +{ + AddEntry(ProfileBufferEntry::Label(aLabel)); + + if (aStr) { + // Store the string using one or more DynamicStringFragment entries. + size_t strLen = strlen(aStr) + 1; // +1 for the null terminator + for (size_t j = 0; j < strLen; ) { + // Store up to kNumChars characters in the entry. + char chars[ProfileBufferEntry::kNumChars]; + size_t len = ProfileBufferEntry::kNumChars; + if (j + len >= strLen) { + len = strLen - j; + } + memcpy(chars, &aStr[j], len); + j += ProfileBufferEntry::kNumChars; + + AddEntry(ProfileBufferEntry::DynamicStringFragment(chars)); + } + } + + if (aLineNumber != -1) { + AddEntry(ProfileBufferEntry::LineNumber(aLineNumber)); + } + + if (aCategory.isSome()) { + AddEntry(ProfileBufferEntry::Category(int(*aCategory))); + } } void diff --git a/tools/profiler/core/ProfileBuffer.h b/tools/profiler/core/ProfileBuffer.h index 2014472580120..d07f80b3aadbe 100644 --- a/tools/profiler/core/ProfileBuffer.h +++ b/tools/profiler/core/ProfileBuffer.h @@ -13,7 +13,7 @@ #include "mozilla/RefPtr.h" #include "mozilla/RefCounted.h" -class ProfileBuffer final +class ProfileBuffer final : public ProfilerStackCollector { public: explicit ProfileBuffer(int aEntrySize); @@ -42,9 +42,16 @@ class ProfileBuffer final // record the resulting generation and index in |aLS| if it's non-null. void AddThreadIdEntry(int aThreadId, LastSample* aLS = nullptr); - // Add to the buffer a dynamic string. It'll be spread across one or more - // DynamicStringFragment entries. - void AddDynamicStringEntry(const char* aStr); + virtual mozilla::Maybe Generation() override + { + return mozilla::Some(mGeneration); + } + + virtual void CollectNativeLeafAddr(void* aAddr) override; + virtual void CollectJitReturnAddr(void* aAddr) override; + virtual void CollectCodeLocation( + const char* aLabel, const char* aStr, int aLineNumber, + const mozilla::Maybe& aCategory) override; // Maximum size of a frameKey string that we'll handle. static const size_t kMaxFrameKeyLength = 512; diff --git a/tools/profiler/core/platform.cpp b/tools/profiler/core/platform.cpp index 53ceb1bd606a1..fabc7d6ecd7d5 100644 --- a/tools/profiler/core/platform.cpp +++ b/tools/profiler/core/platform.cpp @@ -22,7 +22,7 @@ // // - A "backtrace" sample is the simplest kind. It is done in response to an // API call (profiler_suspend_and_sample_thread()). It involves getting a -// stack trace and passing it to a callback function; it does not write to a +// stack trace via a ProfilerStackCollector; it does not write to a // ProfileBuffer. The sampling is done from off-thread, and so uses // SuspendAndSampleAndResumeThread() to get the register values. @@ -54,7 +54,9 @@ #include "nsIXULRuntime.h" #include "nsDirectoryServiceUtils.h" #include "nsDirectoryServiceDefs.h" +#include "nsJSPrincipals.h" #include "nsMemoryReporterManager.h" +#include "nsScriptSecurityManager.h" #include "nsXULAppAPI.h" #include "nsProfilerStartParams.h" #include "ProfilerParent.h" @@ -657,17 +659,31 @@ class Registers #endif }; +static bool +IsChromeJSScript(JSScript* aScript) +{ + // WARNING: this function runs within the profiler's "critical section". + + nsIScriptSecurityManager* const secman = + nsScriptSecurityManager::GetScriptSecurityManager(); + NS_ENSURE_TRUE(secman, false); + + JSPrincipals* const principals = JS_GetScriptPrincipals(aScript); + return secman->IsSystemPrincipal(nsJSPrincipals::get(principals)); +} + static void -AddPseudoEntry(PSLockRef aLock, NotNull aRacyInfo, - const js::ProfileEntry& entry, ProfileBuffer& aBuffer) +AddPseudoEntry(uint32_t aFeatures, NotNull aRacyInfo, + const js::ProfileEntry& entry, + ProfilerStackCollector& aCollector) { // WARNING: this function runs within the profiler's "critical section". + // WARNING: this function might be called while the profiler is inactive, and + // cannot rely on ActivePS. MOZ_ASSERT(entry.kind() == js::ProfileEntry::Kind::CPP_NORMAL || entry.kind() == js::ProfileEntry::Kind::JS_NORMAL); - aBuffer.AddEntry(ProfileBufferEntry::Label(entry.label())); - const char* dynamicString = entry.dynamicString(); int lineno = -1; @@ -675,18 +691,11 @@ AddPseudoEntry(PSLockRef aLock, NotNull aRacyInfo, // |dynamicString|. Perhaps it shouldn't? if (dynamicString) { - // Adjust the dynamic string as necessary. - if (ActivePS::FeaturePrivacy(aLock)) { - dynamicString = "(private)"; - } else if (strlen(dynamicString) >= ProfileBuffer::kMaxFrameKeyLength) { - dynamicString = "(too long)"; - } - - // Store the string using one or more DynamicStringFragment entries. - aBuffer.AddDynamicStringEntry(dynamicString); + bool isChromeJSEntry = false; if (entry.isJs()) { JSScript* script = entry.script(); if (script) { + isChromeJSEntry = IsChromeJSScript(script); if (!entry.pc()) { // The JIT only allows the top-most entry to have a nullptr pc. MOZ_ASSERT(&entry == &aRacyInfo->entries[aRacyInfo->stackSize() - 1]); @@ -697,6 +706,14 @@ AddPseudoEntry(PSLockRef aLock, NotNull aRacyInfo, } else { lineno = entry.line(); } + + // Adjust the dynamic string as necessary. + if (ProfilerFeature::HasPrivacy(aFeatures) && !isChromeJSEntry) { + dynamicString = "(private)"; + } else if (strlen(dynamicString) >= ProfileBuffer::kMaxFrameKeyLength) { + dynamicString = "(too long)"; + } + } else { // XXX: Bug 1010578. Don't assume a CPP entry and try to get the line for // js entries as well. @@ -705,11 +722,8 @@ AddPseudoEntry(PSLockRef aLock, NotNull aRacyInfo, } } - if (lineno != -1) { - aBuffer.AddEntry(ProfileBufferEntry::LineNumber(lineno)); - } - - aBuffer.AddEntry(ProfileBufferEntry::Category(int(entry.category()))); + aCollector.CollectCodeLocation(entry.label(), dynamicString, lineno, + Some(entry.category())); } // Setting MAX_NATIVE_FRAMES too high risks the unwinder wasting a lot of time @@ -747,12 +761,17 @@ struct AutoWalkJSStack } }; +// Merges the pseudo-stack, native stack, and JS stack, outputting the details +// to aCollector. static void -MergeStacksIntoProfile(PSLockRef aLock, bool aIsSynchronous, - const ThreadInfo& aThreadInfo, const Registers& aRegs, - const NativeStack& aNativeStack, ProfileBuffer& aBuffer) +MergeStacks(uint32_t aFeatures, bool aIsSynchronous, + const ThreadInfo& aThreadInfo, const Registers& aRegs, + const NativeStack& aNativeStack, + ProfilerStackCollector& aCollector) { // WARNING: this function runs within the profiler's "critical section". + // WARNING: this function might be called while the profiler is inactive, and + // cannot rely on ActivePS. NotNull racyInfo = aThreadInfo.RacyInfo(); js::ProfileEntry* pseudoEntries = racyInfo->entries; @@ -767,10 +786,10 @@ MergeStacksIntoProfile(PSLockRef aLock, bool aIsSynchronous, // ProfilingFrameIterator to avoid incorrectly resetting the generation of // sampled JIT entries inside the JS engine. See note below concerning 'J' // entries. - uint32_t startBufferGen; - startBufferGen = aIsSynchronous - ? UINT32_MAX - : aBuffer.mGeneration; + uint32_t startBufferGen = UINT32_MAX; + if (!aIsSynchronous && aCollector.Generation().isSome()) { + startBufferGen = *aCollector.Generation(); + } uint32_t jsCount = 0; JS::ProfilingFrameIterator::Frame jsFrames[MAX_JS_FRAMES]; @@ -882,7 +901,7 @@ MergeStacksIntoProfile(PSLockRef aLock, bool aIsSynchronous, // Pseudo-frames with the CPP_MARKER_FOR_JS kind are just annotations and // should not be recorded in the profile. if (pseudoEntry.kind() != js::ProfileEntry::Kind::CPP_MARKER_FOR_JS) { - AddPseudoEntry(aLock, racyInfo, pseudoEntry, aBuffer); + AddPseudoEntry(aFeatures, racyInfo, pseudoEntry, aCollector); } pseudoIndex++; continue; @@ -908,13 +927,11 @@ MergeStacksIntoProfile(PSLockRef aLock, bool aIsSynchronous, // with stale JIT code return addresses. if (aIsSynchronous || jsFrame.kind == JS::ProfilingFrameIterator::Frame_Wasm) { - aBuffer.AddEntry(ProfileBufferEntry::Label("")); - aBuffer.AddDynamicStringEntry(jsFrame.label); + aCollector.CollectCodeLocation("", jsFrame.label, -1, Nothing()); } else { MOZ_ASSERT(jsFrame.kind == JS::ProfilingFrameIterator::Frame_Ion || jsFrame.kind == JS::ProfilingFrameIterator::Frame_Baseline); - aBuffer.AddEntry( - ProfileBufferEntry::JitReturnAddr(jsFrames[jsIndex].returnAddress)); + aCollector.CollectJitReturnAddr(jsFrames[jsIndex].returnAddress); } jsIndex--; @@ -926,7 +943,7 @@ MergeStacksIntoProfile(PSLockRef aLock, bool aIsSynchronous, if (nativeStackAddr) { MOZ_ASSERT(nativeIndex >= 0); void* addr = (void*)aNativeStack.mPCs[nativeIndex]; - aBuffer.AddEntry(ProfileBufferEntry::NativeLeafAddr(addr)); + aCollector.CollectNativeLeafAddr(addr); } if (nativeIndex >= 0) { nativeIndex--; @@ -937,10 +954,11 @@ MergeStacksIntoProfile(PSLockRef aLock, bool aIsSynchronous, // // Do not do this for synchronous samples, which use their own // ProfileBuffers instead of the global one in CorePS. - if (!aIsSynchronous && context) { - MOZ_ASSERT(aBuffer.mGeneration >= startBufferGen); - uint32_t lapCount = aBuffer.mGeneration - startBufferGen; - JS::UpdateJSContextProfilerSampleBufferGen(context, aBuffer.mGeneration, + if (!aIsSynchronous && context && aCollector.Generation().isSome()) { + MOZ_ASSERT(*aCollector.Generation() >= startBufferGen); + uint32_t lapCount = *aCollector.Generation() - startBufferGen; + JS::UpdateJSContextProfilerSampleBufferGen(context, + *aCollector.Generation(), lapCount); } } @@ -965,6 +983,8 @@ DoNativeBacktrace(PSLockRef aLock, const ThreadInfo& aThreadInfo, const Registers& aRegs, NativeStack& aNativeStack) { // WARNING: this function runs within the profiler's "critical section". + // WARNING: this function might be called while the profiler is inactive, and + // cannot rely on ActivePS. // Start with the current function. We use 0 as the frame number here because // the FramePointerStackWalk() and MozStackWalk() calls below will use 1..N. @@ -998,6 +1018,8 @@ DoNativeBacktrace(PSLockRef aLock, const ThreadInfo& aThreadInfo, const Registers& aRegs, NativeStack& aNativeStack) { // WARNING: this function runs within the profiler's "critical section". + // WARNING: this function might be called while the profiler is inactive, and + // cannot rely on ActivePS. const mcontext_t* mcontext = &aRegs.mContext->uc_mcontext; mcontext_t savedContext; @@ -1077,6 +1099,8 @@ DoNativeBacktrace(PSLockRef aLock, const ThreadInfo& aThreadInfo, const Registers& aRegs, NativeStack& aNativeStack) { // WARNING: this function runs within the profiler's "critical section". + // WARNING: this function might be called while the profiler is inactive, and + // cannot rely on ActivePS. const mcontext_t* mc = &aRegs.mContext->uc_mcontext; @@ -1222,13 +1246,13 @@ DoSharedSample(PSLockRef aLock, bool aIsSynchronous, if (ActivePS::FeatureStackWalk(aLock)) { DoNativeBacktrace(aLock, aThreadInfo, aRegs, nativeStack); - MergeStacksIntoProfile(aLock, aIsSynchronous, aThreadInfo, aRegs, - nativeStack, aBuffer); + MergeStacks(ActivePS::Features(aLock), aIsSynchronous, aThreadInfo, aRegs, + nativeStack, aBuffer); } else #endif { - MergeStacksIntoProfile(aLock, aIsSynchronous, aThreadInfo, aRegs, - nativeStack, aBuffer); + MergeStacks(ActivePS::Features(aLock), aIsSynchronous, aThreadInfo, aRegs, + nativeStack, aBuffer); if (ActivePS::FeatureLeaf(aLock)) { aBuffer.AddEntry(ProfileBufferEntry::NativeLeafAddr((void*)aRegs.mPC)); @@ -3046,5 +3070,62 @@ profiler_suspend_and_sample_thread( } } +// NOTE: aCollector's methods will be called while the target thread is paused. +// Doing things in those methods like allocating -- which may try to claim +// locks -- is a surefire way to deadlock. +void +profiler_suspend_and_sample_thread(int aThreadId, + uint32_t aFeatures, + ProfilerStackCollector& aCollector, + bool aSampleNative /* = true */) +{ + // Lock the profiler mutex + PSAutoLock lock(gPSMutex); + + const CorePS::ThreadVector& liveThreads = CorePS::LiveThreads(lock); + for (uint32_t i = 0; i < liveThreads.size(); i++) { + ThreadInfo* info = liveThreads.at(i); + + if (info->ThreadId() == aThreadId) { + if (info->IsMainThread()) { + aCollector.SetIsMainThread(); + } + + // Allocate the space for the native stack + NativeStack nativeStack; + + // Suspend, sample, and then resume the target thread. + Sampler sampler(lock); + sampler.SuspendAndSampleAndResumeThread(lock, *info, + [&](const Registers& aRegs) { + // The target thread is now suspended. Collect a native backtrace, and + // call the callback. + bool isSynchronous = false; +#if defined(HAVE_NATIVE_UNWIND) + if (aSampleNative) { + DoNativeBacktrace(lock, *info, aRegs, nativeStack); + + MergeStacks(aFeatures, isSynchronous, *info, aRegs, nativeStack, + aCollector); + } else +#endif + { + MergeStacks(aFeatures, isSynchronous, *info, aRegs, nativeStack, + aCollector); + + if (ProfilerFeature::HasLeaf(aFeatures)) { + aCollector.CollectNativeLeafAddr((void*)aRegs.mPC); + } + } + }); + + // NOTE: Make sure to disable the sampler before it is destroyed, in case + // the profiler is running at the same time. + sampler.Disable(lock); + break; + } + } +} + // END externally visible functions //////////////////////////////////////////////////////////////////////// diff --git a/tools/profiler/moz.build b/tools/profiler/moz.build index 54c971791c968..e5a2f95006f9b 100644 --- a/tools/profiler/moz.build +++ b/tools/profiler/moz.build @@ -81,6 +81,7 @@ if CONFIG['MOZ_GECKO_PROFILER']: ] LOCAL_INCLUDES += [ + '/caps', '/docshell/base', '/ipc/chromium/src', '/mozglue/linker', diff --git a/tools/profiler/public/GeckoProfiler.h b/tools/profiler/public/GeckoProfiler.h index e42654a079d1f..bbf6fedc5649d 100644 --- a/tools/profiler/public/GeckoProfiler.h +++ b/tools/profiler/public/GeckoProfiler.h @@ -25,6 +25,7 @@ #include "mozilla/Assertions.h" #include "mozilla/Attributes.h" #include "mozilla/GuardObjects.h" +#include "mozilla/Maybe.h" #include "mozilla/Sprintf.h" #include "mozilla/ThreadLocal.h" #include "mozilla/UniquePtr.h" @@ -267,11 +268,52 @@ typedef void ProfilerStackCallback(void** aPCs, size_t aCount, bool aIsMainThrea // WARNING: The target thread is suspended during the callback. Do not try to // allocate or acquire any locks, or you could deadlock. The target thread will // have resumed by the time this function returns. +// +// XXX: this function is in the process of being replaced with the other profiler_suspend_and_sample_thread() function. PROFILER_FUNC_VOID( profiler_suspend_and_sample_thread(int aThreadId, const std::function& aCallback, bool aSampleNative = true)) +// An object of this class is passed to profiler_suspend_and_sample_thread(). +// For each stack frame, one of the Collect methods will be called. +class ProfilerStackCollector +{ +public: + // Some collectors need to worry about possibly overwriting previous + // generations of data. If that's not an issue, this can return Nothing, + // which is the default behaviour. + virtual mozilla::Maybe Generation() { return mozilla::Nothing(); } + + // This method will be called once if the thread being suspended is the main + // thread. Default behaviour is to do nothing. + virtual void SetIsMainThread() {} + + // WARNING: The target thread is suspended when the Collect methods are + // called. Do not try to allocate or acquire any locks, or you could + // deadlock. The target thread will have resumed by the time this function + // returns. + + virtual void CollectNativeLeafAddr(void* aAddr) = 0; + + virtual void CollectJitReturnAddr(void* aAddr) = 0; + + // aLabel is static and never null. aStr may be null. aLineNumber may be -1. + virtual void CollectCodeLocation( + const char* aLabel, const char* aStr, int aLineNumber, + const mozilla::Maybe& aCategory) = 0; +}; + +// This method suspends the thread identified by aThreadId, samples its +// pseudo-stack, JS stack, and (optionally) native stack, passing the collected +// frames into aCollector. aFeatures dictates which compiler features are used. +// |Privacy| and |Leaf| are the only relevant ones. +PROFILER_FUNC_VOID( + profiler_suspend_and_sample_thread(int aThreadId, + uint32_t aFeatures, + ProfilerStackCollector& aCollector, + bool aSampleNative = true)) + struct ProfilerBacktraceDestructor { #ifdef MOZ_GECKO_PROFILER diff --git a/tools/profiler/tests/gtest/GeckoProfiler.cpp b/tools/profiler/tests/gtest/GeckoProfiler.cpp index 03825c94bee7b..fa06002c8705f 100644 --- a/tools/profiler/tests/gtest/GeckoProfiler.cpp +++ b/tools/profiler/tests/gtest/GeckoProfiler.cpp @@ -664,3 +664,70 @@ TEST(GeckoProfiler, Bug1355807) profiler_stop(); } + +class GTestStackCollector final : public ProfilerStackCollector +{ +public: + GTestStackCollector() + : mSetIsMainThread(0) + , mFrames(0) + {} + + virtual void SetIsMainThread() { mSetIsMainThread++; } + + virtual void CollectNativeLeafAddr(void* aAddr) { mFrames++; } + virtual void CollectJitReturnAddr(void* aAddr) { mFrames++; } + virtual void CollectCodeLocation( + const char* aLabel, const char* aStr, int aLineNumber, + const mozilla::Maybe& aCategory) { mFrames++; } + + int mSetIsMainThread; + int mFrames; +}; + +void DoSuspendAndSample(int aTid, nsIThread* aThread) +{ + aThread->Dispatch( + NS_NewRunnableFunction( + "GeckoProfiler_SuspendAndSample_Test::TestBody", + [&]() { + uint32_t features = ProfilerFeature::Leaf; + GTestStackCollector collector; + profiler_suspend_and_sample_thread(aTid, features, collector, + /* sampleNative = */ true); + + ASSERT_TRUE(collector.mSetIsMainThread == 1); + ASSERT_TRUE(collector.mFrames > 5); // approximate; must be > 0 + }), + NS_DISPATCH_SYNC); +} + +TEST(GeckoProfiler, SuspendAndSample) +{ + nsCOMPtr thread; + nsresult rv = NS_NewNamedThread("GeckoProfGTest", getter_AddRefs(thread)); + ASSERT_TRUE(NS_SUCCEEDED(rv)); + + int tid = Thread::GetCurrentId(); + + ASSERT_TRUE(!profiler_is_active()); + + // Suspend and sample while the profiler is inactive. + DoSuspendAndSample(tid, thread); + + uint32_t features = ProfilerFeature::JS | ProfilerFeature::Threads; + const char* filters[] = { "GeckoMain", "Compositor" }; + + profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, + features, filters, MOZ_ARRAY_LENGTH(filters)); + + ASSERT_TRUE(profiler_is_active()); + + // Suspend and sample while the profiler is active. + DoSuspendAndSample(tid, thread); + + profiler_stop(); + + ASSERT_TRUE(!profiler_is_active()); +} + From 2a1feb9d190812a2054c2ad977a1d6540c6809d1 Mon Sep 17 00:00:00 2001 From: Nicholas Nethercote Date: Wed, 19 Jul 2017 17:22:05 +1000 Subject: [PATCH 54/56] Bug 1352573 (part 1) - Convert FlashThrottleAsyncMsg from a ChildAsyncCall to a CancelableRunnable. r=bsmedberg. This requires adding mPendingFlashThrottleMsgs to PluginInstanceChild. It also requires adding FlashThrottleMsg::mInstance, and a FlashThrottleMsg::Cancel() function that nulls mInstance. --HG-- extra : rebase_source : 87e5732ddf2ad57d4f3ff078ab66143797eac49f --- dom/plugins/ipc/PluginInstanceChild.cpp | 41 +++++++++++++++++-------- dom/plugins/ipc/PluginInstanceChild.h | 17 +++++----- 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/dom/plugins/ipc/PluginInstanceChild.cpp b/dom/plugins/ipc/PluginInstanceChild.cpp index da9406aa7974c..c1cd85dbc9853 100644 --- a/dom/plugins/ipc/PluginInstanceChild.cpp +++ b/dom/plugins/ipc/PluginInstanceChild.cpp @@ -2339,7 +2339,7 @@ PluginInstanceChild::SetupFlashMsgThrottle() } WNDPROC -PluginInstanceChild::FlashThrottleAsyncMsg::GetProc() +PluginInstanceChild::FlashThrottleMsg::GetProc() { if (mInstance) { return mWindowed ? mInstance->mPluginWndProc : @@ -2349,13 +2349,17 @@ PluginInstanceChild::FlashThrottleAsyncMsg::GetProc() } NS_IMETHODIMP -PluginInstanceChild::FlashThrottleAsyncMsg::Run() +PluginInstanceChild::FlashThrottleMsg::Run() { - RemoveFromAsyncList(); + if (!mInstance) { + return NS_OK; + } + + mInstance->mPendingFlashThrottleMsgs.RemoveElement(this); // GetProc() checks mInstance, and pulls the procedure from // PluginInstanceChild. We don't transport sub-class procedure - // ptrs around in FlashThrottleAsyncMsg msgs. + // ptrs around in FlashThrottleMsg msgs. if (!GetProc()) return NS_OK; @@ -2364,6 +2368,14 @@ PluginInstanceChild::FlashThrottleAsyncMsg::Run() return NS_OK; } +nsresult +PluginInstanceChild::FlashThrottleMsg::Cancel() +{ + MOZ_ASSERT(mInstance); + mInstance = nullptr; + return NS_OK; +} + void PluginInstanceChild::FlashThrottleMessage(HWND aWnd, UINT aMsg, @@ -2371,15 +2383,13 @@ PluginInstanceChild::FlashThrottleMessage(HWND aWnd, LPARAM aLParam, bool isWindowed) { - // We reuse ChildAsyncCall so we get the cancelation work - // that's done in Destroy. - RefPtr task = - new FlashThrottleAsyncMsg(this, aWnd, aMsg, aWParam, - aLParam, isWindowed); - { - MutexAutoLock lock(mAsyncCallMutex); - mPendingAsyncCalls.AppendElement(task); - } + // We save a reference to the FlashThrottleMsg so we can cancel it in + // Destroy if it's still alive. + RefPtr task = + new FlashThrottleMsg(this, aWnd, aMsg, aWParam, aLParam, isWindowed); + + mPendingFlashThrottleMsgs.AppendElement(task); + MessageLoop::current()->PostDelayedTask(task.forget(), kFlashWMUSERMessageThrottleDelayMs); } @@ -4262,6 +4272,11 @@ PluginInstanceChild::Destroy() DestroyWinlessPopupSurrogate(); UnhookWinlessFlashThrottle(); DestroyPluginWindow(); + + for (uint32_t i = 0; i < mPendingFlashThrottleMsgs.Length(); ++i) { + mPendingFlashThrottleMsgs[i]->Cancel(); + } + mPendingFlashThrottleMsgs.Clear(); #endif // Pending async calls are discarded, not delivered. This matches the diff --git a/dom/plugins/ipc/PluginInstanceChild.h b/dom/plugins/ipc/PluginInstanceChild.h index abd056fef388c..21ac56eb88cf5 100644 --- a/dom/plugins/ipc/PluginInstanceChild.h +++ b/dom/plugins/ipc/PluginInstanceChild.h @@ -348,15 +348,13 @@ class PluginInstanceChild : public PPluginInstanceChild static BOOL WINAPI ImmNotifyIME(HIMC aIMC, DWORD aAction, DWORD aIndex, DWORD aValue); - class FlashThrottleAsyncMsg : public ChildAsyncCall + class FlashThrottleMsg : public CancelableRunnable { public: - FlashThrottleAsyncMsg(); - FlashThrottleAsyncMsg(PluginInstanceChild* aInst, - HWND aWnd, UINT aMsg, - WPARAM aWParam, LPARAM aLParam, - bool isWindowed) - : ChildAsyncCall(aInst, nullptr, nullptr), + FlashThrottleMsg(PluginInstanceChild* aInstance, HWND aWnd, UINT aMsg, + WPARAM aWParam, LPARAM aLParam, bool isWindowed) + : CancelableRunnable("FlashThrottleMsg"), + mInstance(aInstance), mWnd(aWnd), mMsg(aMsg), mWParam(aWParam), @@ -365,6 +363,7 @@ class PluginInstanceChild : public PPluginInstanceChild {} NS_IMETHOD Run() override; + nsresult Cancel() override; WNDPROC GetProc(); HWND GetWnd() { return mWnd; } @@ -373,6 +372,7 @@ class PluginInstanceChild : public PPluginInstanceChild LPARAM GetLParam() { return mLParam; } private: + PluginInstanceChild* mInstance; HWND mWnd; UINT mMsg; WPARAM mWParam; @@ -450,6 +450,9 @@ class PluginInstanceChild : public PPluginInstanceChild friend class ChildAsyncCall; +#if defined(OS_WIN) + nsTArray mPendingFlashThrottleMsgs; +#endif Mutex mAsyncCallMutex; nsTArray mPendingAsyncCalls; nsTArray > mTimers; From 8408f943c1694c557db559029a7dd57c81660b58 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Mon, 24 Jul 2017 22:10:18 -0700 Subject: [PATCH 55/56] Bug 1383215: Follow-up: Fix straggler xpcshell test. MozReview-Commit-ID: IhD2Kqtxtwu --- xpcom/tests/unit/data/child_process_directive_service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xpcom/tests/unit/data/child_process_directive_service.js b/xpcom/tests/unit/data/child_process_directive_service.js index 9bc1c3206fb47..aa3f03df1deec 100644 --- a/xpcom/tests/unit/data/child_process_directive_service.js +++ b/xpcom/tests/unit/data/child_process_directive_service.js @@ -1,7 +1,7 @@ /* 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/. */ -Components.utils.import("resource:///modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); function TestProcessDirective() {} TestProcessDirective.prototype = { From 965777ef3a0855d1f221dc3f7ee1a0e43f70f76f Mon Sep 17 00:00:00 2001 From: "Carsten \"Tomcat\" Book" Date: Tue, 25 Jul 2017 08:44:13 +0200 Subject: [PATCH 56/56] Backed out changeset 3923ce220df3 (bug 1380286) for hazard failures --- tools/profiler/core/ProfileBuffer.cpp | 61 ++----- tools/profiler/core/ProfileBuffer.h | 15 +- tools/profiler/core/platform.cpp | 161 +++++-------------- tools/profiler/moz.build | 1 - tools/profiler/public/GeckoProfiler.h | 42 ----- tools/profiler/tests/gtest/GeckoProfiler.cpp | 67 -------- 6 files changed, 60 insertions(+), 287 deletions(-) diff --git a/tools/profiler/core/ProfileBuffer.cpp b/tools/profiler/core/ProfileBuffer.cpp index d0b29062afdd5..5a77da904347a 100644 --- a/tools/profiler/core/ProfileBuffer.cpp +++ b/tools/profiler/core/ProfileBuffer.cpp @@ -8,8 +8,6 @@ #include "ProfilerMarker.h" -using namespace mozilla; - ProfileBuffer::ProfileBuffer(int aEntrySize) : mEntries(mozilla::MakeUnique(aEntrySize)) , mWritePos(0) @@ -58,55 +56,28 @@ ProfileBuffer::AddThreadIdEntry(int aThreadId, LastSample* aLS) } void -ProfileBuffer::AddStoredMarker(ProfilerMarker *aStoredMarker) +ProfileBuffer::AddDynamicStringEntry(const char* aStr) { - aStoredMarker->SetGeneration(mGeneration); - mStoredMarkers.insert(aStoredMarker); -} - -void -ProfileBuffer::CollectNativeLeafAddr(void* aAddr) -{ - AddEntry(ProfileBufferEntry::NativeLeafAddr(aAddr)); -} + size_t strLen = strlen(aStr) + 1; // +1 for the null terminator + for (size_t j = 0; j < strLen; ) { + // Store up to kNumChars characters in the entry. + char chars[ProfileBufferEntry::kNumChars]; + size_t len = ProfileBufferEntry::kNumChars; + if (j + len >= strLen) { + len = strLen - j; + } + memcpy(chars, &aStr[j], len); + j += ProfileBufferEntry::kNumChars; -void -ProfileBuffer::CollectJitReturnAddr(void* aAddr) -{ - AddEntry(ProfileBufferEntry::JitReturnAddr(aAddr)); + AddEntry(ProfileBufferEntry::DynamicStringFragment(chars)); + } } void -ProfileBuffer::CollectCodeLocation( - const char* aLabel, const char* aStr, int aLineNumber, - const Maybe& aCategory) +ProfileBuffer::AddStoredMarker(ProfilerMarker *aStoredMarker) { - AddEntry(ProfileBufferEntry::Label(aLabel)); - - if (aStr) { - // Store the string using one or more DynamicStringFragment entries. - size_t strLen = strlen(aStr) + 1; // +1 for the null terminator - for (size_t j = 0; j < strLen; ) { - // Store up to kNumChars characters in the entry. - char chars[ProfileBufferEntry::kNumChars]; - size_t len = ProfileBufferEntry::kNumChars; - if (j + len >= strLen) { - len = strLen - j; - } - memcpy(chars, &aStr[j], len); - j += ProfileBufferEntry::kNumChars; - - AddEntry(ProfileBufferEntry::DynamicStringFragment(chars)); - } - } - - if (aLineNumber != -1) { - AddEntry(ProfileBufferEntry::LineNumber(aLineNumber)); - } - - if (aCategory.isSome()) { - AddEntry(ProfileBufferEntry::Category(int(*aCategory))); - } + aStoredMarker->SetGeneration(mGeneration); + mStoredMarkers.insert(aStoredMarker); } void diff --git a/tools/profiler/core/ProfileBuffer.h b/tools/profiler/core/ProfileBuffer.h index d07f80b3aadbe..2014472580120 100644 --- a/tools/profiler/core/ProfileBuffer.h +++ b/tools/profiler/core/ProfileBuffer.h @@ -13,7 +13,7 @@ #include "mozilla/RefPtr.h" #include "mozilla/RefCounted.h" -class ProfileBuffer final : public ProfilerStackCollector +class ProfileBuffer final { public: explicit ProfileBuffer(int aEntrySize); @@ -42,16 +42,9 @@ class ProfileBuffer final : public ProfilerStackCollector // record the resulting generation and index in |aLS| if it's non-null. void AddThreadIdEntry(int aThreadId, LastSample* aLS = nullptr); - virtual mozilla::Maybe Generation() override - { - return mozilla::Some(mGeneration); - } - - virtual void CollectNativeLeafAddr(void* aAddr) override; - virtual void CollectJitReturnAddr(void* aAddr) override; - virtual void CollectCodeLocation( - const char* aLabel, const char* aStr, int aLineNumber, - const mozilla::Maybe& aCategory) override; + // Add to the buffer a dynamic string. It'll be spread across one or more + // DynamicStringFragment entries. + void AddDynamicStringEntry(const char* aStr); // Maximum size of a frameKey string that we'll handle. static const size_t kMaxFrameKeyLength = 512; diff --git a/tools/profiler/core/platform.cpp b/tools/profiler/core/platform.cpp index fabc7d6ecd7d5..53ceb1bd606a1 100644 --- a/tools/profiler/core/platform.cpp +++ b/tools/profiler/core/platform.cpp @@ -22,7 +22,7 @@ // // - A "backtrace" sample is the simplest kind. It is done in response to an // API call (profiler_suspend_and_sample_thread()). It involves getting a -// stack trace via a ProfilerStackCollector; it does not write to a +// stack trace and passing it to a callback function; it does not write to a // ProfileBuffer. The sampling is done from off-thread, and so uses // SuspendAndSampleAndResumeThread() to get the register values. @@ -54,9 +54,7 @@ #include "nsIXULRuntime.h" #include "nsDirectoryServiceUtils.h" #include "nsDirectoryServiceDefs.h" -#include "nsJSPrincipals.h" #include "nsMemoryReporterManager.h" -#include "nsScriptSecurityManager.h" #include "nsXULAppAPI.h" #include "nsProfilerStartParams.h" #include "ProfilerParent.h" @@ -659,31 +657,17 @@ class Registers #endif }; -static bool -IsChromeJSScript(JSScript* aScript) -{ - // WARNING: this function runs within the profiler's "critical section". - - nsIScriptSecurityManager* const secman = - nsScriptSecurityManager::GetScriptSecurityManager(); - NS_ENSURE_TRUE(secman, false); - - JSPrincipals* const principals = JS_GetScriptPrincipals(aScript); - return secman->IsSystemPrincipal(nsJSPrincipals::get(principals)); -} - static void -AddPseudoEntry(uint32_t aFeatures, NotNull aRacyInfo, - const js::ProfileEntry& entry, - ProfilerStackCollector& aCollector) +AddPseudoEntry(PSLockRef aLock, NotNull aRacyInfo, + const js::ProfileEntry& entry, ProfileBuffer& aBuffer) { // WARNING: this function runs within the profiler's "critical section". - // WARNING: this function might be called while the profiler is inactive, and - // cannot rely on ActivePS. MOZ_ASSERT(entry.kind() == js::ProfileEntry::Kind::CPP_NORMAL || entry.kind() == js::ProfileEntry::Kind::JS_NORMAL); + aBuffer.AddEntry(ProfileBufferEntry::Label(entry.label())); + const char* dynamicString = entry.dynamicString(); int lineno = -1; @@ -691,11 +675,18 @@ AddPseudoEntry(uint32_t aFeatures, NotNull aRacyInfo, // |dynamicString|. Perhaps it shouldn't? if (dynamicString) { - bool isChromeJSEntry = false; + // Adjust the dynamic string as necessary. + if (ActivePS::FeaturePrivacy(aLock)) { + dynamicString = "(private)"; + } else if (strlen(dynamicString) >= ProfileBuffer::kMaxFrameKeyLength) { + dynamicString = "(too long)"; + } + + // Store the string using one or more DynamicStringFragment entries. + aBuffer.AddDynamicStringEntry(dynamicString); if (entry.isJs()) { JSScript* script = entry.script(); if (script) { - isChromeJSEntry = IsChromeJSScript(script); if (!entry.pc()) { // The JIT only allows the top-most entry to have a nullptr pc. MOZ_ASSERT(&entry == &aRacyInfo->entries[aRacyInfo->stackSize() - 1]); @@ -706,14 +697,6 @@ AddPseudoEntry(uint32_t aFeatures, NotNull aRacyInfo, } else { lineno = entry.line(); } - - // Adjust the dynamic string as necessary. - if (ProfilerFeature::HasPrivacy(aFeatures) && !isChromeJSEntry) { - dynamicString = "(private)"; - } else if (strlen(dynamicString) >= ProfileBuffer::kMaxFrameKeyLength) { - dynamicString = "(too long)"; - } - } else { // XXX: Bug 1010578. Don't assume a CPP entry and try to get the line for // js entries as well. @@ -722,8 +705,11 @@ AddPseudoEntry(uint32_t aFeatures, NotNull aRacyInfo, } } - aCollector.CollectCodeLocation(entry.label(), dynamicString, lineno, - Some(entry.category())); + if (lineno != -1) { + aBuffer.AddEntry(ProfileBufferEntry::LineNumber(lineno)); + } + + aBuffer.AddEntry(ProfileBufferEntry::Category(int(entry.category()))); } // Setting MAX_NATIVE_FRAMES too high risks the unwinder wasting a lot of time @@ -761,17 +747,12 @@ struct AutoWalkJSStack } }; -// Merges the pseudo-stack, native stack, and JS stack, outputting the details -// to aCollector. static void -MergeStacks(uint32_t aFeatures, bool aIsSynchronous, - const ThreadInfo& aThreadInfo, const Registers& aRegs, - const NativeStack& aNativeStack, - ProfilerStackCollector& aCollector) +MergeStacksIntoProfile(PSLockRef aLock, bool aIsSynchronous, + const ThreadInfo& aThreadInfo, const Registers& aRegs, + const NativeStack& aNativeStack, ProfileBuffer& aBuffer) { // WARNING: this function runs within the profiler's "critical section". - // WARNING: this function might be called while the profiler is inactive, and - // cannot rely on ActivePS. NotNull racyInfo = aThreadInfo.RacyInfo(); js::ProfileEntry* pseudoEntries = racyInfo->entries; @@ -786,10 +767,10 @@ MergeStacks(uint32_t aFeatures, bool aIsSynchronous, // ProfilingFrameIterator to avoid incorrectly resetting the generation of // sampled JIT entries inside the JS engine. See note below concerning 'J' // entries. - uint32_t startBufferGen = UINT32_MAX; - if (!aIsSynchronous && aCollector.Generation().isSome()) { - startBufferGen = *aCollector.Generation(); - } + uint32_t startBufferGen; + startBufferGen = aIsSynchronous + ? UINT32_MAX + : aBuffer.mGeneration; uint32_t jsCount = 0; JS::ProfilingFrameIterator::Frame jsFrames[MAX_JS_FRAMES]; @@ -901,7 +882,7 @@ MergeStacks(uint32_t aFeatures, bool aIsSynchronous, // Pseudo-frames with the CPP_MARKER_FOR_JS kind are just annotations and // should not be recorded in the profile. if (pseudoEntry.kind() != js::ProfileEntry::Kind::CPP_MARKER_FOR_JS) { - AddPseudoEntry(aFeatures, racyInfo, pseudoEntry, aCollector); + AddPseudoEntry(aLock, racyInfo, pseudoEntry, aBuffer); } pseudoIndex++; continue; @@ -927,11 +908,13 @@ MergeStacks(uint32_t aFeatures, bool aIsSynchronous, // with stale JIT code return addresses. if (aIsSynchronous || jsFrame.kind == JS::ProfilingFrameIterator::Frame_Wasm) { - aCollector.CollectCodeLocation("", jsFrame.label, -1, Nothing()); + aBuffer.AddEntry(ProfileBufferEntry::Label("")); + aBuffer.AddDynamicStringEntry(jsFrame.label); } else { MOZ_ASSERT(jsFrame.kind == JS::ProfilingFrameIterator::Frame_Ion || jsFrame.kind == JS::ProfilingFrameIterator::Frame_Baseline); - aCollector.CollectJitReturnAddr(jsFrames[jsIndex].returnAddress); + aBuffer.AddEntry( + ProfileBufferEntry::JitReturnAddr(jsFrames[jsIndex].returnAddress)); } jsIndex--; @@ -943,7 +926,7 @@ MergeStacks(uint32_t aFeatures, bool aIsSynchronous, if (nativeStackAddr) { MOZ_ASSERT(nativeIndex >= 0); void* addr = (void*)aNativeStack.mPCs[nativeIndex]; - aCollector.CollectNativeLeafAddr(addr); + aBuffer.AddEntry(ProfileBufferEntry::NativeLeafAddr(addr)); } if (nativeIndex >= 0) { nativeIndex--; @@ -954,11 +937,10 @@ MergeStacks(uint32_t aFeatures, bool aIsSynchronous, // // Do not do this for synchronous samples, which use their own // ProfileBuffers instead of the global one in CorePS. - if (!aIsSynchronous && context && aCollector.Generation().isSome()) { - MOZ_ASSERT(*aCollector.Generation() >= startBufferGen); - uint32_t lapCount = *aCollector.Generation() - startBufferGen; - JS::UpdateJSContextProfilerSampleBufferGen(context, - *aCollector.Generation(), + if (!aIsSynchronous && context) { + MOZ_ASSERT(aBuffer.mGeneration >= startBufferGen); + uint32_t lapCount = aBuffer.mGeneration - startBufferGen; + JS::UpdateJSContextProfilerSampleBufferGen(context, aBuffer.mGeneration, lapCount); } } @@ -983,8 +965,6 @@ DoNativeBacktrace(PSLockRef aLock, const ThreadInfo& aThreadInfo, const Registers& aRegs, NativeStack& aNativeStack) { // WARNING: this function runs within the profiler's "critical section". - // WARNING: this function might be called while the profiler is inactive, and - // cannot rely on ActivePS. // Start with the current function. We use 0 as the frame number here because // the FramePointerStackWalk() and MozStackWalk() calls below will use 1..N. @@ -1018,8 +998,6 @@ DoNativeBacktrace(PSLockRef aLock, const ThreadInfo& aThreadInfo, const Registers& aRegs, NativeStack& aNativeStack) { // WARNING: this function runs within the profiler's "critical section". - // WARNING: this function might be called while the profiler is inactive, and - // cannot rely on ActivePS. const mcontext_t* mcontext = &aRegs.mContext->uc_mcontext; mcontext_t savedContext; @@ -1099,8 +1077,6 @@ DoNativeBacktrace(PSLockRef aLock, const ThreadInfo& aThreadInfo, const Registers& aRegs, NativeStack& aNativeStack) { // WARNING: this function runs within the profiler's "critical section". - // WARNING: this function might be called while the profiler is inactive, and - // cannot rely on ActivePS. const mcontext_t* mc = &aRegs.mContext->uc_mcontext; @@ -1246,13 +1222,13 @@ DoSharedSample(PSLockRef aLock, bool aIsSynchronous, if (ActivePS::FeatureStackWalk(aLock)) { DoNativeBacktrace(aLock, aThreadInfo, aRegs, nativeStack); - MergeStacks(ActivePS::Features(aLock), aIsSynchronous, aThreadInfo, aRegs, - nativeStack, aBuffer); + MergeStacksIntoProfile(aLock, aIsSynchronous, aThreadInfo, aRegs, + nativeStack, aBuffer); } else #endif { - MergeStacks(ActivePS::Features(aLock), aIsSynchronous, aThreadInfo, aRegs, - nativeStack, aBuffer); + MergeStacksIntoProfile(aLock, aIsSynchronous, aThreadInfo, aRegs, + nativeStack, aBuffer); if (ActivePS::FeatureLeaf(aLock)) { aBuffer.AddEntry(ProfileBufferEntry::NativeLeafAddr((void*)aRegs.mPC)); @@ -3070,62 +3046,5 @@ profiler_suspend_and_sample_thread( } } -// NOTE: aCollector's methods will be called while the target thread is paused. -// Doing things in those methods like allocating -- which may try to claim -// locks -- is a surefire way to deadlock. -void -profiler_suspend_and_sample_thread(int aThreadId, - uint32_t aFeatures, - ProfilerStackCollector& aCollector, - bool aSampleNative /* = true */) -{ - // Lock the profiler mutex - PSAutoLock lock(gPSMutex); - - const CorePS::ThreadVector& liveThreads = CorePS::LiveThreads(lock); - for (uint32_t i = 0; i < liveThreads.size(); i++) { - ThreadInfo* info = liveThreads.at(i); - - if (info->ThreadId() == aThreadId) { - if (info->IsMainThread()) { - aCollector.SetIsMainThread(); - } - - // Allocate the space for the native stack - NativeStack nativeStack; - - // Suspend, sample, and then resume the target thread. - Sampler sampler(lock); - sampler.SuspendAndSampleAndResumeThread(lock, *info, - [&](const Registers& aRegs) { - // The target thread is now suspended. Collect a native backtrace, and - // call the callback. - bool isSynchronous = false; -#if defined(HAVE_NATIVE_UNWIND) - if (aSampleNative) { - DoNativeBacktrace(lock, *info, aRegs, nativeStack); - - MergeStacks(aFeatures, isSynchronous, *info, aRegs, nativeStack, - aCollector); - } else -#endif - { - MergeStacks(aFeatures, isSynchronous, *info, aRegs, nativeStack, - aCollector); - - if (ProfilerFeature::HasLeaf(aFeatures)) { - aCollector.CollectNativeLeafAddr((void*)aRegs.mPC); - } - } - }); - - // NOTE: Make sure to disable the sampler before it is destroyed, in case - // the profiler is running at the same time. - sampler.Disable(lock); - break; - } - } -} - // END externally visible functions //////////////////////////////////////////////////////////////////////// diff --git a/tools/profiler/moz.build b/tools/profiler/moz.build index e5a2f95006f9b..54c971791c968 100644 --- a/tools/profiler/moz.build +++ b/tools/profiler/moz.build @@ -81,7 +81,6 @@ if CONFIG['MOZ_GECKO_PROFILER']: ] LOCAL_INCLUDES += [ - '/caps', '/docshell/base', '/ipc/chromium/src', '/mozglue/linker', diff --git a/tools/profiler/public/GeckoProfiler.h b/tools/profiler/public/GeckoProfiler.h index bbf6fedc5649d..e42654a079d1f 100644 --- a/tools/profiler/public/GeckoProfiler.h +++ b/tools/profiler/public/GeckoProfiler.h @@ -25,7 +25,6 @@ #include "mozilla/Assertions.h" #include "mozilla/Attributes.h" #include "mozilla/GuardObjects.h" -#include "mozilla/Maybe.h" #include "mozilla/Sprintf.h" #include "mozilla/ThreadLocal.h" #include "mozilla/UniquePtr.h" @@ -268,52 +267,11 @@ typedef void ProfilerStackCallback(void** aPCs, size_t aCount, bool aIsMainThrea // WARNING: The target thread is suspended during the callback. Do not try to // allocate or acquire any locks, or you could deadlock. The target thread will // have resumed by the time this function returns. -// -// XXX: this function is in the process of being replaced with the other profiler_suspend_and_sample_thread() function. PROFILER_FUNC_VOID( profiler_suspend_and_sample_thread(int aThreadId, const std::function& aCallback, bool aSampleNative = true)) -// An object of this class is passed to profiler_suspend_and_sample_thread(). -// For each stack frame, one of the Collect methods will be called. -class ProfilerStackCollector -{ -public: - // Some collectors need to worry about possibly overwriting previous - // generations of data. If that's not an issue, this can return Nothing, - // which is the default behaviour. - virtual mozilla::Maybe Generation() { return mozilla::Nothing(); } - - // This method will be called once if the thread being suspended is the main - // thread. Default behaviour is to do nothing. - virtual void SetIsMainThread() {} - - // WARNING: The target thread is suspended when the Collect methods are - // called. Do not try to allocate or acquire any locks, or you could - // deadlock. The target thread will have resumed by the time this function - // returns. - - virtual void CollectNativeLeafAddr(void* aAddr) = 0; - - virtual void CollectJitReturnAddr(void* aAddr) = 0; - - // aLabel is static and never null. aStr may be null. aLineNumber may be -1. - virtual void CollectCodeLocation( - const char* aLabel, const char* aStr, int aLineNumber, - const mozilla::Maybe& aCategory) = 0; -}; - -// This method suspends the thread identified by aThreadId, samples its -// pseudo-stack, JS stack, and (optionally) native stack, passing the collected -// frames into aCollector. aFeatures dictates which compiler features are used. -// |Privacy| and |Leaf| are the only relevant ones. -PROFILER_FUNC_VOID( - profiler_suspend_and_sample_thread(int aThreadId, - uint32_t aFeatures, - ProfilerStackCollector& aCollector, - bool aSampleNative = true)) - struct ProfilerBacktraceDestructor { #ifdef MOZ_GECKO_PROFILER diff --git a/tools/profiler/tests/gtest/GeckoProfiler.cpp b/tools/profiler/tests/gtest/GeckoProfiler.cpp index fa06002c8705f..03825c94bee7b 100644 --- a/tools/profiler/tests/gtest/GeckoProfiler.cpp +++ b/tools/profiler/tests/gtest/GeckoProfiler.cpp @@ -664,70 +664,3 @@ TEST(GeckoProfiler, Bug1355807) profiler_stop(); } - -class GTestStackCollector final : public ProfilerStackCollector -{ -public: - GTestStackCollector() - : mSetIsMainThread(0) - , mFrames(0) - {} - - virtual void SetIsMainThread() { mSetIsMainThread++; } - - virtual void CollectNativeLeafAddr(void* aAddr) { mFrames++; } - virtual void CollectJitReturnAddr(void* aAddr) { mFrames++; } - virtual void CollectCodeLocation( - const char* aLabel, const char* aStr, int aLineNumber, - const mozilla::Maybe& aCategory) { mFrames++; } - - int mSetIsMainThread; - int mFrames; -}; - -void DoSuspendAndSample(int aTid, nsIThread* aThread) -{ - aThread->Dispatch( - NS_NewRunnableFunction( - "GeckoProfiler_SuspendAndSample_Test::TestBody", - [&]() { - uint32_t features = ProfilerFeature::Leaf; - GTestStackCollector collector; - profiler_suspend_and_sample_thread(aTid, features, collector, - /* sampleNative = */ true); - - ASSERT_TRUE(collector.mSetIsMainThread == 1); - ASSERT_TRUE(collector.mFrames > 5); // approximate; must be > 0 - }), - NS_DISPATCH_SYNC); -} - -TEST(GeckoProfiler, SuspendAndSample) -{ - nsCOMPtr thread; - nsresult rv = NS_NewNamedThread("GeckoProfGTest", getter_AddRefs(thread)); - ASSERT_TRUE(NS_SUCCEEDED(rv)); - - int tid = Thread::GetCurrentId(); - - ASSERT_TRUE(!profiler_is_active()); - - // Suspend and sample while the profiler is inactive. - DoSuspendAndSample(tid, thread); - - uint32_t features = ProfilerFeature::JS | ProfilerFeature::Threads; - const char* filters[] = { "GeckoMain", "Compositor" }; - - profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, - features, filters, MOZ_ARRAY_LENGTH(filters)); - - ASSERT_TRUE(profiler_is_active()); - - // Suspend and sample while the profiler is active. - DoSuspendAndSample(tid, thread); - - profiler_stop(); - - ASSERT_TRUE(!profiler_is_active()); -} -