author | Carsten "Tomcat" Book <cbook@mozilla.com> |
Tue, 25 Jul 2017 14:27:17 +0200 | |
changeset 419502 | 07484bfdb96bc7297c404e377eea93f1d8ca4442 |
parent 419444 | 80394cbcae0f02da8eb801edd92e56f974e5db4f (current diff) |
parent 419501 | 18b80915d07365596301f88d6a9b4bbb65eb5a93 (diff) |
child 419503 | ac69b48a3503696c7f26ae70526970f7bde00703 |
child 419540 | 8a193ce80a59d7a2062783efdf215c4c36fd8d6f |
child 419580 | 4b4a6f82e3e8e9b4ecee284666eeb397f31aa8fc |
push id | 7566 |
push user | mtabara@mozilla.com |
push date | Wed, 02 Aug 2017 08:25:16 +0000 |
treeherder | mozilla-beta@86913f512c3c [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | merge |
milestone | 56.0a1 |
first release with | nightly linux32
07484bfdb96b
/
56.0a1
/
20170725144053
/
files
nightly linux64
07484bfdb96b
/
56.0a1
/
20170725144053
/
files
nightly mac
07484bfdb96b
/
56.0a1
/
20170725144001
/
files
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
releases | nightly linux32
56.0a1
/
20170725144053
/
pushlog to previous
nightly linux64
56.0a1
/
20170725144053
/
pushlog to previous
nightly mac
56.0a1
/
20170725144001
/
pushlog to previous
|
--- a/addon-sdk/source/test/fixtures/native-addon-test/index.js +++ b/addon-sdk/source/test/fixtures/native-addon-test/index.js @@ -16,17 +16,22 @@ exports.customMainModule = require('test exports.customMainModuleRelative = require('test-custom-main-relative'); exports.defaultMain = require('test-default-main'); exports.testJSON = require('./dir/c'); 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; var math = require('test-math'); exports.areModulesCached = (math === exports.math); // Added noise to test AST walker function square (x) {
--- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1100,30 +1100,32 @@ pref("security.sandbox.content.level", 1 #endif #endif #if defined(XP_LINUX) && defined(MOZ_SANDBOX) && defined(MOZ_CONTENT_SANDBOX) // This pref is introduced as part of bug 742434, the naming is inspired from // 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 // to whitelist more system calls. // // So the purpose of this setting is to allow nightly users to disable the // sandbox while we fix their problems. This way, they won't have to wait for // another nightly release which disables seccomp-bpf again. // // 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 #if defined(XP_MACOSX) || defined(XP_WIN) #if defined(MOZ_SANDBOX) && defined(MOZ_CONTENT_SANDBOX) // ID (a UUID when set by gecko) that is used to form the name of a // sandbox-writable temporary directory to be used by content processes // when a temporary writable file is required in a level 1 sandbox.
--- a/browser/base/content/test/plugins/browser.ini +++ b/browser/base/content/test/plugins/browser.ini @@ -17,17 +17,16 @@ support-files = plugin_both2.html plugin_bug744745.html plugin_bug749455.html plugin_bug787619.html plugin_bug797677.html plugin_bug820497.html plugin_clickToPlayAllow.html plugin_clickToPlayDeny.html - plugin_data_url.html plugin_favorfallback.html plugin_hidden_to_visible.html plugin_iframe.html plugin_outsideScrollArea.html plugin_overlayed.html plugin_positioned.html plugin_simple_blank.swf plugin_small.html @@ -53,18 +52,16 @@ tags = blocklist [browser_clearplugindata.js] tags = blocklist [browser_CTP_context_menu.js] skip-if = toolkit == "gtk2" || toolkit == "gtk3" # fails intermittently on Linux (bug 909342) 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] [browser_CTP_hide_overlay.js] tags = blocklist [browser_CTP_iframe.js] tags = blocklist [browser_CTP_multi_allow.js]
deleted file mode 100644 --- 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!<embed id='test' style='width:200px; height:200px' type='application/x-test'/>"); - - // 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"); -}); -*/
deleted file mode 100644 --- a/browser/base/content/test/plugins/plugin_data_url.html +++ /dev/null @@ -1,11 +0,0 @@ -<html> -<body> - <a id="data-link-1" href='data:text/html,<embed id="test" style="width: 200px; height: 200px" type="application/x-test"/>'> - data: with one plugin - </a><br /> - <a id="data-link-2" href='data:text/html,<embed id="test1" style="width: 200px; height: 200px" type="application/x-test"/><embed id="test2" style="width: 200px; height: 200px" type="application/x-second-test"/>'> - data: with two plugins - </a><br /> - <object id="test" style="width: 200px; height: 200px" type="application/x-test"></object> -</body> -</html>
--- a/browser/base/content/urlbarBindings.xml +++ b/browser/base/content/urlbarBindings.xml @@ -1688,23 +1688,25 @@ file, You can obtain one at http://mozil <method name="enableOneOffSearches"> <parameter name="enable"/> <body><![CDATA[ this._oneOffSearchesEnabled = enable; if (enable) { this.oneOffSearchButtons.telemetryOrigin = "urlbar"; this.oneOffSearchButtons.style.display = "-moz-box"; + // Set .textbox first, since the popup setter will cause + // a _rebuild call that uses it. + this.oneOffSearchButtons.textbox = this.input; this.oneOffSearchButtons.popup = this; - this.oneOffSearchButtons.textbox = this.input; } else { this.oneOffSearchButtons.telemetryOrigin = null; this.oneOffSearchButtons.style.display = "none"; + this.oneOffSearchButtons.textbox = null; this.oneOffSearchButtons.popup = null; - this.oneOffSearchButtons.textbox = null; } ]]></body> </method> <method name="openSearchSuggestionsNotificationLearnMoreURL"> <body><![CDATA[ let url = Services.urlFormatter.formatURL( Services.prefs.getCharPref("app.support.baseURL") + "suggestions"
--- a/browser/components/search/content/search.xml +++ b/browser/components/search/content/search.xml @@ -97,18 +97,20 @@ // Wait until the popupshowing event to avoid forcing immediate // attachment of the search-one-offs binding. this.textbox.popup.addEventListener("popupshowing", () => { let oneOffButtons = this.textbox.popup.oneOffButtons; // Some accessibility tests create their own <searchbar> that doesn't // use the popup binding below, so null-check oneOffButtons. if (oneOffButtons) { oneOffButtons.telemetryOrigin = "searchbar"; + // Set .textbox first, since the popup setter will cause + // a _rebuild call that uses it. + oneOffButtons.textbox = this.textbox; oneOffButtons.popup = this.textbox.popup; - oneOffButtons.textbox = this.textbox; } }, {capturing: true, once: true}); ]]></constructor> <destructor><![CDATA[ this.destroy(); ]]></destructor>
--- a/browser/themes/shared/incontentprefs-old/preferences.inc.css +++ b/browser/themes/shared/incontentprefs-old/preferences.inc.css @@ -381,17 +381,17 @@ description > html|a { * Sync */ #fxaProfileImage { max-width: 60px; border-radius: 50%; list-style-image: url(chrome://browser/skin/fxa/default-avatar.svg); margin-inline-end: 15px; - image-rendering: -moz-crisp-edges; + image-rendering: auto; border: 1px solid transparent; } #fxaLoginStatus[hasName] #fxaProfileImage { max-width: 80px; } #fxaProfileImage.actionable {
--- a/browser/themes/shared/incontentprefs/preferences.inc.css +++ b/browser/themes/shared/incontentprefs/preferences.inc.css @@ -401,17 +401,17 @@ groupbox { * Sync */ #fxaProfileImage { max-width: 60px; border-radius: 50%; list-style-image: url(chrome://browser/skin/fxa/default-avatar.svg); margin-inline-end: 15px; - image-rendering: -moz-crisp-edges; + image-rendering: auto; border: 1px solid transparent; } #fxaLoginStatus[hasName] #fxaProfileImage { max-width: 80px; } #fxaProfileImage.actionable {
--- a/devtools/client/debugger/new/debugger.css +++ b/devtools/client/debugger/new/debugger.css @@ -1311,17 +1311,18 @@ html[dir="rtl"] .managed-tree .tree .nod .source-footer .tab.active { color: var(--theme-body-color); background-color: var(--theme-body-background); border-color: var(--theme-splitter-color); border-top-color: transparent; } -.source-footer .tab.active path, .source-footer .tab:hover path { +.source-footer .tab.active path, +.source-footer .tab:hover path { fill: var(--theme-body-color); } .outline-list { list-style-type: none; padding-left: 0px; width: 100%; }
--- a/devtools/client/debugger/new/debugger.js +++ b/devtools/client/debugger/new/debugger.js @@ -14037,16 +14037,20 @@ return /******/ (function(modules) { // if (!source) { return; } return getSourceByURL(state, (0, _source2.getPrettySourceURL)(source.get("url"))); } function getSourceByUrlInSources(sources, url) { + if (!url) { + return null; + } + return sources.find(source => source.get("url") === url); } function getSourceInSources(sources, id) { return sources.get(id); } var getSources = exports.getSources = (0, _reselect.createSelector)(getSourcesState, sources => sources.sources); @@ -16715,17 +16719,17 @@ return /******/ (function(modules) { // * @module actions/sources */ // If a request has been made to show this source, go ahead and // select it. function checkSelectedSource(state, dispatch, source) { var pendingLocation = (0, _selectors.getPendingSelectedLocation)(state); - if (pendingLocation && pendingLocation.url === source.url) { + if (pendingLocation && !!source.url && pendingLocation.url === source.url) { dispatch(selectSource(source.id, { line: pendingLocation.line })); } } function newSource(source) { return (() => { var _ref3 = _asyncToGenerator(function* (_ref4) { var dispatch = _ref4.dispatch, @@ -21796,16 +21800,17 @@ return /******/ (function(modules) { // domain: __webpack_require__(353), file: __webpack_require__(354), folder: __webpack_require__(355), globe: __webpack_require__(356), jquery: __webpack_require__(999), underscore: __webpack_require__(1117), lodash: __webpack_require__(1118), ember: __webpack_require__(1119), + vuejs: __webpack_require__(1174), "magnifying-glass": __webpack_require__(357), "arrow-up": __webpack_require__(919), "arrow-down": __webpack_require__(920), pause: __webpack_require__(358), "pause-exceptions": __webpack_require__(359), plus: __webpack_require__(360), prettyPrint: __webpack_require__(361), react: __webpack_require__(1000), @@ -24112,16 +24117,17 @@ return /******/ (function(modules) { // var CallSites = (0, _react.createFactory)(_CallSites3.default); var cssVars = { searchbarHeight: "var(--editor-searchbar-height)", secondSearchbarHeight: "var(--editor-second-searchbar-height)", footerHeight: "var(--editor-footer-height)" }; + var debugExpression = void 0; class Editor extends _react.PureComponent { constructor() { super(); this.cbPanel = null; this.pendingJumpLine = null; this.lastJumpLine = null; @@ -24144,17 +24150,16 @@ return /******/ (function(modules) { // } componentWillReceiveProps(nextProps) { // This lifecycle method is responsible for updating the editor // text. var selectedSource = nextProps.selectedSource, selectedLocation = nextProps.selectedLocation; - this.clearDebugLine(this.props.selectedFrame); if (nextProps.startPanelSize !== this.props.startPanelSize || nextProps.endPanelSize !== this.props.endPanelSize) { this.state.editor.codeMirror.setSize(); } if (!selectedSource) { if (this.props.selectedSource) { this.showMessage(""); @@ -24162,24 +24167,25 @@ return /******/ (function(modules) { // } else if (selectedSource.get("loading")) { this.showMessage(L10N.getStr("loadingText")); } else if (selectedSource.get("error")) { this.showMessage(selectedSource.get("error")); } else if (this.props.selectedSource !== selectedSource) { (0, _editor.showSourceText)(this.state.editor, selectedSource.toJS()); } - if (this.props.linesInScope !== nextProps.linesInScope) { + if (this.state.editor && this.props.linesInScope !== nextProps.linesInScope) { this.state.editor.codeMirror.operation(() => { (0, _editor.clearLineClass)(this.state.editor.codeMirror, "in-scope"); }); - } - - this.setDebugLine(nextProps.selectedFrame, selectedLocation); - (0, _editor.resizeBreakpointGutter)(this.state.editor.codeMirror); + + this.clearDebugLine(this.props.selectedFrame); + this.setDebugLine(nextProps.selectedFrame, selectedLocation); + (0, _editor.resizeBreakpointGutter)(this.state.editor.codeMirror); + } } setupEditor() { var editor = (0, _editor.createEditor)(); // disables the default search shortcuts editor._initShortcuts = () => {}; @@ -24356,43 +24362,51 @@ return /******/ (function(modules) { // var direction = e.shiftKey ? "prev" : "next"; (0, _editor.traverseResults)(e, ctx, query, direction, searchModifiers.toJS()); } clearPreviewSelection() { this.props.clearSelection(); } - openMenu(event, codeMirror) { + inSelectedFrameSource() { var _props3 = this.props, - selectedSource = _props3.selectedSource, selectedLocation = _props3.selectedLocation, - showSource = _props3.showSource, - jumpToMappedLocation = _props3.jumpToMappedLocation, - addExpression = _props3.addExpression, - toggleBlackBox = _props3.toggleBlackBox; + selectedFrame = _props3.selectedFrame; + + return selectedFrame && selectedLocation && selectedFrame.location.sourceId == selectedLocation.sourceId; + } + + openMenu(event, codeMirror) { + var _props4 = this.props, + selectedSource = _props4.selectedSource, + selectedLocation = _props4.selectedLocation, + showSource = _props4.showSource, + jumpToMappedLocation = _props4.jumpToMappedLocation, + addExpression = _props4.addExpression, + toggleBlackBox = _props4.toggleBlackBox; return (0, _EditorMenu2.default)({ codeMirror, event, selectedLocation, selectedSource, showSource, jumpToMappedLocation, addExpression, toggleBlackBox, onGutterContextMenu: this.onGutterContextMenu }); } onGutterClick(cm, line, gutter, ev) { - var _props4 = this.props, - selectedSource = _props4.selectedSource, - toggleBreakpoint = _props4.toggleBreakpoint; + var _props5 = this.props, + selectedSource = _props5.selectedSource, + toggleBreakpoint = _props5.toggleBreakpoint; // ignore right clicks in the gutter if (ev.ctrlKey && ev.button === 0 || ev.which === 3 || selectedSource && selectedSource.get("isBlackBoxed")) { return; } if (this.isCbPanelOpen()) { @@ -24400,21 +24414,21 @@ return /******/ (function(modules) { // } if (gutter !== "CodeMirror-foldgutter") { toggleBreakpoint(line + 1); } } onGutterContextMenu(event) { - var _props5 = this.props, - selectedSource = _props5.selectedSource, - breakpoints = _props5.breakpoints, - toggleBreakpoint = _props5.toggleBreakpoint, - toggleDisabledBreakpoint = _props5.toggleDisabledBreakpoint; + var _props6 = this.props, + selectedSource = _props6.selectedSource, + breakpoints = _props6.breakpoints, + toggleBreakpoint = _props6.toggleBreakpoint, + toggleDisabledBreakpoint = _props6.toggleDisabledBreakpoint; if (selectedSource && selectedSource.get("isBlackBoxed")) { event.preventDefault(); return; } var line = (0, _editor.lineAtHeight)(this.state.editor, event); @@ -24441,20 +24455,20 @@ return /******/ (function(modules) { // } } toggleConditionalPanel(line) { if (this.isCbPanelOpen()) { return this.closeConditionalPanel(); } - var _props6 = this.props, - selectedLocation = _props6.selectedLocation, - setBreakpointCondition = _props6.setBreakpointCondition, - breakpoints = _props6.breakpoints; + var _props7 = this.props, + selectedLocation = _props7.selectedLocation, + setBreakpointCondition = _props7.setBreakpointCondition, + breakpoints = _props7.breakpoints; var sourceId = selectedLocation ? selectedLocation.sourceId : ""; var breakpoint = breakpoints.find(bp => bp.location.line === line); var location = { sourceId, line }; var condition = breakpoint ? breakpoint.condition : ""; var panel = (0, _ConditionalPanel.renderConditionalPanel)({ @@ -24475,35 +24489,38 @@ return /******/ (function(modules) { // this.cbPanel = null; } isCbPanelOpen() { return !!this.cbPanel; } clearDebugLine(selectedFrame) { - if (selectedFrame) { - var line = selectedFrame.location.line; - if (this.debugExpression) { - this.debugExpression.clear(); + if (this.state.editor && selectedFrame) { + var _selectedFrame$locati = selectedFrame.location, + sourceId = _selectedFrame$locati.sourceId, + line = _selectedFrame$locati.line; + + if (debugExpression) { + debugExpression.clear(); } this.state.editor.codeMirror.removeLineClass(line - 1, "line", "new-debug-line"); } } setDebugLine(selectedFrame, selectedLocation) { - if (selectedFrame && selectedLocation && selectedFrame.location.sourceId === selectedLocation.sourceId) { - var _selectedFrame$locati = selectedFrame.location, - line = _selectedFrame$locati.line, - column = _selectedFrame$locati.column; + if (this.state.editor && selectedFrame && selectedLocation && selectedFrame.location.sourceId === selectedLocation.sourceId) { + var _selectedFrame$locati2 = selectedFrame.location, + line = _selectedFrame$locati2.line, + column = _selectedFrame$locati2.column; this.state.editor.codeMirror.addLineClass(line - 1, "line", "new-debug-line"); - this.debugExpression = (0, _editor.markText)(this.state.editor, "debug-expression", { + debugExpression = (0, _editor.markText)(this.state.editor, "debug-expression", { start: { line, column }, end: { line, column: null } }); } } // If the location has changed and a specific line is requested, // move to that line and flash it. @@ -24536,47 +24553,16 @@ return /******/ (function(modules) { // } showMessage(msg) { this.state.editor.replaceDocument(this.state.editor.createDocument()); this.state.editor.setText(msg); this.state.editor.setMode({ name: "text" }); } - renderHighlightLines() { - var highlightedLineRange = this.props.highlightedLineRange; - - - if (!highlightedLineRange) { - return; - } - - return HighlightLines({ - editor: this.state.editor, - highlightedLineRange - }); - } - - renderHitCounts() { - var _props7 = this.props, - hitCount = _props7.hitCount, - selectedSource = _props7.selectedSource; - - - if (!selectedSource || selectedSource.get("loading") || !hitCount || !this.state.editor) { - return; - } - - return hitCount.filter(marker => marker.get("count") > 0).map(marker => HitMarker({ - key: marker.get("line"), - hitData: marker.toJS(), - editor: this.state.editor.codeMirror - })); - } - getInlineEditorStyles() { var _props8 = this.props, selectedSource = _props8.selectedSource, horizontal = _props8.horizontal, searchOn = _props8.searchOn; var subtractions = []; @@ -24590,20 +24576,51 @@ return /******/ (function(modules) { // subtractions.push(cssVars.secondSearchbarHeight); } return { height: subtractions.length === 0 ? "100%" : `calc(100% - ${subtractions.join(" - ")})` }; } - renderPreview() { + renderHighlightLines() { + var highlightedLineRange = this.props.highlightedLineRange; + + + if (!highlightedLineRange) { + return; + } + + return HighlightLines({ + editor: this.state.editor, + highlightedLineRange + }); + } + + renderHitCounts() { var _props9 = this.props, - selectedSource = _props9.selectedSource, - selection = _props9.selection; + hitCount = _props9.hitCount, + selectedSource = _props9.selectedSource; + + + if (!selectedSource || selectedSource.get("loading") || !hitCount || !this.state.editor) { + return; + } + + return hitCount.filter(marker => marker.get("count") > 0).map(marker => HitMarker({ + key: marker.get("line"), + hitData: marker.toJS(), + editor: this.state.editor.codeMirror + })); + } + + renderPreview() { + var _props10 = this.props, + selectedSource = _props10.selectedSource, + selection = _props10.selection; if (!this.state.editor || !selectedSource) { return null; } if (!selection || selection.updating) { return; } @@ -24626,70 +24643,90 @@ return /******/ (function(modules) { // popoverPos: cursorPos, onClose: () => this.clearPreviewSelection() }); } renderInScopeLines() { var linesInScope = this.props.linesInScope; - if (!(0, _devtoolsConfig.isEnabled)("highlightScopeLines") || !linesInScope || !this.inSelectedFrameSource()) { + if (!this.state.editor || !(0, _devtoolsConfig.isEnabled)("highlightScopeLines") || !linesInScope || !this.inSelectedFrameSource()) { return; } this.state.editor.codeMirror.operation(() => { linesInScope.forEach(line => { this.state.editor.codeMirror.addLineClass(line - 1, "line", "in-scope"); }); }); } - inSelectedFrameSource() { - var _props10 = this.props, - selectedLocation = _props10.selectedLocation, - selectedFrame = _props10.selectedFrame; - - return selectedFrame && selectedLocation && selectedFrame.location.sourceId == selectedLocation.sourceId; - } - renderCallSites() { var editor = this.state.editor; if (!editor || !(0, _devtoolsConfig.isEnabled)("columnBreakpoints")) { return null; } return CallSites({ editor }); } - render() { + renderSearchBar() { var _props11 = this.props, selectSource = _props11.selectSource, selectedSource = _props11.selectedSource, highlightLineRange = _props11.highlightLineRange, - clearHighlightLineRange = _props11.clearHighlightLineRange, - coverageOn = _props11.coverageOn, - pauseData = _props11.pauseData, - horizontal = _props11.horizontal; + clearHighlightLineRange = _props11.clearHighlightLineRange; + + + if (!this.state.editor) { + return null; + } + + return SearchBar({ + editor: this.state.editor, + selectSource, + selectedSource, + highlightLineRange, + clearHighlightLineRange + }); + } + + renderFooter() { + var horizontal = this.props.horizontal; + + + if (!this.state.editor) { + return null; + } + return Footer({ editor: this.state.editor, horizontal }); + } + + renderBreakpoints() { + if (!this.state.editor) { + return null; + } + + return Breakpoints({ editor: this.state.editor }); + } + + render() { + var _props12 = this.props, + coverageOn = _props12.coverageOn, + pauseData = _props12.pauseData; return _react.DOM.div({ className: (0, _classnames2.default)("editor-wrapper", { "coverage-on": coverageOn, paused: !!pauseData && (0, _devtoolsConfig.isEnabled)("highlightScopeLines") }) - }, SearchBar({ - editor: this.state.editor, - selectSource, - selectedSource, - highlightLineRange, - clearHighlightLineRange - }), _react.DOM.div({ + }, this.renderSearchBar(), _react.DOM.div({ className: "editor-mount devtools-monospace", style: this.getInlineEditorStyles() - }), this.renderHighlightLines(), this.renderInScopeLines(), this.renderHitCounts(), Footer({ editor: this.state.editor, horizontal }), this.renderPreview(), this.renderCallSites(), Breakpoints({ editor: this.state.editor })); + }), this.renderHighlightLines(), this.renderInScopeLines(), this.renderHitCounts(), this.renderFooter(), this.renderPreview(), this.renderCallSites(), this.renderBreakpoints()); } } Editor.displayName = "Editor"; Editor.propTypes = { breakpoints: _reactImmutableProptypes2.default.map, hitCount: _react.PropTypes.object, @@ -27814,35 +27851,31 @@ return /******/ (function(modules) { // editor = _props2.editor, breakpoint = _props2.breakpoint, selectedSource = _props2.selectedSource; return editor !== nextProps.editor || breakpoint.disabled !== nextProps.breakpoint.disabled || breakpoint.condition !== nextProps.breakpoint.condition || breakpoint.loading !== nextProps.breakpoint.loading || selectedSource !== nextProps.selectedSource; } componentDidMount() { - if (!this.props.editor) { - return; - } - this.addBreakpoint(); } componentDidUpdate() { this.addBreakpoint(); } componentWillUnmount() { var _props3 = this.props, editor = _props3.editor, breakpoint = _props3.breakpoint, selectedSource = _props3.selectedSource; - if (!editor || !selectedSource) { + if (!selectedSource) { return; } if (breakpoint.loading) { return; } var line = breakpoint.location.line - 1; @@ -27903,32 +27936,24 @@ return /******/ (function(modules) { // this.props.editor.addLineClass(line, "line", "hit-marker"); } shouldComponentUpdate(nextProps) { return this.props.editor !== nextProps.editor || this.props.hitData !== nextProps.hitData; } componentDidMount() { - if (!this.props.editor) { - return; - } - this.addMarker(); } componentDidUpdate() { this.addMarker(); } componentWillUnmount() { - if (!this.props.editor) { - return; - } - var hitData = this.props.hitData; var line = hitData.line - 1; this.props.editor.setGutterMarker(line, "hit-markers", null); this.props.editor.removeLineClass(line, "line", "hit-marker"); } render() { @@ -45928,17 +45953,18 @@ return /******/ (function(modules) { // * numbers gutter. Editor CSS will absolutely position the gutter * beneath the line numbers. This makes it easy to be flexible with * how we overlay breakpoints. */ function resizeBreakpointGutter(editor) { var gutters = editor.display.gutters; var lineNumbers = gutters.querySelector(".CodeMirror-linenumbers"); var breakpoints = gutters.querySelector(".breakpoints"); - breakpoints.style.width = `${lineNumbers.clientWidth}px`; + var width = lineNumbers.clientWidth; + breakpoints.style.width = `${width}px`; } module.exports = { removeLineClass, clearLineClass, getTextForLine, getCursorLine, resizeBreakpointGutter @@ -46992,16 +47018,20 @@ return /******/ (function(modules) { // function isLodash(frame) { return getFrameUrl(frame).match(/lodash/i); } function isEmber(frame) { return getFrameUrl(frame).match(/ember/i); } + function isVueJS(frame) { + return getFrameUrl(frame).match(/vue\.js/i); + } + function isRxJs(frame) { return getFrameUrl(frame).match(/rxjs/i); } function isAngular(frame) { return getFrameUrl(frame).match(/angular/i); } @@ -47048,16 +47078,20 @@ return /******/ (function(modules) { // if (isLodash(frame)) { return "Lodash"; } if (isEmber(frame)) { return "Ember"; } + if (isVueJS(frame)) { + return "VueJS"; + } + if (isRxJs(frame)) { return "RxJS"; } if (isAngular(frame)) { return "Angular"; } } @@ -47070,16 +47104,19 @@ return /******/ (function(modules) { // jQuery: { "jQuery.event.dispatch": "Dispatch Event" }, React: { // eslint-disable-next-line max-len "ReactCompositeComponent._renderValidatedComponentWithoutOwnerOrContext/renderedElement<": "Render", _renderValidatedComponentWithoutOwnerOrContext: "Render" }, + VueJS: { + "renderMixin/Vue.prototype._render": "Render" + }, Webpack: { // eslint-disable-next-line camelcase __webpack_require__: "Bootstrap" } }; function mapDisplayNames(frame, library) { var map = displayNameMap[library]; @@ -47376,20 +47413,16 @@ return /******/ (function(modules) { // class HighlightLines extends _react.Component { constructor() { super(); this.highlightLineRange = this.highlightLineRange.bind(this); } componentDidMount() { - if (!this.props.editor) { - return; - } - this.highlightLineRange(); } componentWillUpdate() { this.clearHighlightRange(); } componentDidUpdate() { @@ -47399,26 +47432,20 @@ return /******/ (function(modules) { // componentWillUnmount() { this.clearHighlightRange(); } clearHighlightRange() { var _props = this.props, highlightedLineRange = _props.highlightedLineRange, editor = _props.editor; - - - if (!editor) { - return; - } - var codeMirror = editor.codeMirror; - if ((0, _isEmpty2.default)(highlightedLineRange) || !editor || !codeMirror) { + if ((0, _isEmpty2.default)(highlightedLineRange) || !codeMirror) { return; } var start = highlightedLineRange.start, end = highlightedLineRange.end; codeMirror.operation(() => { (0, _range2.default)(start - 1, end).forEach(line => { @@ -47426,22 +47453,16 @@ return /******/ (function(modules) { // }); }); } highlightLineRange() { var _props2 = this.props, highlightedLineRange = _props2.highlightedLineRange, editor = _props2.editor; - - - if (!editor) { - return; - } - var codeMirror = editor.codeMirror; if ((0, _isEmpty2.default)(highlightedLineRange) || !codeMirror) { return; } var start = highlightedLineRange.start, @@ -52305,22 +52326,18 @@ return /******/ (function(modules) { // shouldComponentUpdate(nextProps) { return this.props.editor !== nextProps.editor; } componentDidMount() { var _props = this.props, breakpoint = _props.breakpoint, - editor = _props.editor, showCallSite = _props.showCallSite; - if (!editor) { - return; - } if (!breakpoint && !showCallSite) { return; } this.addCallSite(); } @@ -52344,17 +52361,17 @@ return /******/ (function(modules) { // } } else if (!nextProps.breakpoint) { this.clearCallSite(); } } } componentWillUnmount() { - if (!this.props.editor || !this.marker) { + if (!this.marker) { return; } this.marker.clear(); } render() { return null; } @@ -52467,16 +52484,17 @@ return /******/ (function(modules) { // if (!prevProps.enabled && this.props.enabled) { this.updateResults(this.state.query); } } openSymbolModal(_, e) { e.preventDefault(); + e.stopPropagation(); this.props.setActiveSearch("symbol"); } onClick(e) { e.stopPropagation(); } onChange(e) { @@ -52488,16 +52506,17 @@ return /******/ (function(modules) { // this.setState({ query: e.target.value }); return this.updateResults(e.target.value); } closeModal() { this.props.closeActiveSearch(); this.props.clearHighlightLineRange(); + this.setState({ query: "" }); } selectResultItem(e, item) { var _props = this.props, selectSource = _props.selectSource, selectedSource = _props.selectedSource; @@ -52693,12 +52712,20 @@ return /******/ (function(modules) { // }, dispatch => (0, _redux.bindActionCreators)(_actions2.default, dispatch))(SymbolModal); /***/ }, /* 1171 */ /***/ function(module, exports) { // removed by extract-text-webpack-plugin +/***/ }, +/* 1172 */, +/* 1173 */, +/* 1174 */ +/***/ function(module, exports) { + + module.exports = "<svg xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:cc=\"http://creativecommons.org/ns#\" xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:svg=\"http://www.w3.org/2000/svg\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 400 400\" xml:space=\"preserve\" id=\"svg2\" version=\"1.1\"><metadata id=\"metadata8\"><rdf:RDF><cc:Work rdf:about><dc:format>image/svg+xml</dc:format><dc:type rdf:resource=\"http://purl.org/dc/dcmitype/StillImage\"></dc></cc:Work></rdf:RDF></metadata><defs id=\"defs6\"></defs><g transform=\"matrix(1.3333333,0,0,-1.3333333,0,400)\" id=\"g10\"><g transform=\"translate(178.0626,235.0086)\" id=\"g12\"><path id=\"path14\" style=\"fill:#41b883;fill-opacity:1;fill-rule:nonzero;stroke:none\" d=\"M 0,0 -22.669,-39.264 -45.338,0 h -75.491 L -22.669,-170.017 75.491,0 Z\"></path></g><g transform=\"translate(178.0626,235.0086)\" id=\"g16\"><path id=\"path18\" style=\"fill:#34495e;fill-opacity:1;fill-rule:nonzero;stroke:none\" d=\"M 0,0 -22.669,-39.264 -45.338,0 H -81.565 L -22.669,-102.01 36.227,0 Z\"></path></g></g></svg>" + /***/ } /******/ ]) }); ; \ No newline at end of file
--- a/devtools/client/debugger/new/parser-worker.js +++ b/devtools/client/debugger/new/parser-worker.js @@ -30884,17 +30884,17 @@ return /******/ (function(modules) { // function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var ASTs = new Map(); function _parse(code, opts) { return babylon.parse(code, Object.assign({}, opts, { sourceType: "module", - plugins: ["jsx", "flow"] + plugins: ["jsx", "flow", "objectRestSpread"] })); } function parse(text, opts) { var ast = void 0; if (!text) { return; }
--- a/devtools/client/debugger/new/test/mochitest/browser.ini +++ b/devtools/client/debugger/new/test/mochitest/browser.ini @@ -47,16 +47,17 @@ support-files = [browser_dbg-breakpoints.js] [browser_dbg-breakpoints-reloading.js] [browser_dbg-breakpoints-cond.js] [browser_dbg-call-stack.js] [browser_dbg-expressions.js] [browser_dbg-scopes.js] [browser_dbg-chrome-create.js] [browser_dbg-chrome-debugging.js] +skip-if = debug # bug 1374187 [browser_dbg-console.js] [browser_dbg-debugger-buttons.js] [browser_dbg-editor-gutter.js] [browser_dbg-editor-select.js] [browser_dbg-editor-highlight.js] [browser_dbg-iframes.js] [browser_dbg_keyboard_navigation.js] [browser_dbg_keyboard-shortcuts.js]
--- a/devtools/client/inspector/layout/components/App.js +++ b/devtools/client/inspector/layout/components/App.js @@ -35,17 +35,16 @@ const App = createClass({ propTypes: { boxModel: PropTypes.shape(BoxModelTypes.boxModel).isRequired, getSwatchColorPickerTooltip: PropTypes.func.isRequired, grids: PropTypes.arrayOf(PropTypes.shape(GridTypes.grid)).isRequired, highlighterSettings: PropTypes.shape(GridTypes.highlighterSettings).isRequired, setSelectedNode: PropTypes.func.isRequired, showBoxModelProperties: PropTypes.bool.isRequired, - showGridOutline: PropTypes.bool.isRequired, onHideBoxModelHighlighter: PropTypes.func.isRequired, onPromoteLearnMoreClick: PropTypes.func.isRequired, onSetGridOverlayColor: PropTypes.func.isRequired, onShowBoxModelEditor: PropTypes.func.isRequired, onShowBoxModelHighlighter: PropTypes.func.isRequired, onShowBoxModelHighlighterForNode: PropTypes.func.isRequired, onToggleGridHighlighter: PropTypes.func.isRequired, onToggleShowGridLineNumbers: PropTypes.func.isRequired,
--- a/devtools/client/inspector/rules/rules.js +++ b/devtools/client/inspector/rules/rules.js @@ -21,16 +21,17 @@ const ClassListPreviewer = require("devt const {gDevTools} = require("devtools/client/framework/devtools"); const {getCssProperties} = require("devtools/shared/fronts/css-properties"); const { VIEW_NODE_SELECTOR_TYPE, VIEW_NODE_PROPERTY_TYPE, 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"); const {createChild, promiseWarn, debounce} = require("devtools/client/inspector/shared/utils"); const EventEmitter = require("devtools/shared/event-emitter"); const KeyShortcuts = require("devtools/client/shared/key-shortcuts"); const clipboardHelper = require("devtools/shared/platform/clipboard"); const AutocompletePopup = require("devtools/client/shared/autocomplete-popup"); @@ -43,16 +44,17 @@ const PREF_ENABLE_MDN_DOCS_TOOLTIP = const FILTER_CHANGED_TIMEOUT = 150; const PREF_ORIG_SOURCES = "devtools.styleeditor.source-maps-enabled"; // This is used to parse user input when filtering. 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: * * ElementStyle: * Responsible for keeping track of which properties are overridden. * Maintains a list of Rule objects that apply to the element. * Rule: @@ -328,16 +330,29 @@ CssRuleView.prototype = { property: getPropertyNameAndValue(node).name, value: node.textContent, enabled: prop.enabled, overridden: prop.overridden, pseudoElement: prop.rule.pseudoElement, 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; value = { property: getPropertyNameAndValue(node).name, value: node.parentNode.textContent, url: node.href, enabled: prop.enabled, @@ -1534,16 +1549,62 @@ function getPropertyNameAndValue(node) { name: node.querySelector(".ruleview-propertyname").textContent, value: node.querySelector(".ruleview-propertyvalue").textContent }; } node = node.parentNode; } } +/** + * 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; this.view = new CssRuleView(this.inspector, this.document); this.clearUserProperties = this.clearUserProperties.bind(this); this.refresh = this.refresh.bind(this);
--- a/devtools/client/inspector/rules/test/browser.ini +++ b/devtools/client/inspector/rules/test/browser.ini @@ -228,16 +228,22 @@ subsuite = clipboard skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts [browser_rules_selector-highlighter-on-navigate.js] [browser_rules_selector-highlighter_01.js] [browser_rules_selector-highlighter_02.js] [browser_rules_selector-highlighter_03.js] [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] [browser_rules_strict-search-filter_02.js] [browser_rules_strict-search-filter_03.js] [browser_rules_style-editor-link.js] skip-if = !debug # Bug 1309759 [browser_rules_url-click-opens-new-tab.js]
--- 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 @@ -45,24 +45,24 @@ add_task(function* () { info("Toggling ON the CSS grid highlighter from the rule-view."); let onHighlighterShown = highlighters.once("grid-highlighter-shown"); gridToggle.click(); yield onHighlighterShown; 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"); info("Check that the grid highlighter can be displayed after reloading the page"); ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown."); 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"); ok(!highlighters.gridHighlighterShown, "CSS grid highlighter is hidden."); });
new file mode 100644 --- /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 = ` + <style type='text/css'> + #shape { + width: 800px; + height: 800px; + clip-path: circle(25%); + } + </style> + <div id="shape"></div> +`; + +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."); +});
new file mode 100644 --- /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 = ` + <style type='text/css'> + #shape { + width: 800px; + height: 800px; + clip-path: circle(25%); + } + div { + clip-path: circle(30%); + } + </style> + <div id="shape"></div> +`; + +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."); +});
new file mode 100644 --- /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 = ` + <style type='text/css'> + .shape { + width: 800px; + height: 800px; + clip-path: circle(25%); + } + </style> + <div class="shape" id="shape1"></div> + <div class="shape" id="shape2"></div> +`; + +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."); +});
new file mode 100644 --- /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 = ` + <style type='text/css'> + #shape { + width: 800px; + height: 800px; + clip-path: circle(25%); + } + </style> + <div id="shape"></div> +`; + +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."); +});
new file mode 100644 --- /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 = ` + <style type='text/css'> + #shape { + width: 800px; + height: 800px; + clip-path: circle(25%); + } + </style> + <div id="shape"></div> +`; + +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."); +});
new file mode 100644 --- /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 = ` + <style type='text/css'> + .shape { + width: 800px; + height: 800px; + clip-path: circle(25%); + shape-outside: circle(25%); + } + </style> + <div class="shape" id="shape1"></div> + <div class="shape" id="shape2"></div> +`; + +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."); +});
--- a/devtools/client/inspector/rules/test/head.js +++ b/devtools/client/inspector/rules/test/head.js @@ -18,18 +18,21 @@ var {getInplaceEditorForSpan: inplaceEdi require("devtools/client/shared/inplace-editor"); const ROOT_TEST_DIR = getRootDirectory(gTestPath); 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"); }); /** * The rule-view tests rely on a frame-script to be injected in the content test * page. So override the shared-head's addTab to load the frame script after the * tab was added. * FIXME: Refactor the rule-view tests to use the testActor instead of a frame * script, so they can run on remote targets too.
--- a/devtools/client/inspector/rules/views/text-property-editor.js +++ b/devtools/client/inspector/rules/views/text-property-editor.js @@ -22,16 +22,17 @@ const Services = require("Services"); const HTML_NS = "http://www.w3.org/1999/xhtml"; const SHARED_SWATCH_CLASS = "ruleview-swatch"; 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 * (e.g. shows a color tooltip, opens a link, …). */ const ACTIONABLE_ELEMENTS_SELECTORS = [ `.${COLOR_SWATCH_CLASS}`, `.${BEZIER_SWATCH_CLASS}`, @@ -73,16 +74,17 @@ function TextPropertyEditor(ruleEditor, this._onNameDone = this._onNameDone.bind(this); this._onValueDone = this._onValueDone.bind(this); this._onSwatchCommit = this._onSwatchCommit.bind(this); this._onSwatchPreview = this._onSwatchPreview.bind(this); this._onSwatchRevert = this._onSwatchRevert.bind(this); 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(); } TextPropertyEditor.prototype = { /** * Boolean indicating if the name or value is being currently edited. @@ -295,16 +297,18 @@ TextPropertyEditor.prototype = { contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE, property: this.prop, popup: this.popup, multiline: true, maxWidth: () => this.container.getBoundingClientRect().width, cssProperties: this.cssProperties, contextMenu: this.ruleView.inspector.onTextBoxContextMenu }); + + this.ruleView.highlighters.on("hover-shape-point", this._onHoverShapePoint); } }, /** * Get the path from which to resolve requests for this * rule's stylesheet. * * @return {String} the stylesheet's href. @@ -353,16 +357,17 @@ TextPropertyEditor.prototype = { angleSwatchClass: SHARED_SWATCH_CLASS + " " + ANGLE_SWATCH_CLASS, bezierClass: "ruleview-bezier", bezierSwatchClass: SHARED_SWATCH_CLASS + " " + BEZIER_SWATCH_CLASS, colorClass: "ruleview-color", colorSwatchClass: SHARED_SWATCH_CLASS + " " + COLOR_SWATCH_CLASS, filterClass: "ruleview-filter", filterSwatchClass: SHARED_SWATCH_CLASS + " " + FILTER_SWATCH_CLASS, gridClass: "ruleview-grid", + shapeClass: "ruleview-shape", defaultColorType: !propDirty, urlClass: "theme-link", baseURI: this.sheetHref }; let frag = outputParser.parseCssProperty(name, val, parserOptions); this.valueSpan.innerHTML = ""; this.valueSpan.appendChild(frag); @@ -437,16 +442,31 @@ TextPropertyEditor.prototype = { if (gridToggle) { gridToggle.setAttribute("title", l10n("rule.gridToggle.tooltip")); if (this.ruleView.highlighters.gridHighlighterShown === this.ruleView.inspector.selection.nodeFront) { gridToggle.classList.add("active"); } } + 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"); + highlighters.highlightRuleViewShapePoint(highlighters.state.shapes.hoverPoint); + } + } + // 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. if (this._hasPendingClick) { this._hasPendingClick = false; let elToClick; if (this._clickedElementOptions !== null) { @@ -924,12 +944,73 @@ TextPropertyEditor.prototype = { * Returns true if the property is a `display: [inline-]grid` declaration. * * @return {Boolean} true if the property is a `display: [inline-]grid` declaration. */ isDisplayGrid: function () { 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;
--- a/devtools/client/inspector/shared/highlighters-overlay.js +++ b/devtools/client/inspector/shared/highlighters-overlay.js @@ -4,19 +4,23 @@ * 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/. */ "use strict"; 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. * * @param {Inspector} inspector * Inspector toolbox panel. */ function HighlightersOverlay(inspector) { @@ -30,29 +34,34 @@ function HighlightersOverlay(inspector) // NodeFront of element that is highlighted by the geometry editor. this.geometryEditorHighlighterShown = null; // NodeFront of the grid container that is highlighted. this.gridHighlighterShown = null; // Name of the highlighter shown on mouse hover. 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); this.onMarkupMutation = this.onMarkupMutation.bind(this); this.onMouseMove = this.onMouseMove.bind(this); 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); + this._onHighlighterEvent = this._onHighlighterEvent.bind(this); // Add inspector events, not specific to a given view. this.inspector.on("markupmutation", this.onMarkupMutation); this.inspector.target.on("navigate", this.onNavigate); this.inspector.target.on("will-navigate", this.onWillNavigate); EventEmitter.decorate(this); } @@ -97,16 +106,136 @@ HighlightersOverlay.prototype = { let el = view.element; el.removeEventListener("click", this.onClick, true); el.removeEventListener("mousemove", this.onMouseMove); 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 = {}; + }), + + /** + * 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. * * @param {NodeFront} node * The NodeFront of the grid container element to highlight. * @param {Object} options * Object used for passing options to the grid highlighter. * @param. {String|null} trigger * String name matching "grid" or "rule" to indicate where the @@ -136,17 +265,17 @@ HighlightersOverlay.prototype = { return; } let isShown = yield highlighter.show(node, options); if (!isShown) { return; } - this._toggleRuleViewGridIcon(node, true); + this._toggleRuleViewIcon(node, true, ".ruleview-grid"); if (trigger == "grid") { Services.telemetry.scalarAdd("devtools.grid.gridinspector.opened", 1); } else if (trigger == "rule") { Services.telemetry.scalarAdd("devtools.rules.gridinspector.opened", 1); } try { @@ -170,17 +299,17 @@ HighlightersOverlay.prototype = { * @param {NodeFront} node * The NodeFront of the grid container element to unhighlight. */ hideGridHighlighter: Task.async(function* (node) { if (!this.gridHighlighterShown || !this.highlighters.CssGridHighlighter) { return; } - this._toggleRuleViewGridIcon(node, false); + this._toggleRuleViewIcon(node, false, ".ruleview-grid"); yield this.highlighters.CssGridHighlighter.hide(); // Emit the NodeFront of the grid container element that the grid highlighter was // hidden for. this.emit("grid-highlighter-hidden", this.gridHighlighterShown, this.state.grid.options); this.gridHighlighterShown = null; @@ -236,43 +365,67 @@ HighlightersOverlay.prototype = { yield this.highlighters.GeometryEditorHighlighter.hide(); this.emit("geometry-editor-highlighter-hidden"); 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 {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. + * expected highlighters are displayed. */ - restoreState: Task.async(function* () { - let { selector, options, url } = this.state.grid; - + 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("state-restored", { restored: false }); + this.emit(`${name}-state-restored`, { restored: false }); return; } // Wait for the new root to be ready in the inspector. yield this.onInspectorNewRoot; let walker = this.inspector.walker; let rootNode = yield walker.getRootNode(); let nodeFront = yield walker.querySelector(rootNode, selector); if (nodeFront) { - yield this.showGridHighlighter(nodeFront, options); - this.emit("state-restored", { restored: true }); + if (options.hoverPoint) { + options.hoverPoint = null; + } + yield showFunction(nodeFront, options); + this.emit(`${name}-state-restored`, { restored: true }); } - this.emit("state-restored", { restored: false }); + this.emit(`${name}-state-restored`, { restored: false }); }), /** * Get a highlighter front given a type. It will only be initialized once. * * @param {String} type * The highlighter type. One of this.highlighters. * @return {Promise} that resolves to the highlighter @@ -291,48 +444,68 @@ HighlightersOverlay.prototype = { } catch (e) { // Ignore any error } if (!highlighter) { return null; } + highlighter.on("highlighter-event", this._onHighlighterEvent); this.highlighters[type] = highlighter; return highlighter; }), _handleRejection: function (error) { if (!this.destroyed) { console.error(error); } }, /** - * 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); } }, /** + * 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. */ _hideHoveredHighlighter: function () { if (!this.hoveredHighlighterShown || !this.highlighters[this.hoveredHighlighterShown]) { return; } @@ -369,46 +542,75 @@ HighlightersOverlay.prototype = { * @param {DOMNode} node * @return {Boolean} */ _isRuleViewDisplayGrid: function (node) { 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. * * @param {Object} nodeInfo * @return {Boolean} */ _isRuleViewTransform: function (nodeInfo) { let isTransform = nodeInfo.type === VIEW_NODE_VALUE_TYPE && nodeInfo.value.property === "transform"; let isEnabled = nodeInfo.value.enabled && !nodeInfo.value.overridden && !nodeInfo.value.pseudoElement; return this.isRuleView && isTransform && isEnabled; }, - onClick: function (event) { - // Bail out if the target is not a grid property value. - if (!this._isRuleViewDisplayGrid(event.target)) { - return; - } - - event.stopPropagation(); + /** + * 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; + }, - let { store } = this.inspector; - let { grids, highlighterSettings } = store.getState(); - let grid = grids.find(g => g.nodeFront == this.inspector.selection.nodeFront); + onClick: function (event) { + if (this._isRuleViewDisplayGrid(event.target)) { + event.stopPropagation(); + + 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"); + this.toggleGridHighlighter(this.inspector.selection.nodeFront, highlighterSettings, + "rule"); + } else if (this._isRuleViewShape(event.target)) { + event.stopPropagation(); + + let settings = { mode: event.target.dataset.mode }; + this.toggleShapesHighlighter(this.inspector.selection.nodeFront, settings); + } }, onMouseMove: function (event) { // Bail out if the target is the same as for the last mousemove. if (event.target === this._lastHovered) { return; } @@ -420,16 +622,23 @@ HighlightersOverlay.prototype = { let view = this.isRuleView ? this.inspector.getPanel("ruleview").view : this.inspector.getPanel("computedview").computedView; let nodeInfo = view.getNodeInfo(event.target); if (!nodeInfo) { 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) || this._isComputedViewTransform(nodeInfo)) { type = "CssTransformHighlighter"; } if (type) { @@ -448,75 +657,103 @@ HighlightersOverlay.prototype = { onMouseOut: function (event) { // Only hide the highlighter if the mouse leaves the currently hovered node. if (!this._lastHovered || (event && this._lastHovered.contains(event.relatedTarget))) { return; } // 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(); }, /** - * 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); } - } 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); + } } }), /** * Restore saved highlighter state after navigate. */ 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); } }), /** * Clear saved highlighter shown properties on will-navigate. */ onWillNavigate: function () { this.geometryEditorHighlighterShown = null; 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"); }, /** * Destroy this overlay instance, removing it from the view and destroying * all initialized highlighters. */ 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; } } // Remove inspector events. this.inspector.off("markupmutation", this.onMarkupMutation); this.inspector.target.off("navigate", this.onNavigate); @@ -529,14 +766,15 @@ HighlightersOverlay.prototype = { this.highlighterUtils = null; this.supportsHighlighters = null; this.state = null; this.geometryEditorHighlighterShown = null; this.gridHighlighterShown = null; this.hoveredHighlighterShown = null; this.selectorHighlighterShown = null; + this.shapesHighlighterShown = null; this.destroyed = true; } }; module.exports = HighlightersOverlay;
--- a/devtools/client/inspector/shared/node-types.js +++ b/devtools/client/inspector/shared/node-types.js @@ -10,8 +10,9 @@ * Types of nodes used in the rule and omputed view. */ exports.VIEW_NODE_SELECTOR_TYPE = 1; 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;
--- a/devtools/client/inspector/test/browser.ini +++ b/devtools/client/inspector/test/browser.ini @@ -74,16 +74,17 @@ skip-if = os == "mac" # Full keyboard na [browser_inspector_highlighter-cancel.js] [browser_inspector_highlighter-comments.js] [browser_inspector_highlighter-cssgrid_01.js] [browser_inspector_highlighter-cssgrid_02.js] [browser_inspector_highlighter-cssshape_01.js] [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] [browser_inspector_highlighter-eyedropper-clipboard.js] subsuite = clipboard skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts [browser_inspector_highlighter-eyedropper-csp.js] [browser_inspector_highlighter-eyedropper-events.js]
new file mode 100644 --- /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"); +}
--- a/devtools/client/inspector/test/head.js +++ b/devtools/client/inspector/test/head.js @@ -798,8 +798,38 @@ function* getDisplayedNodeTextContent(se yield inspector.markup.expandNode(container.node); yield waitForMultipleChildrenUpdates(inspector); if (container) { let textContainer = container.elt.querySelector("pre"); return textContainer.textContent; } 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; + } +}
--- a/devtools/client/preferences/devtools.js +++ b/devtools/client/preferences/devtools.js @@ -59,16 +59,18 @@ pref("devtools.inspector.imagePreviewToo // Enable user agent style inspection in rule-view pref("devtools.inspector.showUserAgentStyles", false); // Show all native anonymous content (like controls in <video> tags) pref("devtools.inspector.showAllAnonymousContent", false); // Enable the MDN docs tooltip 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); // Counter to promote the inspector layout view. // @remove after release 56 (See Bug 1355747) pref("devtools.promote.layoutview", 1); // Whether or not to show the promote bar in the layout view
--- a/devtools/client/shared/output-parser.js +++ b/devtools/client/shared/output-parser.js @@ -5,24 +5,27 @@ "use strict"; const {angleUtils} = require("devtools/client/shared/css-angle"); const {colorUtils} = require("devtools/shared/css/color"); 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 } = 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"; 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 * linking JS files with the debugger, CSS files with the style editor, JS * functions with the debugger, placing color swatches next to colors and * adding doorhanger previews where possible (images, angles, lengths, * border radius, cubic-bezier etc.). * @@ -72,16 +75,17 @@ OutputParser.prototype = { * A document fragment containing color swatches etc. */ parseCssProperty: function (name, value, options = {}) { options = this._mergeOptions(options); 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); // The filter property is special in that we want to show the // swatch even if the value is invalid, because this way the user // can easily use the editor to fix it. if (options.expectFilter || this._cssPropertySupportsValue(name, value)) { return this._parse(value, options); @@ -157,24 +161,22 @@ OutputParser.prototype = { outerMostFunctionTakesColor); }; let angleOK = function (angle) { return (new angleUtils.CssAngle(angle)).valid; }; 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; } switch (token.tokenType) { case "function": { if (COLOR_TAKING_FUNCTIONS.includes(token.text) || ANGLE_TAKING_FUNCTIONS.includes(token.text)) { // The function can accept a color or an angle argument, and we know @@ -192,16 +194,20 @@ OutputParser.prototype = { let functionText = this._collectFunctionText(token, text, tokenStream); if (options.expectCubicBezier && token.text === "cubic-bezier") { this._appendCubicBezier(functionText, options); } 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); } } break; } case "ident": @@ -268,16 +274,18 @@ OutputParser.prototype = { } // If this token might possibly introduce token pasting when // color-cycling, require a space. spaceNeeded = (token.tokenType === "ident" || token.tokenType === "at" || 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(); if (options.expectFilter && !options.filterSwatch) { result = this._wrapFilter(text, options, result); } @@ -349,16 +357,548 @@ OutputParser.prototype = { value.textContent = grid; container.appendChild(toggle); container.appendChild(value); 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 + }); + + 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); + } + } + + 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 * * @param {String} angle * angle to append * @param {Object} options * Options object. For valid options and default values see * _mergeOptions() */ @@ -692,16 +1232,17 @@ OutputParser.prototype = { * // that follows the swatch. * - colorSwatchClass: "" // The class to use for color swatches. * - filterSwatch: false // A special case for parsing a * // "filter" property, causing the * // parser to skip the call to * // _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 * // relative links. * @return {Object} * Overridden options object */ _mergeOptions: function (overrides) { @@ -710,16 +1251,17 @@ OutputParser.prototype = { angleClass: "", angleSwatchClass: "", bezierClass: "", bezierSwatchClass: "", colorClass: "", colorSwatchClass: "", filterSwatch: false, gridClass: "", + shapeClass: "", supportsColor: false, urlClass: "", baseURI: undefined, }; for (let item in overrides) { defaults[item] = overrides[item]; }
--- a/devtools/client/shared/test/browser_outputparser.js +++ b/devtools/client/shared/test/browser_outputparser.js @@ -1,15 +1,16 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; 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"); yield performTest(); gBrowser.removeCurrentTab(); }); function* performTest() { @@ -22,16 +23,17 @@ function* performTest() { let cssProperties = getCssProperties(toolbox); let parser = new OutputParser(doc, cssProperties); testParseCssProperty(doc, parser); testParseCssVar(doc, parser); testParseURL(doc, parser); testParseFilter(doc, parser); testParseAngle(doc, parser); + testParseShape(doc, parser); host.destroy(); } // Class name used in color swatch. var COLOR_TEST_CLASS = "test-class"; // Create a new CSS color-parsing test. |name| is the name of the CSS @@ -288,8 +290,126 @@ function testParseAngle(doc, parser) { frag = parser.parseCssProperty("background-image", "linear-gradient(90deg, red, blue", { angleSwatchClass: "test-angleswatch" }); 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"); + } +}
--- a/devtools/client/sourceeditor/codemirror/README +++ b/devtools/client/sourceeditor/codemirror/README @@ -1,16 +1,16 @@ This is the CodeMirror editor packaged for the Mozilla Project. CodeMirror is a JavaScript component that provides a code editor in the browser. When a mode is available for the language you are coding in, it will color your 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]. Then to recreate codemirror.bundle.js: > cd devtools/client/sourceeditor > npm install > webpack
--- a/devtools/client/sourceeditor/codemirror/addon/fold/foldcode.js +++ b/devtools/client/sourceeditor/codemirror/addon/fold/foldcode.js @@ -60,16 +60,18 @@ function makeWidget(cm, options) { var widget = getOption(cm, options, "widget"); if (typeof widget == "string") { var text = document.createTextNode(widget); widget = document.createElement("span"); widget.appendChild(text); widget.className = "CodeMirror-foldmarker"; + } else if (widget) { + widget = widget.cloneNode(true) } return widget; } // Clumsy backwards-compatible interface CodeMirror.newFoldFunction = function(rangeFinder, widget) { return function(cm, pos) { doFold(cm, pos, {rangeFinder: rangeFinder, widget: widget}); }; };
--- a/devtools/client/sourceeditor/codemirror/addon/search/searchcursor.js +++ b/devtools/client/sourceeditor/codemirror/addon/search/searchcursor.js @@ -123,21 +123,23 @@ doFold = function(str) { return str.toLowerCase() } noFold = function(str) { return str } } // Maps a position in a case-folded line back to a position in the original line // (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 } } function searchStringForward(doc, query, start, caseFold) { // Empty string would match anything and never progress, so we // define it to match nothing instead. if (!query.length) return null var fold = caseFold ? doFold : noFold
--- a/devtools/client/sourceeditor/codemirror/addon/tern/tern.js +++ b/devtools/client/sourceeditor/codemirror/addon/tern/tern.js @@ -329,17 +329,21 @@ if (arg.type != "?") { tip.appendChild(document.createTextNode(":\u00a0")); tip.appendChild(elt("span", cls + "type", arg.type)); } } 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) { var args = [], pos = 3; function skipMatching(upto) { var depth = 0, start = pos; for (;;) { @@ -599,34 +603,42 @@ var where = cm.cursorCoords(); var tip = cm.state.ternTooltip = makeTooltip(where.right + 1, where.bottom, content); function maybeClear() { old = true; if (!mouseOnTip) clear(); } 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; }); CodeMirror.on(tip, "mouseout", function(e) { if (!CodeMirror.contains(tip, e.relatedTarget || e.toElement)) { if (old) clear(); else mouseOnTip = false; } }); 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) { var node = elt("div", cls + "tooltip", content); node.style.left = x + "px"; node.style.top = y + "px"; document.body.appendChild(node); return node; @@ -645,17 +657,21 @@ function showError(ts, cm, msg) { if (ts.options.showError) ts.options.showError(cm, msg); else tempTooltip(cm, String(msg), ts); } 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) { var val = doc.doc.getValue(); if (ts.options.fileFilter) val = ts.options.fileFilter(val, doc.name, doc.doc); return val; }
--- a/devtools/client/sourceeditor/codemirror/codemirror.bundle.js +++ b/devtools/client/sourceeditor/codemirror/codemirror.bundle.js @@ -7017,25 +7017,25 @@ var CodeMirror = ); }, goLineStartSmart: function (cm) { return cm.extendSelectionsBy(function (range) { return lineStartSmart(cm, range.head); }, {origin: "+move", bias: 1} ); }, goLineEnd: function (cm) { return cm.extendSelectionsBy(function (range) { return lineEnd(cm, range.head.line); }, {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 }, sel_move); }, goLineUp: function (cm) { return cm.moveV(-1, "line"); }, goLineDown: function (cm) { return cm.moveV(1, "line"); }, goPageUp: function (cm) { return cm.moveV(-1, "page"); }, goPageDown: function (cm) { return cm.moveV(1, "page"); }, @@ -8580,16 +8580,18 @@ var CodeMirror = { if (line.widgets[i].noHScroll) { regLineChange(this$1, lineNo, "widget"); break } } } ++lineNo }) this.curOp.forceUpdate = true signal(this, "refresh", this) }), 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 regChange(this) this.curOp.forceUpdate = true clearCaches(this) scrollToCoords(this, this.doc.scrollLeft, this.doc.scrollTop) updateGutterSpace(this) @@ -9232,19 +9234,16 @@ var CodeMirror = this.prevInput = "" // Flag that indicates whether we expect input to appear real soon // now (after some event like 'keypress' or 'input') and are // polling intensively. 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 }; TextareaInput.prototype.init = function (display) { var this$1 = this; @@ -9271,22 +9270,16 @@ var CodeMirror = cm.state.pasteIncoming = true input.fastPoll() }) function prepareCopyCut(e) { 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 { var ranges = copyableRanges(cm) setLastCopied({lineWise: true, text: ranges.text}) if (e.type == "cut") { cm.setSelections(ranges.ranges, null, sel_dontScroll) } else { @@ -9355,31 +9348,27 @@ var CodeMirror = this.wrapper.style.left = drawn.teLeft + "px" } }; // Reset the input to correspond to the selection (or to be empty, // 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 } } else if (!typing) { this.prevInput = this.textarea.value = "" if (ie && ie_version >= 9) { this.hasSelection = null } } - this.inaccurateSelection = minimal }; TextareaInput.prototype.getField = function () { return this.textarea }; TextareaInput.prototype.supportsTouch = function () { return false }; TextareaInput.prototype.focus = function () { if (this.cm.options.readOnly != "nocursor" && (!mobile || activeElt() != this.textarea)) { @@ -9720,17 +9709,17 @@ var CodeMirror = CodeMirror.defineDocExtension = function (name, func) { Doc.prototype[name] = func } CodeMirror.fromTextArea = fromTextArea addLegacyProps(CodeMirror) - CodeMirror.version = "5.27.4" + CodeMirror.version = "5.28.0" return CodeMirror; }))); /***/ }), /* 3 */ /***/ (function(module, exports, __webpack_require__) { @@ -9860,21 +9849,23 @@ var CodeMirror = doFold = function(str) { return str.toLowerCase() } noFold = function(str) { return str } } // Maps a position in a case-folded line back to a position in the original line // (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 } } function searchStringForward(doc, query, start, caseFold) { // Empty string would match anything and never progress, so we // define it to match nothing instead. if (!query.length) return null var fold = caseFold ? doFold : noFold @@ -20677,16 +20668,33 @@ var CodeMirror = if (!found || isSelectedRange(cm.listSelections(), cur.from(), cur.to())) return CodeMirror.Pass cm.addSelection(cur.from(), cur.to()); } if (fullWord) 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 return false } var mirror = "(){}[]"; function selectBetweenBrackets(cm) { @@ -21179,16 +21187,18 @@ var CodeMirror = function makeWidget(cm, options) { var widget = getOption(cm, options, "widget"); if (typeof widget == "string") { var text = document.createTextNode(widget); widget = document.createElement("span"); widget.appendChild(text); widget.className = "CodeMirror-foldmarker"; + } else if (widget) { + widget = widget.cloneNode(true) } return widget; } // Clumsy backwards-compatible interface CodeMirror.newFoldFunction = function(rangeFinder, widget) { return function(cm, pos) { doFold(cm, pos, {rangeFinder: rangeFinder, widget: widget}); }; };
--- a/devtools/client/sourceeditor/codemirror/keymap/sublime.js +++ b/devtools/client/sourceeditor/codemirror/keymap/sublime.js @@ -160,16 +160,33 @@ if (!found || isSelectedRange(cm.listSelections(), cur.from(), cur.to())) return CodeMirror.Pass cm.addSelection(cur.from(), cur.to()); } if (fullWord) 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 return false } var mirror = "(){}[]"; function selectBetweenBrackets(cm) {
--- a/devtools/client/sourceeditor/codemirror/lib/codemirror.js +++ b/devtools/client/sourceeditor/codemirror/lib/codemirror.js @@ -6775,25 +6775,25 @@ var commands = { ); }, goLineStartSmart: function (cm) { return cm.extendSelectionsBy(function (range) { return lineStartSmart(cm, range.head); }, {origin: "+move", bias: 1} ); }, goLineEnd: function (cm) { return cm.extendSelectionsBy(function (range) { return lineEnd(cm, range.head.line); }, {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 }, sel_move); }, goLineUp: function (cm) { return cm.moveV(-1, "line"); }, goLineDown: function (cm) { return cm.moveV(1, "line"); }, goPageUp: function (cm) { return cm.moveV(-1, "page"); }, goPageDown: function (cm) { return cm.moveV(1, "page"); }, @@ -8338,16 +8338,18 @@ function addEditorMethods(CodeMirror) { { if (line.widgets[i].noHScroll) { regLineChange(this$1, lineNo, "widget"); break } } } ++lineNo }) this.curOp.forceUpdate = true signal(this, "refresh", this) }), 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 regChange(this) this.curOp.forceUpdate = true clearCaches(this) scrollToCoords(this, this.doc.scrollLeft, this.doc.scrollTop) updateGutterSpace(this) @@ -8990,19 +8992,16 @@ var TextareaInput = function(cm) { this.prevInput = "" // Flag that indicates whether we expect input to appear real soon // now (after some event like 'keypress' or 'input') and are // polling intensively. 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 }; TextareaInput.prototype.init = function (display) { var this$1 = this; @@ -9029,22 +9028,16 @@ TextareaInput.prototype.init = function cm.state.pasteIncoming = true input.fastPoll() }) function prepareCopyCut(e) { 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 { var ranges = copyableRanges(cm) setLastCopied({lineWise: true, text: ranges.text}) if (e.type == "cut") { cm.setSelections(ranges.ranges, null, sel_dontScroll) } else { @@ -9113,31 +9106,27 @@ TextareaInput.prototype.showSelection = this.wrapper.style.left = drawn.teLeft + "px" } }; // Reset the input to correspond to the selection (or to be empty, // 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 } } else if (!typing) { this.prevInput = this.textarea.value = "" if (ie && ie_version >= 9) { this.hasSelection = null } } - this.inaccurateSelection = minimal }; TextareaInput.prototype.getField = function () { return this.textarea }; TextareaInput.prototype.supportsTouch = function () { return false }; TextareaInput.prototype.focus = function () { if (this.cm.options.readOnly != "nocursor" && (!mobile || activeElt() != this.textarea)) { @@ -9478,13 +9467,13 @@ CodeMirror.defineExtension = function (n CodeMirror.defineDocExtension = function (name, func) { Doc.prototype[name] = func } CodeMirror.fromTextArea = fromTextArea addLegacyProps(CodeMirror) -CodeMirror.version = "5.27.4" +CodeMirror.version = "5.28.0" return CodeMirror; }))); \ No newline at end of file
--- a/devtools/client/sourceeditor/editor.js +++ b/devtools/client/sourceeditor/editor.js @@ -457,17 +457,19 @@ Editor.prototype = { }, /** * Replaces the current document with a new source document */ replaceDocument: function (doc) { let cm = editors.get(this); cm.swapDoc(doc); - this._updateLineNumberFormat(); + if (!Services.prefs.getBoolPref("devtools.debugger.new-debugger-frontend")) { + this._updateLineNumberFormat(); + } }, /** * Changes the value of a currently used highlighting mode. * See Editor.modes for the list of all supported modes. */ setMode: function (value) { this.setOption("mode", value); @@ -563,17 +565,20 @@ Editor.prototype = { lines.push(";; .... text is truncated due to the size"); } if (!done) { lines.push(";; .... possible error during wast conversion"); } // cm will try to split into lines anyway, saving memory value = { split: () => lines }; } - this._updateLineNumberFormat(); + + if (!Services.prefs.getBoolPref("devtools.debugger.new-debugger-frontend")) { + this._updateLineNumberFormat(); + } cm.setValue(value); this.resetIndentUnit(); }, /** * Reloads the state of the editor based on all current preferences.
--- a/devtools/client/sourceeditor/test/codemirror/search_test.js +++ b/devtools/client/sourceeditor/test/codemirror/search_test.js @@ -69,9 +69,17 @@ is(+new Date - t0 < 100) }) test("expandingCaseFold", function() { var doc = new CodeMirror.Doc("<b>İİ İİ</b>\n<b>uu uu</b>") run(doc, "</b>", 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) + }) })();
--- a/devtools/client/sourceeditor/test/codemirror/test.js +++ b/devtools/client/sourceeditor/test/codemirror/test.js @@ -1587,34 +1587,34 @@ testCM("lineWidgetChanged", function(cm) // If the widget is measured at a width much narrower than it is displayed at, the underHalf children will span two lines and break the test. // If the widget is measured at a width much wider than it is displayed at, the overHalf children will combine and break the test. // Note that this test only checks widgets where coverGutter is true, because these require extra styling to get the width right. // It may also be worthwhile to check this for non-coverGutter widgets. // Visually: // 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 // Bad (too narrow): // | ------------- display width ------------- | // | ------ widget-width when measured ----- | < -- uh oh // | | -- under-half -- | | // | | -- under-half -- | | < -- when measured, shoved to next line // | | --- over-half --- | | // | | --- over-half --- | | // Height: measured as 4 lines, more than expected . Will be displayed as 3 lines! // 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! var barelyUnderHalfWidthHtml = '<div style="display: inline-block; height: 1px; width: '+(285 - halfScrollbarWidth)+'px;"></div>'; var barelyOverHalfWidthHtml = '<div style="display: inline-block; height: 1px; width: '+(305 - halfScrollbarWidth)+'px;"></div>'; node.innerHTML = new Array(3).join(barelyUnderHalfWidthHtml) + new Array(3).join(barelyOverHalfWidthHtml); node.style.cssText = "background: yellow;font-size:0;line-height: " + (expectedWidgetHeight/expectedLinesInWidget) + "px;"; return node;
--- a/devtools/client/themes/rules.css +++ b/devtools/client/themes/rules.css @@ -447,17 +447,18 @@ height: 100%; } .ruleview-overridden-item:last-child:after { display: none; } .ruleview-grid, -.ruleview-swatch { +.ruleview-swatch, +.ruleview-shape { cursor: pointer; border-radius: 50%; width: 1em; height: 1em; vertical-align: middle; /* align the swatch with its value */ margin-top: -1px; margin-inline-end: 5px; @@ -465,16 +466,26 @@ position: relative; } .ruleview-grid { background: url("chrome://devtools/skin/images/grid.svg"); border-radius: 0; } +.ruleview-shape { + background: url("chrome://devtools/skin/images/tool-shadereditor.svg"); + border-radius: 0; + background-size: 1em; +} + +.ruleview-shape-point.active { + background-color: var(--rule-highlight-background-color); +} + .ruleview-colorswatch::before { content: ''; background-color: #eee; background-image: linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc), linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc); background-size: 12px 12px; background-position: 0 0, 6px 6px; position: absolute; @@ -591,17 +602,18 @@ } .ruleview-selectorhighlighter:hover { filter: url(images/filters.svg#checked-icon-state); } .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); } #ruleview-add-rule-button::before { background-image: url("chrome://devtools/skin/images/add.svg"); } #pseudo-class-panel-toggle::before {
--- a/devtools/client/themes/variables.css +++ b/devtools/client/themes/variables.css @@ -73,16 +73,18 @@ /* Tooltips */ --theme-tooltip-border: #d9e1e8; --theme-tooltip-background: rgba(255, 255, 255, .9); --theme-tooltip-shadow: rgba(155, 155, 155, 0.26); /* Command line */ --theme-command-line-image: url(chrome://devtools/skin/images/commandline-icon.svg#light-theme); --theme-command-line-image-focus: url(chrome://devtools/skin/images/commandline-icon.svg#light-theme-focus); + + --theme-codemirror-gutter-background: #f4f4f4; } :root.theme-dark { --theme-body-background: #393f4c; --theme-sidebar-background: #393f4c; --theme-contrast-background: #ffb35b; --theme-tab-toolbar-background: #272b35; @@ -139,16 +141,18 @@ /* Tooltips */ --theme-tooltip-border: #434850; --theme-tooltip-background: rgba(19, 28, 38, .9); --theme-tooltip-shadow: rgba(25, 25, 25, 0.76); /* Command line */ --theme-command-line-image: url(chrome://devtools/skin/images/commandline-icon.svg#dark-theme); --theme-command-line-image-focus: url(chrome://devtools/skin/images/commandline-icon.svg#dark-theme-focus); + + --theme-codemirror-gutter-background: #262b37; } :root.theme-firebug { --theme-body-background: #fff; --theme-sidebar-background: #fcfcfc; --theme-contrast-background: #e6b064; --theme-tab-toolbar-background: rgb(240, 240, 240) linear-gradient(rgba(255, 255, 255, 0.8), transparent); @@ -206,16 +210,18 @@ /* Toolbar buttons */ --toolbarbutton-background: transparent linear-gradient(rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.2)) no-repeat; --toolbarbutton-hover-background: transparent; --toolbarbutton-hover-border-color: var(--theme-splitter-color); --toolbarbutton-checked-background: linear-gradient(rgba(0, 0, 0, 0.1), transparent); --toolbarbutton-checked-color: var(--theme-body-color); --toolbarbutton-checked-border-color: var(--toolbarbutton-hover-border-color); + + --theme-codemirror-gutter-background: #ebeced; } :root.theme-firebug[platform="win"] { --theme-tab-toolbar-background: #d8eaf9 linear-gradient(rgba(253, 253, 253, 0.2), rgba(253, 253, 253, 0)); --theme-toolbar-background: #d8eaf9 linear-gradient(rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.2)); --theme-toolbar-tab-selected-background: rgb(247, 251, 254); --theme-splitter-color: #aabccf; }
--- a/devtools/server/actors/highlighters.css +++ b/devtools/server/actors/highlighters.css @@ -608,8 +608,12 @@ stroke: var(--highlighter-guide-color); shape-rendering: geometricPrecision; vector-effect: non-scaling-stroke; } :-moz-native-anonymous .shapes-markers { fill: var(--highlighter-marker-color); } + +:-moz-native-anonymous .shapes-marker-hover { + fill: var(--highlighter-guide-color); +}
--- a/devtools/server/actors/highlighters.js +++ b/devtools/server/actors/highlighters.js @@ -454,16 +454,19 @@ exports.CustomHighlighterActor = protoco } // The assumption is that all custom highlighters need the canvasframe // container to append their elements, so if this is a XUL window, bail out. if (!isXUL(this._inspector.tabActor.window)) { 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"); } }, get conn() { return this._inspector && this._inspector.conn; @@ -507,21 +510,31 @@ exports.CustomHighlighterActor = protoco */ hide: function () { if (this._highlighter) { this._highlighter.hide(); } }, /** + * 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; } if (this._highlighterEnv) { this._highlighterEnv.destroy(); this._highlighterEnv = null; }
--- a/devtools/server/actors/highlighters/shapes.js +++ b/devtools/server/actors/highlighters/shapes.js @@ -10,43 +10,49 @@ const { setIgnoreLayoutChanges, getCurre const { AutoRefreshHighlighter } = require("./auto-refresh"); const { getDistance, clickedOnEllipseEdge, distanceToLine, 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 const LINE_CLICK_WIDTH = 5; const DOM_EVENTS = ["mousedown", "mousemove", "mouseup", "dblclick"]; const _dragging = Symbol("shapes/dragging"); /** * The ShapesHighlighter draws an outline shapes in the page. * The idea is to have something that is able to wrap complex shapes for css properties * such as shape-outside/inside, clip-path but also SVG elements. */ 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)); + this.onPageHide = this.onPageHide.bind(this); let { pageListenerTarget } = this.highlighterEnv; DOM_EVENTS.forEach(event => pageListenerTarget.addEventListener(event, this)); + pageListenerTarget.addEventListener("pagehide", this.onPageHide); } _buildMarkup() { let container = createNode(this.win, { attributes: { "class": "highlighter-container" } }); @@ -115,16 +121,27 @@ class ShapesHighlighter extends AutoRefr parent: mainSvg, attributes: { "id": "markers", "class": "markers", }, 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; } get currentDimensions() { let { top, left, width, height } = this.currentQuads[this.referenceBox][0].bounds; // If an SVG element has a stroke, currentQuads will return the stroke bounding box. // However, clip-path always uses the object bounding box unless "stroke-box" is @@ -194,16 +211,17 @@ class ShapesHighlighter extends AutoRefr if (this.property === "shape-outside") { this.currentNode.style.setProperty("width", this[_dragging].origWidth); } this[_dragging] = null; } break; case "mousemove": if (!this[_dragging]) { + this._handleMouseMoveNotDragging(pageX, pageY); return; } event.stopPropagation(); event.preventDefault(); let { point } = this[_dragging]; if (this.shapeType === "polygon") { this._handlePolygonMove(pageX, pageY); @@ -213,17 +231,17 @@ class ShapesHighlighter extends AutoRefr this._handleEllipseMove(point, pageX, pageY); } else if (this.shapeType === "inset") { this._handleInsetMove(point, pageX, pageY); } break; 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; } this._deletePolygonPoint(index); } break; @@ -233,17 +251,17 @@ class ShapesHighlighter extends AutoRefr /** * Handle a click when highlighting a polygon. * @param {any} pageX the x coordinate of the click * @param {any} pageY the y coordinate of the click */ _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; } let [x, y] = this.coordUnits[point]; let xComputed = this.coordinates[point][0] / 100 * width; let yComputed = this.coordinates[point][1] / 100 * height; let unitX = getUnit(x); @@ -266,66 +284,73 @@ class ShapesHighlighter extends AutoRefr */ _handlePolygonMove(pageX, pageY) { let { point, unitX, unitY, valueX, valueY, ratioX, ratioY, x, y } = this[_dragging]; let deltaX = (pageX - x) * ratioX; let deltaY = (pageY - y) * ratioY; 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}` : `polygon(${polygonDef})`; this.currentNode.style.setProperty(this.property, polygonDef, "important"); } /** * Set the inline style of the polygon, adding a new point. * @param {Number} after the index of the point that the new point should be added after * @param {Number} x the x coordinate of the new point * @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"); } /** * Set the inline style of the polygon, deleting the given point. * @param {Number} point the index of the point to delete */ _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"); } /** * Handle a click when highlighting a circle. * @param {any} pageX the x coordinate of the click * @param {any} pageY the y coordinate of the click */ _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; } if (point === "center") { let { cx, cy } = this.coordUnits; let cxComputed = this.coordinates.cx / 100 * width; let cyComputed = this.coordinates.cy / 100 * height; @@ -396,17 +421,17 @@ class ShapesHighlighter extends AutoRefr /** * Handle a click when highlighting an ellipse. * @param {any} pageX the x coordinate of the click * @param {any} pageY the y coordinate of the click */ _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; } if (point === "center") { let { cx, cy } = this.coordUnits; let cxComputed = this.coordinates.cx / 100 * width; let cyComputed = this.coordinates.cy / 100 * height; @@ -495,17 +520,17 @@ class ShapesHighlighter extends AutoRefr /** * Handle a click when highlighting an inset. * @param {any} pageX the x coordinate of the click * @param {any} pageY the y coordinate of the click */ _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; } let value = this.coordUnits[point]; let size = (point === "left" || point === "right") ? width : height; let computedValue = this.coordinates[point] / 100 * size; let unit = getUnit(value); @@ -548,16 +573,134 @@ class ShapesHighlighter extends AutoRefr `inset(${top} ${right} ${bottom} ${left} round ${round})` : `inset(${top} ${right} ${bottom} ${left})`; insetDef += (this.geometryBox) ? this.geometryBox : ""; 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. * @param {Number} pageX the x coordinate on the page * @param {Number} pageY the y coordinate on the page * @returns {Object} object of form {percentX, percentY}, which are the x/y coords * in percentages relative to the element. */ @@ -585,23 +728,23 @@ class ShapesHighlighter extends AutoRefr x = x * width / 100; y = y * height / 100; x += left; y += top; return { x, y }; } /** - * 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); let clickRadiusX = BASE_MARKER_SIZE / zoom * 100 / width; let clickRadiusY = BASE_MARKER_SIZE / zoom * 100 / height; for (let [index, coord] of coordinates.entries()) { let [x, y] = coord; @@ -640,23 +783,23 @@ class ShapesHighlighter extends AutoRefr let [newX, newY] = projection(x1, y1, x2, y2, pageX, pageY); this._addPolygonPoint(i, newX, newY); return; } } } /** - * 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); let clickRadiusX = BASE_MARKER_SIZE / zoom * 100 / width; let clickRadiusY = BASE_MARKER_SIZE / zoom * 100 / height; if (clickedOnPoint(pageX, pageY, cx, cy, clickRadiusX, clickRadiusY)) { return "center"; @@ -668,24 +811,24 @@ class ShapesHighlighter extends AutoRefr clickedOnPoint(pageX, pageY, cx + rx, cy, clickRadiusX, clickRadiusY)) { return "radius"; } return ""; } /** - * 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); let clickRadiusX = BASE_MARKER_SIZE / zoom * 100 / width; let clickRadiusY = BASE_MARKER_SIZE / zoom * 100 / height; if (clickedOnPoint(pageX, pageY, cx, cy, clickRadiusX, clickRadiusY)) { return "center"; @@ -698,23 +841,23 @@ class ShapesHighlighter extends AutoRefr if (clickedOnPoint(pageX, pageY, cx, cy + ry, clickRadiusX, clickRadiusY)) { return "ry"; } return ""; } /** - * 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; let clickWidthX = LINE_CLICK_WIDTH * 100 / width; let clickWidthY = LINE_CLICK_WIDTH * 100 / height; let clickRadiusX = BASE_MARKER_SIZE / zoom * 100 / width; let clickRadiusY = BASE_MARKER_SIZE / zoom * 100 / height; let centerX = (left + (100 - right)) / 2; @@ -806,33 +949,44 @@ class ShapesHighlighter extends AutoRefr * Parses the definition of the CSS polygon() function and returns its points, * converted to percentages. * @param {String} definition the arguments of the polygon() function * @returns {Array} an array of the points of the polygon, with all values * evaluated and converted to percentages */ 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)); }); } /** * Parse the raw (non-computed) definition of the CSS polygon. * @returns {Array} an array of the points of the polygon, with units preserved. */ polygonRawPoints() { let definition = getDefinedShapeProperties(this.currentNode, this.property); if (definition === this.rawDefinition) { return this.coordUnits; } 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, " "); }); }); } /** @@ -1069,16 +1223,17 @@ class ShapesHighlighter extends AutoRefr this.getElement("polygon").hasAttribute("hidden") && this.getElement("rect").hasAttribute("hidden"); } /** * Show the highlighter on a given node */ _show() { + this.hoveredPoint = this.options.hoverPoint; return this._update(); } /** * The AutoRefreshHighlighter's _hasMoved method returns true only if the element's * quads have changed. Override it so it also returns true if the element's shape has * changed (which can happen when you change a CSS properties for instance). */ @@ -1145,16 +1300,18 @@ class ShapesHighlighter extends AutoRefr } else if (this.shapeType === "circle") { this._updateCircleShape(width, height, zoom); } else if (this.shapeType === "ellipse") { this._updateEllipseShape(width, height, zoom); } else if (this.shapeType === "inset") { this._updateInsetShape(width, height, zoom); } + this._handleMarkerHover(this.hoveredPoint); + let { width: winWidth, height: winHeight } = this._winDimensions; root.removeAttribute("hidden"); root.setAttribute("style", `position:absolute; width:${winWidth}px;height:${winHeight}px; overflow:hidden`); setIgnoreLayoutChanges(false, this.highlighterEnv.window.document.documentElement); return true; @@ -1257,16 +1414,24 @@ class ShapesHighlighter extends AutoRefr _hide() { setIgnoreLayoutChanges(true); this._hideShapes(); this.getElement("markers").setAttribute("d", ""); 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(); + } + } } /** * Get the "raw" (i.e. non-computed) shape definition on the given node. * @param {nsIDOMNode} node the node to analyze * @param {String} property the CSS property for which a value should be retrieved. * @returns {String} the value of the given CSS property on the given node. */
--- a/devtools/shared/css/properties-db.js +++ b/devtools/shared/css/properties-db.js @@ -70,16 +70,18 @@ exports.COLOR_TAKING_FUNCTIONS = ["linea * Functions that accept an angle argument. This list can be manually edited. */ exports.ANGLE_TAKING_FUNCTIONS = ["linear-gradient", "-moz-linear-gradient", "repeating-linear-gradient", "-moz-repeating-linear-gradient", "rotate", "rotateX", "rotateY", "rotateZ", "rotate3d", "skew", "skewX", "skewY", "hue-rotate"]; +exports.BASIC_SHAPE_FUNCTIONS = ["polygon", "circle", "ellipse", "inset"]; + /** * The list of all CSS Pseudo Elements. * * This list can be updated with `mach devtools-css-db`. */ exports.PSEUDO_ELEMENTS = db.PSEUDO_ELEMENTS; /**
--- a/devtools/shared/specs/highlighters.js +++ b/devtools/shared/specs/highlighters.js @@ -33,16 +33,23 @@ const highlighterSpec = generateActorSpe } }); exports.highlighterSpec = highlighterSpec; const customHighlighterSpec = generateActorSpec({ typeName: "customhighlighter", + events: { + "highlighter-event": { + type: "highlighter-event", + data: Arg(0, "json") + } + }, + methods: { release: { release: true }, show: { request: { node: Arg(0, "domnode"), options: Arg(1, "nullable:json")
--- a/docshell/base/crashtests/432114-2.html +++ b/docshell/base/crashtests/432114-2.html @@ -4,13 +4,18 @@ </head> <body> <script> window.addEventListener("DOMNodeRemoved", function() { setTimeout(function() { document.documentElement.removeAttribute("class"); }, 0); }); + var iframe = document.getElementById("content"); + iframe.onload=function() { + dump("iframe onload\n"); + console.log("iframe onload"); + }; </script> -<iframe id="content" src="data:application/xhtml+xml;charset=utf-8,%3Chtml%20xmlns%3D%22http%3A//www.w3.org/1999/xhtml%22%3E%0A%3Cframeset%20contenteditable%3D%22true%22/%3E%0A%3Cscript%3E%0Afunction%20doExecCommand%28%29%7B%0Adocument.execCommand%28%27formatBlock%27%2C%20false%2C%20%27p%27%29%3B%0A%7D%0AsetTimeout%28doExecCommand%2C100%29%3B%0Awindow.addEventListener%28%27DOMNodeRemoved%27%2C%20function%28%29%20%7Bwindow.frameElement.parentNode.removeChild%28window.frameElement%29%3B%7D%2C%20true%29%3B%0A%3C/script%3E%0A%3C/html%3E" style="width:1000px;height: 200px;"></iframe> +<iframe id="content" src="file_432114-2.xhtml" style="width:1000px;height: 200px;"></iframe> </body> </html>
--- a/docshell/base/crashtests/914521.html +++ b/docshell/base/crashtests/914521.html @@ -15,19 +15,27 @@ function f() } window.addEventListener("popstate", spin); window.close(); window.location = "#c"; finish(); } +function init() +{ + SpecialPowers.pushPrefEnv({"set": [["security.data_uri.unique_opaque_origin", false]]}, start); +} + function start() { var html = "<script>" + f + "<\/script><body onload=f()>"; var win = window.open("data:text/html," + encodeURIComponent(html), null, "width=300,height=300"); - win.finish = function() { document.documentElement.removeAttribute("class"); }; + win.finish = function() { + SpecialPowers.clearUserPref("security.data_uri.unique_opaque_origin"); + document.documentElement.removeAttribute("class"); + }; } </script> </head> -<body onload="start();"></body> +<body onload="init();"></body> </html>
new file mode 100644 --- /dev/null +++ b/docshell/base/crashtests/file_432114-2.xhtml @@ -0,0 +1,1 @@ +<html xmlns='http://www.w3.org/1999/xhtml'><frameset contenteditable='true'/><script>function doExecCommand(){dump("doExecCommand\n");document.execCommand('formatBlock', false, 'p');}setTimeout(doExecCommand,100); window.addEventListener('DOMNodeRemoved', function() {window.frameElement.parentNode.removeChild(window.frameElement);}, true);</script></html>
--- a/docshell/base/nsDocShell.cpp +++ b/docshell/base/nsDocShell.cpp @@ -10285,18 +10285,21 @@ nsDocShell::InternalLoad(nsIURI* aURI, nsCOMPtr<nsIDocShellTreeItem> parent = GetParentDocshell(); if (parent) { nsCOMPtr<nsIDocument> doc = parent->GetDocument(); if (doc) { doc->TryCancelFrameLoaderInitialization(this); } } + 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) { NS_WARNING("blocked external chrome: url -- use '--chrome' option"); return NS_ERROR_FAILURE; } // clear the decks to prevent context bleed-through (bug 298255) @@ -10789,17 +10792,18 @@ nsDocShell::InternalLoad(nsIURI* aURI, attrs.SetFirstPartyDomain(isTopLevelDoc, aURI); net::PredictorLearn(aURI, nullptr, nsINetworkPredictor::LEARN_LOAD_TOPLEVEL, attrs); net::PredictorPredict(aURI, nullptr, nsINetworkPredictor::PREDICT_LOAD, attrs, nullptr); nsCOMPtr<nsIRequest> 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, aFileName, aPostData, aHeadersData, aFirstParty, aDocShell, getter_AddRefs(req), (aFlags & INTERNAL_LOAD_FLAGS_FIRST_LOAD) != 0, (aFlags & INTERNAL_LOAD_FLAGS_BYPASS_CLASSIFIER) != 0, (aFlags & INTERNAL_LOAD_FLAGS_FORCE_ALLOW_COOKIES) != 0, @@ -10870,16 +10874,17 @@ nsDocShell::GetInheritedPrincipal(bool a return nullptr; } nsresult nsDocShell::DoURILoad(nsIURI* aURI, nsIURI* aOriginalURI, Maybe<nsCOMPtr<nsIURI>> const& aResultPrincipalURI, bool aLoadReplace, + bool aLoadFromExternal, nsIURI* aReferrerURI, bool aSendReferrer, uint32_t aReferrerPolicy, nsIPrincipal* aTriggeringPrincipal, nsIPrincipal* aPrincipalToInherit, const char* aTypeHint, const nsAString& aFileName, nsIInputStream* aPostData, @@ -11022,35 +11027,44 @@ nsDocShell::DoURILoad(nsIURI* aURI, nsCOMPtr<nsILoadInfo> loadInfo = (aContentPolicyType == nsIContentPolicy::TYPE_DOCUMENT) ? new LoadInfo(loadingWindow, aTriggeringPrincipal, securityFlags) : 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; } } if (aPrincipalToInherit) { loadInfo->SetPrincipalToInherit(aPrincipalToInherit); } // We have to do this in case our OriginAttributes are different from the
--- a/docshell/base/nsDocShell.h +++ b/docshell/base/nsDocShell.h @@ -372,16 +372,17 @@ protected: // and the contents of aSrcdoc will be loaded instead of aURI. // aOriginalURI will be set as the originalURI on the channel that does the // load. If aOriginalURI is null, aURI will be set as the originalURI. // If aLoadReplace is true, LOAD_REPLACE flag will be set to the nsIChannel. nsresult DoURILoad(nsIURI* aURI, nsIURI* aOriginalURI, mozilla::Maybe<nsCOMPtr<nsIURI>> const& aResultPrincipalURI, bool aLoadReplace, + bool aLoadFromExternal, nsIURI* aReferrer, bool aSendReferrer, uint32_t aReferrerPolicy, nsIPrincipal* aTriggeringPrincipal, nsIPrincipal* aPrincipalToInherit, const char* aTypeHint, const nsAString& aFileName, nsIInputStream* aPostData,
new file mode 100644 --- /dev/null +++ b/dom/base/crashtests/1383478.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<html lang="en" dir="ltr"> + <head> + <meta charset="utf-8" /> + <title>This browser is crashing</title> + </head> + <body> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> +<Object class="crashDroid" /> + </body> +</html>
--- a/dom/base/crashtests/504224.html +++ b/dom/base/crashtests/504224.html @@ -1,22 +1,23 @@ <html class="reftest-wait"> <head> <title>Crash [@ nsFocusManager::GetCommonAncestor], part 2</title> </head> <body> -<iframe src="data:text/html;charset=utf-8,%3Chtml%3E%3Chead%3E%3C/head%3E%0A%3Cbody%20onunload%3D%22window.frameElement.parentNode.removeChild%28window.frameElement%29%22%20tabindex%3D%221%22%3E%0A%3Cscript%3E%0Adocument.body.focus%28%29%3B%0A%3C/script%3E%0A%3C/body%3E%0A%3C/html%3E" id="content"></iframe> +<iframe src="file_504224.html" id="content"></iframe> <script> var src=document.getElementById('src'); setInterval(function() { if (!document.getElementById('content')) { var x=document.createElement('iframe'); x.src=src; x.id = 'content'; document.body.appendChild(x); setTimeout(function() { window.focus(); document.documentElement.removeAttribute('class'); }, 100); - } else + } else { window.frames[0].location.reload(); + } }, 500); </script> </body> </html>
--- a/dom/base/crashtests/593302-1.html +++ b/dom/base/crashtests/593302-1.html @@ -19,11 +19,11 @@ s.id = i; return s; } function remove(n) { n.remove(); } </script> </head> <body onload="boom();"> - <iframe id="iframe" src="data:text/html,S"></iframe> + <iframe id="iframe" srcdoc="<html>S</html>"></iframe> </body> </html>
--- a/dom/base/crashtests/851353-1.html +++ b/dom/base/crashtests/851353-1.html @@ -15,11 +15,11 @@ } doc.open(); setTimeout(runnable, 0); } </script> </head> <body onload='start()'> - <iframe src="data:text/html,<meta charset=UTF-8><body><video src=http://localhost:8080/ controls=true loop=true autoplay=true autobuffer=false></video>"></iframe> + <iframe srcdoc="<body><video src=http://localhost:8080/ controls=true loop=true autoplay=true autobuffer=false></video>"></iframe> </body> </html>
--- a/dom/base/crashtests/crashtests.list +++ b/dom/base/crashtests/crashtests.list @@ -214,8 +214,9 @@ pref(dom.IntersectionObserver.enabled,tr load 1370072.html pref(clipboard.autocopy,true) load 1370737.html pref(dom.IntersectionObserver.enabled,true) load 1370968.html load 1377826.html load structured_clone_container_throws.html HTTP(..) load xhr_abortinprogress.html load xhr_empty_datauri.html load xhr_html_nullresponse.html +load 1383478.html
new file mode 100644 --- /dev/null +++ b/dom/base/crashtests/file_504224.html @@ -0,0 +1,7 @@ +<html><head></head> + <body onunload="window.frameElement.parentNode.removeChild(window.frameElement)" tabindex="1"> + <script> +document.body.focus(); + </script> + </body> +</html>
--- a/dom/base/nsDOMNavigationTiming.h +++ b/dom/base/nsDOMNavigationTiming.h @@ -79,21 +79,17 @@ public: return mLoadEventStart; } DOMTimeMilliSec GetLoadEventEnd() const { return mLoadEventEnd; } DOMTimeMilliSec GetTimeToNonBlankPaint() const { - if (mNonBlankPaintTimeStamp.IsNull()) { - return 0; - } - - return TimeStampToDOMHighRes(mNonBlankPaintTimeStamp); + return TimeStampToDOM(mNonBlankPaintTimeStamp); } enum class DocShellState : uint8_t { eActive, eInactive }; void NotifyNavigationStart(DocShellState aDocShellState);
--- a/dom/base/nsGlobalWindow.cpp +++ b/dom/base/nsGlobalWindow.cpp @@ -4420,16 +4420,21 @@ void nsPIDOMWindowInner::SyncStateFromParentWindow() { nsGlobalWindow::Cast(this)->SyncStateFromParentWindow(); } bool nsPIDOMWindowInner::IsPlayingAudio() { + for (uint32_t i = 0; i < mAudioContexts.Length(); i++) { + if (mAudioContexts[i]->IsRunning()) { + return true; + } + } RefPtr<AudioChannelService> acs = AudioChannelService::Get(); if (!acs) { return false; } auto outer = GetOuterWindow(); if (!outer) { // We've been unlinked and are about to die. Not a good time to pretend to // be playing audio.
--- a/dom/base/nsObjectLoadingContent.cpp +++ b/dom/base/nsObjectLoadingContent.cpp @@ -3112,31 +3112,42 @@ nsObjectLoadingContent::LoadFallback(Fal nsTArray<nsINodeList*> childNodes; if ((thisContent->IsHTMLElement(nsGkAtoms::object) || thisContent->IsHTMLElement(nsGkAtoms::applet)) && (aType == eFallbackUnsupported || aType == eFallbackDisabled || aType == eFallbackBlocklisted || aType == eFallbackAlternate)) { - for (nsIContent* child = thisContent->GetFirstChild(); child; - child = child->GetNextNode(thisContent)) { + for (nsIContent* child = thisContent->GetFirstChild(); child; ) { + // When we advance to our next child, we don't want to traverse subtrees + // under descendant <object> and <embed> elements; those will handle + // those subtrees themselves if they end up falling back. + bool skipChildDescendants = false; if (aType != eFallbackAlternate && !child->IsHTMLElement(nsGkAtoms::param) && nsStyleUtil::IsSignificantChild(child, true, false)) { aType = eFallbackAlternate; } if (thisIsObject) { if (child->IsHTMLElement(nsGkAtoms::embed)) { HTMLSharedObjectElement* embed = static_cast<HTMLSharedObjectElement*>(child); embed->StartObjectLoad(true, true); + skipChildDescendants = true; } else if (auto object = HTMLObjectElement::FromContent(child)) { object->StartObjectLoad(true, true); + skipChildDescendants = true; } } + + if (skipChildDescendants) { + child = child->GetNextNonChildNode(thisContent); + } else { + child = child->GetNextNode(thisContent); + } } } mFallbackType = aType; // Notify if (!aNotify) { return; // done
--- a/dom/base/test/browser.ini +++ b/dom/base/test/browser.ini @@ -17,16 +17,17 @@ support-files = file_use_counter_outer.html file_use_counter_svg_getElementById.svg file_use_counter_svg_currentScale.svg file_use_counter_svg_fill_pattern_definition.svg file_use_counter_svg_fill_pattern.svg 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 [browser_blocking_image.js] [browser_bug593387.js] [browser_bug902350.js] tags = mcb [browser_bug1011748.js]
--- a/dom/base/test/browser_timeout_throttling_with_audio_playback.js +++ b/dom/base/test/browser_timeout_throttling_with_audio_playback.js @@ -6,17 +6,17 @@ if (!gMultiProcessBrowser) { } const kBaseURI = "http://mochi.test:8888/browser/dom/base/test/empty.html"; const kPluginJS = "chrome://mochitests/content/browser/dom/base/test/plugin.js"; 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 // treated the same as a foreground tab as far as timeout throttling is concerned. // So we use a 100,000 second minimum timeout value for background tabs. This // means that in case the test fails, it will time out in practice, but just for // sanity the test condition ensures that the observed timeout delay falls in // this range.
--- a/dom/base/test/file_webaudioLoop.html +++ b/dom/base/test/file_webaudioLoop.html @@ -3,29 +3,20 @@ var ac = new AudioContext(); var runningPromise = new Promise(resolve => { ac.onstatechange = event => { if (ac.state == "running") { resolve(); } }; }); -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() { runningPromise.then(() => { suspendPromise = ac.suspend(); }); }
new file mode 100644 --- /dev/null +++ b/dom/base/test/file_webaudio_startstop.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<script> +var ac = new AudioContext(); +var runningPromise = new Promise(resolve => { + ac.onstatechange = event => { + if (ac.state == "running") { + resolve(); + } + }; +}); + +var osc = ac.createOscillator(); +osc.connect(ac.destination); +osc.start(0); +osc.stop(osc.context.currentTime + 2.0); + +var suspendPromise; +function suspendAC() { + runningPromise.then(() => { + suspendPromise = ac.suspend(); + }); +} + +var resumePromise; +function resumeAC() { + suspendPromise.then(() => { + resumePromise = ac.resume(); + }); +} + +function closeAC() { + resumePromise.then(() => { + ac.close(); + }); +} +</script>
--- a/dom/base/test/mochitest.ini +++ b/dom/base/test/mochitest.ini @@ -205,16 +205,17 @@ support-files = viewport_helpers.js w3element_traversal.svg wholeTexty-helper.xml referrerHelper.js img_referrer_testserver.sjs file_audioLoop.html file_webaudioLoop.html file_webaudioLoop2.html + file_webaudio_startstop.html file_pluginAudio.html file_pluginAudioNonAutoStart.html noaudio.webm referrer_helper.js referrer_testserver.sjs script_postmessages_fileList.js iframe_postMessages.html test_anonymousContent_style_csp.html^headers^
--- a/dom/indexedDB/ActorsParent.cpp +++ b/dom/indexedDB/ActorsParent.cpp @@ -2,16 +2,17 @@ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* 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/. */ #include "ActorsParent.h" #include <algorithm> +#include <stdint.h> // UINTPTR_MAX, uintptr_t #include "FileInfo.h" #include "FileManager.h" #include "IDBObjectStore.h" #include "IDBTransaction.h" #include "IndexedDatabase.h" #include "IndexedDatabaseInlines.h" #include "IndexedDatabaseManager.h" #include "js/StructuredClone.h" @@ -849,16 +850,21 @@ ReadCompressedIndexDataValuesFromBlob(co MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aBlobData); MOZ_ASSERT(aBlobDataLength); MOZ_ASSERT(aIndexValues.IsEmpty()); 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; while (blobDataIter < blobDataEnd) { int64_t indexId; bool unique; ReadCompressedIndexId(&blobDataIter, blobDataEnd, &indexId, &unique); @@ -868,17 +874,18 @@ ReadCompressedIndexDataValuesFromBlob(co } // Read key buffer length. const uint64_t keyBufferLength = ReadCompressedNumber(&blobDataIter, blobDataEnd); 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; } nsCString keyBuffer(reinterpret_cast<const char*>(blobDataIter), uint32_t(keyBufferLength)); blobDataIter += keyBufferLength; @@ -886,17 +893,18 @@ ReadCompressedIndexDataValuesFromBlob(co // Read sort key buffer length. const uint64_t sortKeyBufferLength = ReadCompressedNumber(&blobDataIter, blobDataEnd); 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; } nsCString sortKeyBuffer(reinterpret_cast<const char*>(blobDataIter), uint32_t(sortKeyBufferLength)); blobDataIter += sortKeyBufferLength;
--- a/dom/ipc/ContentChild.cpp +++ b/dom/ipc/ContentChild.cpp @@ -1617,17 +1617,20 @@ ContentChild::RecvSetProcessSandbox(cons for (const nsACString& callNrString : extraSyscalls.Split(',')) { nsresult rv; int callNr = PromiseFlatCString(callNrString).ToInteger(&rv); if (NS_SUCCEEDED(rv)) { syscallWhitelist.push_back(callNr); } } } - 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(); #elif defined(XP_MACOSX) sandboxEnabled = StartMacOSContentSandbox(); #endif #if defined(MOZ_CRASHREPORTER)
--- a/dom/ipc/ContentParent.cpp +++ b/dom/ipc/ContentParent.cpp @@ -2430,18 +2430,19 @@ ContentParent::InitInternal(ProcessPrior // should be changed so that it is required to restart firefox for the change // of value to take effect. shouldSandbox = (GetEffectiveContentSandboxLevel() > 0) && !PR_GetEnv("MOZ_DISABLE_CONTENT_SANDBOX"); #ifdef XP_LINUX if (shouldSandbox) { MOZ_ASSERT(!mSandboxBroker); + bool isFileProcess = mRemoteType.EqualsLiteral(FILE_REMOTE_TYPE); UniquePtr<SandboxBroker::Policy> policy = - sSandboxBrokerPolicyFactory->GetContentPolicy(Pid()); + sSandboxBrokerPolicyFactory->GetContentPolicy(Pid(), isFileProcess); if (policy) { brokerFd = FileDescriptor(); mSandboxBroker = SandboxBroker::Create(Move(policy), Pid(), brokerFd); if (!mSandboxBroker) { KillHard("SandboxBroker::Create failed"); return; } MOZ_ASSERT(static_cast<const FileDescriptor&>(brokerFd).IsValid());
--- a/dom/locales/en-US/chrome/security/security.properties +++ b/dom/locales/en-US/chrome/security/security.properties @@ -76,8 +76,11 @@ WeakCipherSuiteWarning=This site uses th #XCTO: nosniff # LOCALIZATION NOTE: Do not translate "X-Content-Type-Options: nosniff". MimeTypeMismatch=The resource from “%1$S” was blocked due to MIME type mismatch (X-Content-Type-Options: nosniff). # LOCALIZATION NOTE: Do not translate "X-Content-Type-Options" and also do not trasnlate "nosniff". 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”)
--- a/dom/media/gmp/GMPChild.cpp +++ b/dom/media/gmp/GMPChild.cpp @@ -273,16 +273,17 @@ GMPChild::RecvPreloadLibs(const nsCStrin // Pre-load DLLs that need to be used by the EME plugin but that can't be // loaded after the sandbox has started // Items in this must be lowercase! static const char *const whitelist[] = { "dxva2.dll", // Get monitor information "evr.dll", // MFGetStrideForBitmapInfoHeader "mfplat.dll", // MFCreateSample, MFCreateAlignedMemoryBuffer, MFCreateMediaType "msmpeg2vdec.dll", // H.264 decoder + "psapi.dll", // For GetMappedFileNameW, see bug 1383611 }; nsTArray<nsCString> libs; SplitAt(", ", aLibs, libs); for (nsCString lib : libs) { ToLowerCase(lib); for (const char* whiteListedLib : whitelist) { if (lib.EqualsASCII(whiteListedLib)) {
--- a/dom/media/gmp/GMPParent.cpp +++ b/dom/media/gmp/GMPParent.cpp @@ -786,17 +786,19 @@ GMPParent::ParseChromiumManifest(const n if (mDisplayName.EqualsASCII("clearkey")) { kEMEKeySystem = kEMEKeySystemClearkey; #if XP_WIN mLibs = NS_LITERAL_CSTRING("dxva2.dll, msmpeg2vdec.dll, evr.dll, mfh264dec.dll, mfplat.dll"); #endif } 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__); } GMPCapability video; nsCString codecsString = NS_ConvertUTF16toUTF8(m.mX_cdm_codecs);
--- a/dom/media/webaudio/AudioContext.cpp +++ b/dom/media/webaudio/AudioContext.cpp @@ -487,16 +487,22 @@ AudioListener* AudioContext::Listener() { if (!mListener) { mListener = new AudioListener(this); } return mListener; } +bool +AudioContext::IsRunning() const +{ + return mAudioContextState == AudioContextState::Running; +} + already_AddRefed<Promise> AudioContext::DecodeAudioData(const ArrayBuffer& aBuffer, const Optional<OwningNonNull<DecodeSuccessCallback> >& aSuccessCallback, const Optional<OwningNonNull<DecodeErrorCallback> >& aFailureCallback, ErrorResult& aRv) { nsCOMPtr<nsIGlobalObject> parentObject = do_QueryInterface(GetParentObject()); RefPtr<Promise> promise;
--- a/dom/media/webaudio/AudioContext.h +++ b/dom/media/webaudio/AudioContext.h @@ -176,16 +176,17 @@ public: bool ShouldSuspendNewStream() const { return mSuspendCalled; } double CurrentTime() const; 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) // thread(s). To avoid having to match the calls and asychronous result when // the operation is completed, we keep a reference to the promises on the main // thread, and then send the promises pointers down the MSG thread, as a void* // (to make it very clear that the pointer is to merely be treated as an ID). // When back on the main thread, we can resolve or reject the promise, by
--- a/dom/plugins/ipc/PluginInstanceChild.cpp +++ b/dom/plugins/ipc/PluginInstanceChild.cpp @@ -2334,57 +2334,67 @@ PluginInstanceChild::SetupFlashMsgThrott } else { // Already setup through quirks and the subclass. return; } } WNDPROC -PluginInstanceChild::FlashThrottleAsyncMsg::GetProc() +PluginInstanceChild::FlashThrottleMsg::GetProc() { if (mInstance) { return mWindowed ? mInstance->mPluginWndProc : mInstance->mWinlessThrottleOldWndProc; } return nullptr; } 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; // deliver the event to flash CallWindowProc(GetProc(), GetWnd(), GetMsg(), GetWParam(), GetLParam()); return NS_OK; } +nsresult +PluginInstanceChild::FlashThrottleMsg::Cancel() +{ + MOZ_ASSERT(mInstance); + mInstance = nullptr; + return NS_OK; +} + void PluginInstanceChild::FlashThrottleMessage(HWND aWnd, UINT aMsg, WPARAM aWParam, LPARAM aLParam, bool isWindowed) { - // We reuse ChildAsyncCall so we get the cancelation work - // that's done in Destroy. - RefPtr<FlashThrottleAsyncMsg> 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<FlashThrottleMsg> task = + new FlashThrottleMsg(this, aWnd, aMsg, aWParam, aLParam, isWindowed); + + mPendingFlashThrottleMsgs.AppendElement(task); + MessageLoop::current()->PostDelayedTask(task.forget(), kFlashWMUSERMessageThrottleDelayMs); } #endif // OS_WIN mozilla::ipc::IPCResult PluginInstanceChild::AnswerSetPluginFocus() @@ -4257,16 +4267,21 @@ PluginInstanceChild::Destroy() // PluginInstanceDestroyed call above. mCachedWindowActor = nullptr; mCachedElementActor = nullptr; #if defined(OS_WIN) 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 // in-process behavior. for (uint32_t i = 0; i < mPendingAsyncCalls.Length(); ++i) mPendingAsyncCalls[i]->Cancel(); mPendingAsyncCalls.Clear();
--- a/dom/plugins/ipc/PluginInstanceChild.h +++ b/dom/plugins/ipc/PluginInstanceChild.h @@ -343,41 +343,41 @@ private: static BOOL WINAPI ImmReleaseContextProc(HWND aWND, HIMC aIMC); static LONG WINAPI ImmGetCompositionStringProc(HIMC aIMC, DWORD aIndex, LPVOID aBuf, DWORD aLen); static BOOL WINAPI ImmSetCandidateWindowProc(HIMC hIMC, LPCANDIDATEFORM plCandidate); 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), mLParam(aLParam), mWindowed(isWindowed) {} NS_IMETHOD Run() override; + nsresult Cancel() override; WNDPROC GetProc(); HWND GetWnd() { return mWnd; } UINT GetMsg() { return mMsg; } WPARAM GetWParam() { return mWParam; } LPARAM GetLParam() { return mLParam; } private: + PluginInstanceChild* mInstance; HWND mWnd; UINT mMsg; WPARAM mWParam; LPARAM mLParam; bool mWindowed; }; bool ShouldPostKeyMessage(UINT message, WPARAM wParam, LPARAM lParam); @@ -445,16 +445,19 @@ private: HWND mWinlessPopupSurrogateHWND; nsIntPoint mPluginSize; WNDPROC mWinlessThrottleOldWndProc; HWND mWinlessHiddenMsgHWND; #endif friend class ChildAsyncCall; +#if defined(OS_WIN) + nsTArray<FlashThrottleMsg*> mPendingFlashThrottleMsgs; +#endif Mutex mAsyncCallMutex; nsTArray<ChildAsyncCall*> mPendingAsyncCalls; nsTArray<nsAutoPtr<ChildTimer> > mTimers; /** * During destruction we enumerate all remaining scriptable objects and * invalidate/delete them. Enumeration can re-enter, so maintain a * hash separate from PluginModuleChild.mObjectMap.
--- a/dom/plugins/test/mochitest/browser.ini +++ b/dom/plugins/test/mochitest/browser.ini @@ -1,16 +1,18 @@ [DEFAULT] support-files = head.js + plugin_data_url_test.html plugin_test.html plugin_subframe_test.html plugin_no_scroll_div.html [browser_bug1163570.js] skip-if = true # Bug 1249878 [browser_bug1196539.js] skip-if = (!e10s || os != "win") [browser_tabswitchbetweenplugins.js] skip-if = (!e10s || os != "win") [browser_pluginscroll.js] skip-if = (true || !e10s || os != "win") # Bug 1213631 [browser_bug1335475.js] +[browser_data_url_plugin.js]
new file mode 100644 --- /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"); + }); + }); +});
new file mode 100644 --- /dev/null +++ b/dom/plugins/test/mochitest/plugin_data_url_test.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +</head> +<body> + <embed id="testplugin" type="application/x-test" drawmode="solid" color="ff00ff00" wmode="window" + style="position:absolute; top:50px; left:50px; width:500px; height:250px"> +<div style="display:block; height:3000px;"></div> + +<iframe id="dataFrame" src="data:text/html,foo" width="300" height="300"></iframe> + +</body> +</html>
new file mode 100644 --- /dev/null +++ b/dom/security/test/general/browser.ini @@ -0,0 +1,2 @@ +[DEFAULT] +[browser_test_toplevel_data_navigations.js]
new file mode 100644 --- /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,<body>" + kDataBody + "</body>"; + +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"); + }); + }); +});
new file mode 100644 --- /dev/null +++ b/dom/security/test/general/file_block_toplevel_data_navigation.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Toplevel data navigation</title> +</head> +<body> +test1: clicking data: URI tries to navigate window<br/> +<a id="testlink" href="data:text/html,<body>toplevel data: URI navigations should be blocked</body>">click me</a> +<script> + document.getElementById('testlink').click(); +</script> +</body> +</html>
new file mode 100644 --- /dev/null +++ b/dom/security/test/general/file_block_toplevel_data_navigation2.html @@ -0,0 +1,29 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Toplevel data navigation</title> +</head> +<body> +test2: data: URI in iframe tries to window.open(data:, _blank);<br/> +<iframe id="testFrame" src=""></iframe> +<script> + let DATA_URI = `data:text/html,<body><script> + var win = window.open("data:text/html,<body>toplevel data: URI navigations should be blocked</body>", "_blank"); + setTimeout(function () { + var result = win.document.body.innerHTML === "" ? "blocked" : "navigated"; + parent.postMessage(result, "*"); + win.close(); + }, 1000); + <\/script></body>`; + + window.addEventListener("message", receiveMessage); + function receiveMessage(event) { + window.removeEventListener("message", receiveMessage); + // propagate the information back to the caller + window.opener.postMessage(event.data, "*"); + } + document.getElementById('testFrame').src = DATA_URI; +</script> +</body> +</html>
new file mode 100644 --- /dev/null +++ b/dom/security/test/general/file_block_toplevel_data_navigation3.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Toplevel data navigation</title> +</head> +<body> +test3: performing data: URI navigation through win.loc.href<br/> +<script> + window.location.href = "data:text/html,<body>toplevel data: URI navigations should be blocked</body>"; +</script> +</body> +</html>
--- a/dom/security/test/general/mochitest.ini +++ b/dom/security/test/general/mochitest.ini @@ -1,9 +1,13 @@ [DEFAULT] 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]
new file mode 100644 --- /dev/null +++ b/dom/security/test/general/test_block_toplevel_data_navigation.html @@ -0,0 +1,78 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1331351 - Block top level window data: URI navigations</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> +SpecialPowers.setBoolPref("security.data_uri.block_toplevel_data_uri_navigations", true); +SimpleTest.registerCleanupFunction(() => { + SpecialPowers.clearUserPref("security.data_uri.block_toplevel_data_uri_navigations"); +}); + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("have to test that top level data: URI navgiation is blocked"); + +function test1() { + // simple data: URI click navigation should be prevented + let TEST_FILE = "file_block_toplevel_data_navigation.html"; + let win1 = window.open(TEST_FILE); + var readyStateCheckInterval = setInterval(function() { + let state = win1.document.readyState; + if (state === "interactive" || state === "complete") { + clearInterval(readyStateCheckInterval); + ok(win1.document.body.innerHTML.indexOf("test1:") !== -1, + "toplevel data: URI navigation through click() should be blocked"); + win1.close(); + test2(); + } + }, 200); +} + +function test2() { + // data: URI in iframe which opens data: URI in _blank should be blocked + let win2 = window.open("file_block_toplevel_data_navigation2.html"); + window.addEventListener("message", receiveMessage); + function receiveMessage(event) { + window.removeEventListener("message", receiveMessage); + is(event.data, "blocked", + "data: URI navigation using _blank from data: URI should be blocked"); + win2.close(); + test3(); + } +} + +function test3() { + // navigating to a data: URI using window.location.href should be blocked + let win3 = window.open("file_block_toplevel_data_navigation3.html"); + setTimeout(function () { + ok(win3.document.body.innerHTML.indexOf("test3:") !== -1, + "data: URI navigation through win.loc.href should be blocked"); + win3.close(); + test4(); + }, 1000); +} + +function test4() { + // navigating to a data: URI using window.open() should be blocked + let win4 = window.open("data:text/html,<body>toplevel data: URI navigations should be blocked</body>"); + setTimeout(function () { + // Please note that the data: URI will be displayed in the URL-Bar but not + // loaded, hence we rather rely on document.body than document.location + is(win4.document.body.innerHTML, "", + "navigating to a data: URI using window.open() should be blocked"); + win4.close(); + SimpleTest.finish(); + }, 1000); +} + +// fire up the tests +test1(); + +</script> +</body> +</html>
--- a/dom/security/test/moz.build +++ b/dom/security/test/moz.build @@ -24,10 +24,11 @@ MOCHITEST_MANIFESTS += [ ] MOCHITEST_CHROME_MANIFESTS += [ 'general/chrome.ini', ] BROWSER_CHROME_MANIFESTS += [ 'csp/browser.ini', + 'general/browser.ini', 'hsts/browser.ini', ]
--- a/dom/smil/crashtests/590425-1.html +++ b/dom/smil/crashtests/590425-1.html @@ -13,12 +13,12 @@ function boom() document.documentElement.removeAttribute("class"); } </script> </head> <body onload="boom()"> -<iframe id="frame" src="data:text/html,%3Cbody%3E%3Csvg id=s%3E"></iframe> +<iframe id="frame" srcdoc="<body><svg id=s>"></iframe> </body> </html>
--- a/dom/xhr/tests/browser_blobFromFile.js +++ b/dom/xhr/tests/browser_blobFromFile.js @@ -1,45 +1,60 @@ let { classes: Cc, interfaces: Ci } = Components; add_task(async function test() { await SpecialPowers.pushPrefEnv( {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) .getProtocolHandler("file") .QueryInterface(Ci.nsIFileProtocolHandler); let fileURL = fileHandler.getURLSpecFromFile(file); info("Opening url: " + fileURL); let tab = BrowserTestUtils.addTab(gBrowser, fileURL); 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); } }); }); 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); });
--- a/editor/libeditor/crashtests/336081-1.xhtml +++ b/editor/libeditor/crashtests/336081-1.xhtml @@ -34,19 +34,19 @@ function init() } ]]> </script> </head> <body onload="init()"> -<iframe src="data:text/html," style="width: 95%; height: 500px;"/> +<iframe srcdoc="<html></html>" style="width: 95%; height: 500px;"/> <div id="rootish"> <div id="out1"/> <div id="in1"/> <div id="in2"/> <div id="out2"/> </div> </body> -</html> \ No newline at end of file +</html>
--- a/editor/libeditor/crashtests/382527-1.html +++ b/editor/libeditor/crashtests/382527-1.html @@ -2,17 +2,17 @@ <html class="reftest-wait"> <head> <script> function init1() { targetIframe = document.createElementNS('http://www.w3.org/1999/xhtml', 'iframe'); - targetIframe.src = "data:text/html,"; + targetIframe.srcdoc = "<html></html>"; targetIframe.setAttribute("style", "width: 300px; height: 200px; border: 1px dotted green;"); targetIframe.addEventListener("load", init2); document.body.appendChild(targetIframe); } function init2() {
--- a/editor/libeditor/crashtests/382778-1.html +++ b/editor/libeditor/crashtests/382778-1.html @@ -2,17 +2,17 @@ <html class="reftest-wait"> <head> <script> function init1() { // Create an html:iframe in HTML mode (so designMode can be used 320092) targetIframe = document.createElementNS('http://www.w3.org/1999/xhtml', 'iframe'); - targetIframe.src = "data:text/html,"; + targetIframe.srcdoc = "<html></html>"; targetIframe.setAttribute("style", "width: 700px; height: 500px; border: 1px dotted green;"); targetIframe.addEventListener("load", init2); document.body.appendChild(targetIframe); } function init2() {
--- a/gfx/2d/ScaledFontDWrite.cpp +++ b/gfx/2d/ScaledFontDWrite.cpp @@ -151,17 +151,29 @@ SkTypeface* ScaledFontDWrite::GetSkTypeface() { if (!mTypeface) { RefPtr<IDWriteFactory> factory = Factory::GetDWriteFactory(); if (!factory) { 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; } #endif void ScaledFontDWrite::CopyGlyphsToBuilder(const GlyphBuffer &aBuffer, PathBuilder *aBuilder, const Matrix *aTransformHint) {
--- a/gfx/tests/crashtests/1343666.html +++ b/gfx/tests/crashtests/1343666.html @@ -4,17 +4,17 @@ <script> function f() { finish(); } window.onload = function() { - let a = window.open("data:text/plain,test", null, "width=300,height=300"); + let a = window.open("empty.html", null, "width=300,height=300"); setTimeout(function(){ a.close(); a.addEventListener("vrdisplayconnect", function(){}); window.close(); document.documentElement.removeAttribute("class"); }, 0); };
--- a/gfx/tests/crashtests/372094-1.xhtml +++ b/gfx/tests/crashtests/372094-1.xhtml @@ -28,18 +28,18 @@ function boom() } ]]> </script> </head> <body onload="init()"> -<iframe src="data:text/html," style="width: 95%; height: 500px;"/> + <iframe srcdoc="<html></html>" style="width: 95%; height: 500px;"/> <div id="rootish"> <div>Foo</div> <div id="bar">Bar</div> <div><select><option id="baz">baz</option></select></div> </div> </body> -</html> \ No newline at end of file +</html>
--- a/gfx/thebes/gfxBlur.cpp +++ b/gfx/thebes/gfxBlur.cpp @@ -31,59 +31,62 @@ gfxAlphaBoxBlur::~gfxAlphaBoxBlur() } already_AddRefed<gfxContext> gfxAlphaBoxBlur::Init(gfxContext* aDestinationCtx, const gfxRect& aRect, const IntSize& aSpreadRadius, const IntSize& aBlurRadius, const gfxRect* aDirtyRect, - const gfxRect* aSkipRect) + const gfxRect* aSkipRect, + bool aUseHardwareAccel) { DrawTarget* refDT = aDestinationCtx->GetDrawTarget(); Maybe<Rect> dirtyRect = aDirtyRect ? Some(ToRect(*aDirtyRect)) : Nothing(); Maybe<Rect> skipRect = aSkipRect ? Some(ToRect(*aSkipRect)) : Nothing(); RefPtr<DrawTarget> dt = InitDrawTarget(refDT, ToRect(aRect), aSpreadRadius, aBlurRadius, - dirtyRect.ptrOr(nullptr), skipRect.ptrOr(nullptr)); + dirtyRect.ptrOr(nullptr), skipRect.ptrOr(nullptr), + aUseHardwareAccel); if (!dt) { return nullptr; } RefPtr<gfxContext> context = gfxContext::CreateOrNull(dt); MOZ_ASSERT(context); // already checked for target above context->SetMatrix(gfxMatrix::Translation(-mBlur.GetRect().TopLeft())); return context.forget(); } already_AddRefed<DrawTarget> gfxAlphaBoxBlur::InitDrawTarget(const DrawTarget* aReferenceDT, const Rect& aRect, const IntSize& aSpreadRadius, const IntSize& aBlurRadius, const Rect* aDirtyRect, - const Rect* aSkipRect) + const Rect* aSkipRect, + bool aUseHardwareAccel) { mBlur.Init(aRect, aSpreadRadius, aBlurRadius, aDirtyRect, aSkipRect); size_t blurDataSize = mBlur.GetSurfaceAllocationSize(); if (blurDataSize == 0) { return nullptr; } BackendType backend = aReferenceDT->GetBackendType(); // Check if the backend has an accelerated DrawSurfaceWithShadow. // Currently, only D2D1.1 supports this. // Otherwise, DrawSurfaceWithShadow only supports square blurs without spread. // When blurring small draw targets such as short spans text, the cost of // creating and flushing an accelerated draw target may exceed the speedup - // gained from the faster blur, so we also make sure the blurred data exceeds - // a sufficient number of pixels to offset this cost. + // gained from the faster blur. It's up to the users of this blur + // to determine whether they want to use hardware acceleration. if (aBlurRadius.IsSquare() && aSpreadRadius.IsEmpty() && - blurDataSize >= 8192 && + aUseHardwareAccel && backend == BackendType::DIRECT2D1_1) { mAccelerated = true; mDrawTarget = aReferenceDT->CreateShadowDrawTarget(mBlur.GetSize(), SurfaceFormat::A8, AlphaBoxBlur::CalculateBlurSigma(aBlurRadius.width)); } else { // Make an alpha-only surface to draw on. We will play with the data after
--- a/gfx/thebes/gfxBlur.h +++ b/gfx/thebes/gfxBlur.h @@ -69,32 +69,37 @@ public: * * @param aDirtyRect A pointer to a dirty rect, measured in device units, * if available. This will be used for optimizing the blur operation. It * is safe to pass nullptr here. * * @param aSkipRect A pointer to a rect, measured in device units, that * represents an area where blurring is unnecessary and shouldn't be done * for speed reasons. It is safe to pass nullptr here. + * + * @param aUseHardwareAccel Flag to state whether or not we can use hardware + * acceleration to speed up this blur. */ already_AddRefed<gfxContext> Init(gfxContext* aDestinationCtx, const gfxRect& aRect, const mozilla::gfx::IntSize& aSpreadRadius, const mozilla::gfx::IntSize& aBlurRadius, const gfxRect* aDirtyRect, - const gfxRect* aSkipRect); + const gfxRect* aSkipRect, + bool aUseHardwareAccel = true); already_AddRefed<DrawTarget> InitDrawTarget(const mozilla::gfx::DrawTarget* aReferenceDT, const mozilla::gfx::Rect& aRect, const mozilla::gfx::IntSize& aSpreadRadius, const mozilla::gfx::IntSize& aBlurRadius, const mozilla::gfx::Rect* aDirtyRect = nullptr, - const mozilla::gfx::Rect* aSkipRect = nullptr); + const mozilla::gfx::Rect* aSkipRect = nullptr, + bool aUseHardwareAccel = true); /** * Performs the blur and optionally colors the result if aShadowColor is not null. */ already_AddRefed<mozilla::gfx::SourceSurface> DoBlur(const mozilla::gfx::Color* aShadowColor = nullptr, mozilla::gfx::IntPoint* aOutTopLeft = nullptr);
--- a/gfx/thebes/gfxWindowsPlatform.cpp +++ b/gfx/thebes/gfxWindowsPlatform.cpp @@ -1188,24 +1188,16 @@ gfxWindowsPlatform::SetupClearTypeParams RegCloseKey(hKey); } if (contrast < 0.0 || contrast > 10.0) { contrast = 1.0; } } - 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) { gamma = defaultRenderingParams->GetGamma(); } if (level < 0.0 || level > 1.0) { level = defaultRenderingParams->GetClearTypeLevel();
--- a/js/public/TrackedOptimizationInfo.h +++ b/js/public/TrackedOptimizationInfo.h @@ -18,16 +18,17 @@ namespace JS { _(GetProp_Constant) \ _(GetProp_NotDefined) \ _(GetProp_StaticName) \ _(GetProp_SimdGetter) \ _(GetProp_TypedObject) \ _(GetProp_DefiniteSlot) \ _(GetProp_Unboxed) \ _(GetProp_CommonGetter) \ + _(GetProp_Static) \ _(GetProp_InlineAccess) \ _(GetProp_Innerize) \ _(GetProp_InlineCache) \ _(GetProp_SharedCache) \ _(GetProp_ModuleNamespace) \ \ _(SetProp_CommonSetter) \ _(SetProp_TypedObject) \
--- a/js/src/devtools/automation/autospider.py +++ b/js/src/devtools/automation/autospider.py @@ -137,18 +137,21 @@ def ensure_dir_exists(name, clobber=True with open(os.path.join(DIR.scripts, "variants", args.variant)) as fh: variant = json.load(fh) if args.variant == 'nonunified': # Rewrite js/src/**/moz.build to replace UNIFIED_SOURCES to SOURCES. # 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") POBJDIR = posixpath.join(PDIR.source, args.objdir) AUTOMATION = env.get('AUTOMATION', False) MAKE = env.get('MAKE', 'make') MAKEFLAGS = env.get('MAKEFLAGS', '-j6' + ('' if AUTOMATION else ' -s')) UNAME_M = subprocess.check_output(['uname', '-m']).strip()
--- a/js/src/frontend/BytecodeEmitter.cpp +++ b/js/src/frontend/BytecodeEmitter.cpp @@ -9511,16 +9511,25 @@ BytecodeEmitter::emitCallOrNew(ParseNode if (!emitGetName(pn2, callop)) return false; break; case PNK_DOT: MOZ_ASSERT(emitterMode != BytecodeEmitter::SelfHosting); if (pn2->as<PropertyAccess>().isSuper()) { if (!emitSuperPropOp(pn2, JSOP_GETPROP_SUPER, /* isCall = */ callop)) return false; + } else if ((pn->getOp() == JSOP_FUNCALL || pn->getOp() == JSOP_FUNAPPLY) && + pn2->expr()->getKind() == PNK_FUNCTION && + checkRunOnceContext()) { + // Top level lambdas whose .call or .apply methods are immediately + // invoked should be treated as run once lambdas. + emittingRunOnceLambda = true; + if (!emitPropOp(pn2, callop ? JSOP_CALLPROP : JSOP_GETPROP)) + return false; + emittingRunOnceLambda = false; } else { if (!emitPropOp(pn2, callop ? JSOP_CALLPROP : JSOP_GETPROP)) return false; } break; case PNK_ELEM: MOZ_ASSERT(emitterMode != BytecodeEmitter::SelfHosting);
--- a/js/src/gc/Nursery.cpp +++ b/js/src/gc/Nursery.cpp @@ -696,20 +696,21 @@ js::Nursery::doCollection(JS::gcreason:: // Move objects pointed to by roots from the nursery to the major heap. TenuringTracer mover(rt, this); // Mark the store buffer. This must happen first. 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); sb.traceValues(mover); endProfile(ProfileKey::TraceValues); startProfile(ProfileKey::TraceCells); sb.traceCells(mover);
--- a/js/src/gdb/mozilla/asmjs.py +++ b/js/src/gdb/mozilla/asmjs.py @@ -14,25 +14,25 @@ def on_stop(event): if isinstance(event, gdb.SignalEvent) and event.stop_signal == 'SIGSEGV': # Allocate memory for sigaction, once per js shell process. process = gdb.selected_inferior() buf = sigaction_buffers.get(process) if buf is None: buf = gdb.parse_and_eval("(struct sigaction *) malloc(sizeof(struct sigaction))") sigaction_buffers[process] = buf - # See if AsmJSFaultHandler is installed as the SIGSEGV signal action. + # See if WasmFaultHandler is installed as the SIGSEGV signal action. sigaction_fn = gdb.parse_and_eval('__sigaction') sigaction_fn(SIGSEGV, 0, buf) - AsmJSFaultHandler = gdb.parse_and_eval("AsmJSFaultHandler") - if buf['__sigaction_handler']['sa_handler'] == AsmJSFaultHandler: + WasmFaultHandler = gdb.parse_and_eval("WasmFaultHandler<(Signal)0>") + if buf['__sigaction_handler']['sa_handler'] == WasmFaultHandler: # Advise the user that magic is happening. - print("js/src/gdb/mozilla/asmjs.py: Allowing AsmJSFaultHandler to run.") + print("js/src/gdb/mozilla/asmjs.py: Allowing WasmFaultHandler to run.") - # If AsmJSFaultHandler doesn't handle this segfault, it will unhook + # If WasmFaultHandler doesn't handle this segfault, it will unhook # itself and re-raise. gdb.execute("continue") def on_exited(event): if event.inferior in sigaction_buffers: del sigaction_buffers[event.inferior] def install():
--- a/js/src/gdb/mozilla/unwind.py +++ b/js/src/gdb/mozilla/unwind.py @@ -38,16 +38,17 @@ def debug(something): SizeOfFramePrefix = { 'JitFrame_IonJS': 'ExitFrameLayout', 'JitFrame_BaselineJS': 'JitFrameLayout', 'JitFrame_BaselineStub': 'BaselineStubFrameLayout', 'JitFrame_IonStub': 'JitStubFrameLayout', 'JitFrame_Entry': 'JitFrameLayout', 'JitFrame_Rectifier': 'RectifierFrameLayout', 'JitFrame_IonAccessorIC': 'IonAccessorICFrameLayout', + 'JitFrame_IonICCall': 'IonICCallFrameLayout', 'JitFrame_Exit': 'ExitFrameLayout', 'JitFrame_Bailout': 'JitFrameLayout', } # All types and symbols that we need are attached to an object that we # can dispose of as needed. class UnwinderTypeCache(object): def __init__(self): @@ -324,22 +325,22 @@ class UnwinderState(object): return False # See whether |pc| is claimed by the Jit. def is_jit_address(self, pc): if self.proc_mappings != None: return not self.text_address_claimed(pc) cx = self.get_tls_context() - runtime = cx['runtime_'] - if runtime == 0: + runtime = cx['runtime_']['value'] + if long(runtime.address) == 0: return False jitRuntime = runtime['jitRuntime_'] - if jitRuntime == 0: + if long(jitRuntime.address) == 0: return False execAllocators = [jitRuntime['execAlloc_'], jitRuntime['backedgeExecAlloc_']] for execAlloc in execAllocators: for pool in jsjitExecutableAllocator(execAlloc, self.typecache): pages = pool['m_allocation']['pages'] size = pool['m_allocation']['size'] if pages <= pc and pc < pages + size:
new file mode 100644 --- /dev/null +++ b/js/src/jit-test/tests/ion/bailoutStaticObject.js @@ -0,0 +1,28 @@ +// Test bailouts from loading particular non-singleton static objects. + +function wrap(fun) { + function wrapper() { + return fun.apply(this, arguments); + } + return wrapper; +} + +var adder = wrap(function(a, b) { return a + b; }); +var subber = wrap(function(a, b) { return a - b; }); +var tmp = adder; +adder = subber; +adder = tmp; + +function foo() { + var i = 0; + var a = 0; + for (var i = 0; i < 10000; i++) { + a = adder(a, 1); + a = subber(a, 1); + } + return a; +} + +assertEq(foo(), 0); +adder = subber; +assertEq(foo(), -20000);
--- a/js/src/jit/BaselineBailouts.cpp +++ b/js/src/jit/BaselineBailouts.cpp @@ -1998,16 +1998,17 @@ jit::FinishBailoutToBaseline(BaselineBai case Bailout_NonObjectInput: case Bailout_NonStringInput: case Bailout_NonSymbolInput: case Bailout_UnexpectedSimdInput: case Bailout_NonSharedTypedArrayInput: case Bailout_Debugger: case Bailout_UninitializedThis: case Bailout_BadDerivedConstructorReturn: + case Bailout_LoadStaticObject: // Do nothing. break; case Bailout_FirstExecution: // Do not return directly, as this was not frequent in the first place, // thus rely on the check for frequent bailouts to recompile the current // script. break;
--- a/js/src/jit/BaselineInspector.cpp +++ b/js/src/jit/BaselineInspector.cpp @@ -1035,17 +1035,17 @@ GetMegamorphicGetterSetterFunction(ICStu JSObject* obj = isGetter ? propShape->getterObject() : propShape->setterObject(); return &obj->as<JSFunction>(); } bool BaselineInspector::megamorphicGetterSetterFunction(jsbytecode* pc, bool isGetter, JSFunction** getterOrSetter) { - if (!hasBaselineScript()) + if (!hasBaselineScript() || *pc == JSOP_SETALIASEDVAR) return false; *getterOrSetter = nullptr; const ICEntry& entry = icEntryFromPC(pc); for (ICStub* stub = entry.firstStub(); stub; stub = stub->next()) { if (stub->isCacheIR_Monitored()) { MOZ_ASSERT(isGetter); @@ -1186,17 +1186,17 @@ AddCacheIRSetPropFunction(ICCacheIR_Upda } bool BaselineInspector::commonSetPropFunction(jsbytecode* pc, JSObject** holder, Shape** holderShape, JSFunction** commonSetter, bool* isOwnProperty, ReceiverVector& receivers, ObjectGroupVector& convertUnboxedGroups) { - if (!hasBaselineScript()) + if (!hasBaselineScript() || *pc == JSOP_SETALIASEDVAR) return false; MOZ_ASSERT(receivers.empty()); MOZ_ASSERT(convertUnboxedGroups.empty()); *commonSetter = nullptr; const ICEntry& entry = icEntryFromPC(pc);
--- a/js/src/jit/IonBuilder.cpp +++ b/js/src/jit/IonBuilder.cpp @@ -7270,27 +7270,16 @@ IonBuilder::ensureDefiniteTypeSet(MDefin } // Create a NOP mir instruction to filter the typeset. MFilterTypeSet* filter = MFilterTypeSet::New(alloc(), def, types); current->add(filter); return filter; } -static size_t -NumFixedSlots(JSObject* object) -{ - // Note: we can't use object->numFixedSlots() here, as this will read the - // shape and can race with the active thread if we are building off thread. - // The allocation kind and object class (which goes through the type) can - // be read freely, however. - gc::AllocKind kind = object->asTenured().getAllocKind(); - return gc::GetGCKindSlots(kind, object->getClass()); -} - static bool IsUninitializedGlobalLexicalSlot(JSObject* obj, PropertyName* name) { LexicalEnvironmentObject &globalLexical = obj->as<LexicalEnvironmentObject>(); MOZ_ASSERT(globalLexical.isGlobal()); Shape* shape = globalLexical.lookupPure(name); if (!shape) return false; @@ -7302,42 +7291,58 @@ IonBuilder::getStaticName(bool* emitted, MDefinition* lexicalCheck) { MOZ_ASSERT(*emitted == false); jsid id = NameToId(name); bool isGlobalLexical = staticObject->is<LexicalEnvironmentObject>() && staticObject->as<LexicalEnvironmentObject>().isGlobal(); - MOZ_ASSERT(isGlobalLexical || - staticObject->is<GlobalObject>() || - staticObject->is<CallObject>() || - staticObject->is<ModuleEnvironmentObject>()); - MOZ_ASSERT(staticObject->isSingleton()); // Always emit the lexical check. This could be optimized, but is // currently not for simplicity's sake. if (lexicalCheck) return Ok(); + // Only optimize accesses on native objects. + if (!staticObject->isNative()) + return Ok(); + + // Only optimize accesses on own data properties. + Shape* propertyShape = staticObject->as<NativeObject>().lastProperty()->searchLinear(NameToId(name)); + if (!propertyShape || !propertyShape->isDataDescriptor() || !propertyShape->hasSlot()) + return Ok(); + uint32_t slot = propertyShape->slot(); + TypeSet::ObjectKey* staticKey = TypeSet::ObjectKey::get(staticObject); if (analysisContext) staticKey->ensureTrackedProperty(analysisContext, NameToId(name)); - if (staticKey->unknownProperties()) - return Ok(); - - HeapTypeSetKey property = staticKey->property(id); - if (!property.maybeTypes() || - !property.maybeTypes()->definiteProperty() || - property.nonData(constraints())) - { - // The property has been reconfigured as non-configurable, non-enumerable - // or non-writable. - return Ok(); + // Make sure the property is a normal data property. This is not done for + // call objects, as they are not tracked by TI and their data properties + // cannot be dynamically reconfigured. + Maybe<HeapTypeSetKey> property; + if (!staticObject->is<CallObject>()) { + if (staticKey->unknownProperties()) + return Ok(); + + property.emplace(staticKey->property(id)); + + if (property.ref().nonData(constraints()) || + !property.ref().maybeTypes() || + !property.ref().maybeTypes()->definiteProperty()) + { + // We can't be sure the slot will match at runtime, so include a + // shape guard on the object. + MInstruction* obj = MConstant::NewConstraintlessObject(alloc(), staticObject); + current->add(obj); + addShapeGuard(obj, staticObject->as<NativeObject>().lastProperty(), Bailout_ShapeGuard); + } else { + MOZ_ASSERT(slot == property.ref().maybeTypes()->definiteSlot()); + } } // Don't optimize global lexical bindings if they aren't initialized at // compile time. if (isGlobalLexical && IsUninitializedGlobalLexicalSlot(staticObject, name)) return Ok(); *emitted = true; @@ -7353,23 +7358,47 @@ IonBuilder::getStaticName(bool* emitted, if (testSingletonProperty(staticObject, id) == singleton) { pushConstant(ObjectValue(*singleton)); return Ok(); } } // Try to inline properties that have never been overwritten. Value constantValue; - if (property.constant(constraints(), &constantValue)) { + if (property.isSome() && property.ref().constant(constraints(), &constantValue)) { pushConstant(constantValue); return Ok(); } } - MOZ_TRY(loadStaticSlot(staticObject, barrier, types, property.maybeTypes()->definiteSlot())); + MOZ_TRY(loadStaticSlot(staticObject, barrier, types, slot)); + + // If the static object has a function object stored in this property, + // test that the result is that specific function. This is yet another + // technique for trying to force a property load to be a specific value, + // and is included because other mechanisms (property types and observed + // types) do not always work, especially in polymorphic framework code. + // We restrict this optimization to function properties, as they are less + // likely to change over time and are more likely to require precise + // information for inlining decisions. + if (!outermostBuilder()->script()->hadFrequentBailouts()) { + Value v = staticObject->as<NativeObject>().getSlot(slot); + if (v.isObject() && + v.toObject().is<JSFunction>() && + v.toObject().as<JSFunction>().isInterpreted()) + { + JSObject* result = checkNurseryObject(&v.toObject().as<JSFunction>()); + MDefinition* load = current->pop(); + MInstruction* expected = MConstant::NewConstraintlessObject(alloc(), result); + expected->setResultTypeSet(MakeSingletonTypeSet(constraints(), result)); + current->add(expected); + current->add(MGuardObjectIdentity::New(alloc(), load, expected, false, Bailout_LoadStaticObject)); + current->push(expected); + } + } return Ok(); } AbortReasonOr<Ok> IonBuilder::loadStaticSlot(JSObject* staticObject, BarrierKind barrier, TemporaryTypeSet* types, uint32_t slot) { @@ -7387,17 +7416,17 @@ IonBuilder::loadStaticSlot(JSObject* sta } MInstruction* obj = constant(ObjectValue(*staticObject)); MIRType rvalType = types->getKnownMIRType(); if (barrier != BarrierKind::NoBarrier) rvalType = MIRType::Value; - return loadSlot(obj, slot, NumFixedSlots(staticObject), rvalType, barrier, types); + return loadSlot(obj, slot, staticObject->as<NativeObject>().numFixedSlots(), rvalType, barrier, types); } // Whether a write of the given value may need a post-write barrier for GC purposes. bool jit::NeedsPostBarrier(MDefinition* value) { if (!GetJitContext()->compartment->zone()->nurseryExists()) return false; @@ -7452,17 +7481,18 @@ IonBuilder::setStaticName(JSObject* stat // If the property has a known type, we may be able to optimize typed stores by not // storing the type tag. MIRType slotType = MIRType::None; MIRType knownType = property.knownMIRType(constraints()); if (knownType != MIRType::Value) slotType = knownType; bool needsPreBarrier = property.needsBarrier(constraints()); - return storeSlot(obj, property.maybeTypes()->definiteSlot(), NumFixedSlots(staticObject), + return storeSlot(obj, property.maybeTypes()->definiteSlot(), + staticObject->as<NativeObject>().numFixedSlots(), value, needsPreBarrier, slotType); } JSObject* IonBuilder::testGlobalLexicalBinding(PropertyName* name) { MOZ_ASSERT(JSOp(*pc) == JSOP_BINDGNAME || JSOp(*pc) == JSOP_GETGNAME || @@ -8534,18 +8564,17 @@ IonBuilder::getElemAddCache(MDefinition* } TemporaryTypeSet* 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<HeapTypeSetKey, 4, SystemAllocPolicy> properties; if (!properties.reserve(objTypes->getObjectCount())) return nullptr; for (unsigned i = 0; i < objTypes->getObjectCount(); i++) { TypeSet::ObjectKey* key = objTypes->getObject(i); @@ -8555,17 +8584,24 @@ IonBuilder::computeHeapType(const Tempor HeapTypeSetKey property = key->property(id); HeapTypeSet* currentSet = property.maybeTypes(); if (!currentSet || currentSet->unknown()) 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; } // Freeze all the properties associated with the refined type set. for (HeapTypeSetKey* i = properties.begin(); i != properties.end(); i++) i->freeze(constraints()); @@ -10387,16 +10423,22 @@ IonBuilder::jsop_getprop(PropertyName* n return Ok(); // Try to inline a common property getter, or make a call. trackOptimizationAttempt(TrackedStrategy::GetProp_CommonGetter); MOZ_TRY(getPropTryCommonGetter(&emitted, obj, name, types)); if (emitted) return Ok(); + // Try to optimize for loads from a specific object. + trackOptimizationAttempt(TrackedStrategy::GetProp_Static); + MOZ_TRY(getPropTryStaticAccess(&emitted, obj, name, barrier, types)); + if (emitted) + return Ok(); + // Try to emit a monomorphic/polymorphic access based on baseline caches. trackOptimizationAttempt(TrackedStrategy::GetProp_InlineAccess); MOZ_TRY(getPropTryInlineAccess(&emitted, obj, name, barrier, types)); if (emitted) return Ok(); // Try to emit loads from a module namespace. trackOptimizationAttempt(TrackedStrategy::GetProp_ModuleNamespace); @@ -11187,16 +11229,27 @@ PropertyShapesHaveSameSlot(const Baselin return nullptr; } } return firstShape; } AbortReasonOr<Ok> +IonBuilder::getPropTryStaticAccess(bool* emitted, MDefinition* obj, PropertyName* name, + BarrierKind barrier, TemporaryTypeSet* types) +{ + if (!obj->isConstant() || obj->type() != MIRType::Object) + return Ok(); + + obj->setImplicitlyUsedUnchecked(); + return getStaticName(emitted, &obj->toConstant()->toObject(), name); +} + +AbortReasonOr<Ok> IonBuilder::getPropTryInlineAccess(bool* emitted, MDefinition* obj, PropertyName* name, BarrierKind barrier, TemporaryTypeSet* types) { MOZ_ASSERT(*emitted == false); BaselineInspector::ReceiverVector receivers(alloc()); BaselineInspector::ObjectGroupVector convertUnboxedGroups(alloc()); if (!inspector->maybeInfoForPropertyOp(pc, receivers, convertUnboxedGroups)) @@ -12619,21 +12672,59 @@ IonBuilder::walkEnvironmentChain(unsigne MInstruction* ins = MEnclosingEnvironment::New(alloc(), env); current->add(ins); env = ins; } return env; } +static bool +SearchEnvironmentChainForCallObject(JSObject* environment, JSScript* script, JSObject** pcall) +{ + while (environment && !environment->is<GlobalObject>()) { + if (environment->is<CallObject>() && + environment->as<CallObject>().callee().nonLazyScript() == script) + { + *pcall = environment; + return true; + } + environment = environment->enclosingEnvironment(); + } + return false; +} + bool IonBuilder::hasStaticEnvironmentObject(EnvironmentCoordinate ec, JSObject** pcall) { JSScript* outerScript = EnvironmentCoordinateFunctionScript(script(), pc); - if (!outerScript || !outerScript->treatAsRunOnce()) + if (!outerScript) + return false; + + // JSOP_SETALIASEDVAR only emits a cache when the outer script is a run + // once script. To avoid problems with the generic jsop_setprop() paths, + // only use static environment objects when a baseline cache exists. + if (*pc == JSOP_SETALIASEDVAR && !outerScript->treatAsRunOnce()) + return false; + + // If the callee is a specific JSFunction then there is a specific + // environment object on its chain we can use. + if (inlineCallInfo_) { + MDefinition* calleeDef = inlineCallInfo_->fun(); + if (calleeDef->isConstant()) { + JSFunction* callee = &calleeDef->toConstant()->toObject().template as<JSFunction>(); + JSObject* environment = callee->environment(); + if (SearchEnvironmentChainForCallObject(environment, outerScript, pcall)) + return true; + } + } + + // Otherwise, if the outer script will only run once then we can go looking + // for its call object. + if (!outerScript->treatAsRunOnce()) return false; TypeSet::ObjectKey* funKey = TypeSet::ObjectKey::get(outerScript->functionNonDelazifying()); if (funKey->hasFlags(constraints(), OBJECT_FLAG_RUNONCE_INVALIDATED)) return false; // The script this aliased var operation is accessing will run only once, @@ -12644,42 +12735,28 @@ IonBuilder::hasStaticEnvironmentObject(E // Look for the call object on the current script's function's env chain. // If the current script is inner to the outer script and the function has // singleton type then it should show up here. MDefinition* envDef = current->getSlot(info().environmentChainSlot()); envDef->setImplicitlyUsedUnchecked(); JSObject* environment = script()->functionNonDelazifying()->environment(); - while (environment && !environment->is<GlobalObject>()) { - if (environment->is<CallObject>() && - environment->as<CallObject>().callee().nonLazyScript() == outerScript) - { - MOZ_ASSERT(environment->isSingleton()); - *pcall = environment; - return true; - } - environment = environment->enclosingEnvironment(); - } + if (SearchEnvironmentChainForCallObject(environment, outerScript, pcall)) + return true; // Look for the call object on the current frame, if we are compiling the // outer script itself. Don't do this if we are at entry to the outer // script, as the call object we see will not be the real one --- after // entering the Ion code a different call object will be created. if (script() == outerScript && baselineFrame_ && info().osrPc()) { JSObject* singletonScope = baselineFrame_->singletonEnvChain; - if (singletonScope && - singletonScope->is<CallObject>() && - singletonScope->as<CallObject>().callee().nonLazyScript() == outerScript) - { - MOZ_ASSERT(singletonScope->isSingleton()); - *pcall = singletonScope; + if (SearchEnvironmentChainForCallObject(singletonScope, outerScript, pcall)) return true; - } } return true; } MDefinition* IonBuilder::getAliasedVar(EnvironmentCoordinate ec) {
--- a/js/src/jit/IonBuilder.h +++ b/js/src/jit/IonBuilder.h @@ -241,16 +241,18 @@ class IonBuilder AbortReasonOr<Ok> getPropTryDefiniteSlot(bool* emitted, MDefinition* obj, PropertyName* name, BarrierKind barrier, TemporaryTypeSet* types); AbortReasonOr<Ok> getPropTryModuleNamespace(bool* emitted, MDefinition* obj, PropertyName* name, BarrierKind barrier, TemporaryTypeSet* types); AbortReasonOr<Ok> getPropTryUnboxed(bool* emitted, MDefinition* obj, PropertyName* name, BarrierKind barrier, TemporaryTypeSet* types); AbortReasonOr<Ok> getPropTryCommonGetter(bool* emitted, MDefinition* obj, PropertyName* name, TemporaryTypeSet* types, bool innerized = false); + AbortReasonOr<Ok> getPropTryStaticAccess(bool* emitted, MDefinition* obj, PropertyName* name, + BarrierKind barrier, TemporaryTypeSet* types); AbortReasonOr<Ok> getPropTryInlineAccess(bool* emitted, MDefinition* obj, PropertyName* name, BarrierKind barrier, TemporaryTypeSet* types); AbortReasonOr<Ok> getPropTryTypedObject(bool* emitted, MDefinition* obj, PropertyName* name); AbortReasonOr<Ok> getPropTryScalarPropOfTypedObject(bool* emitted, MDefinition* typedObj, int32_t fieldOffset, TypedObjectPrediction fieldTypeReprs); AbortReasonOr<Ok> getPropTryReferencePropOfTypedObject(bool* emitted, MDefinition* typedObj, int32_t fieldOffset,
--- a/js/src/jit/IonTypes.h +++ b/js/src/jit/IonTypes.h @@ -137,16 +137,19 @@ enum BailoutKind Bailout_NonStringInputInvalidate, // Used for integer division, multiplication and modulo. // If there's a remainder, bails to return a double. // Can also signal overflow or result of -0. // Can also signal division by 0 (returns inf, a double). Bailout_DoubleOutput, + // Load of a value from a static object retrieved an unexpected value. + Bailout_LoadStaticObject, + // END Invalid assumptions bailouts // A bailout at the very start of a function indicates that there may be // a type mismatch in the arguments that necessitates a reflow. Bailout_ArgumentCheck, // A bailout triggered by a bounds-check failure. @@ -228,16 +231,18 @@ BailoutKindString(BailoutKind kind) // Bailouts caused by invalid assumptions. case Bailout_OverflowInvalidate: return "Bailout_OverflowInvalidate"; case Bailout_NonStringInputInvalidate: return "Bailout_NonStringInputInvalidate"; case Bailout_DoubleOutput: return "Bailout_DoubleOutput"; + case Bailout_LoadStaticObject: + return "Bailout_LoadStaticObject"; // Other bailouts. case Bailout_ArgumentCheck: return "Bailout_ArgumentCheck"; case Bailout_BoundsCheck: return "Bailout_BoundsCheck"; case Bailout_Detached: return "Bailout_Detached";
--- a/js/src/jit/Lowering.cpp +++ b/js/src/jit/Lowering.cpp @@ -3895,17 +3895,17 @@ LIRGenerator::visitCallBindVar(MCallBind define(lir, ins); } void LIRGenerator::visitGuardObjectIdentity(MGuardObjectIdentity* ins) { LGuardObjectIdentity* guard = new(alloc()) LGuardObjectIdentity(useRegister(ins->object()), useRegister(ins->expected())); - assignSnapshot(guard, Bailout_ObjectIdentityOrTypeGuard); + assignSnapshot(guard, ins->bailoutKind()); add(guard, ins); redefine(ins, ins->object()); } void LIRGenerator::visitGuardClass(MGuardClass* ins) { LDefinition t = temp();
--- a/js/src/jit/MIR.cpp +++ b/js/src/jit/MIR.cpp @@ -6192,19 +6192,28 @@ jit::PropertyReadNeedsTypeBarrier(JSCont if (obj->unknownProperties()) break; HeapTypeSetKey property = obj->property(NameToId(name)); if (property.maybeTypes()) { TypeSet::TypeList types; if (!property.maybeTypes()->enumerateTypes(&types)) break; - if (types.length() == 1) { + // If there is a single possible type for the property, + // optimistically add it to the observed set. Don't do this + // for the special uninitialized lexical type, which will + // never actually be observed here and will cause problems + // downstream during compilation. + if (types.length() == 1 && + (!types[0].isPrimitive() || + types[0].primitive() != JSVAL_TYPE_MAGIC)) + { // Note: the return value here is ignored. observed->addType(types[0], GetJitContext()->temp->lifoAlloc()); + break; } break; } if (!obj->proto().isObject()) break; obj = TypeSet::ObjectKey::get(obj->proto().toObject()); } while (obj);
--- a/js/src/jit/MIR.h +++ b/js/src/jit/MIR.h @@ -11485,39 +11485,47 @@ class MGuardObjectGroup }; // Guard on an object's identity, inclusively or exclusively. class MGuardObjectIdentity : public MBinaryInstruction, public SingleObjectPolicy::Data { bool bailOnEquality_; - - MGuardObjectIdentity(MDefinition* obj, MDefinition* expected, bool bailOnEquality) + BailoutKind bailoutKind_; + + MGuardObjectIdentity(MDefinition* obj, MDefinition* expected, bool bailOnEquality, + BailoutKind bailoutKind = Bailout_ObjectIdentityOrTypeGuard) : MBinaryInstruction(obj, expected), - bailOnEquality_(bailOnEquality) + bailOnEquality_(bailOnEquality), + bailoutKind_(bailoutKind) { setGuard(); setMovable(); setResultType(MIRType::Object); } public: INSTRUCTION_HEADER(GuardObjectIdentity) TRIVIAL_NEW_WRAPPERS NAMED_OPERANDS((0, object), (1, expected)) bool bailOnEquality() const { return bailOnEquality_; } + BailoutKind bailoutKind() const { + return bailoutKind_; + } bool congruentTo(const MDefinition* ins) const override { if (!ins->isGuardObjectIdentity()) return false; if (bailOnEquality() != ins->toGuardObjectIdentity()->bailOnEquality()) return false; + if (bailoutKind() != ins->toGuardObjectIdentity()->bailoutKind()) + return false; return congruentIfOperandsEqual(ins); } AliasSet getAliasSet() const override { return AliasSet::Load(AliasSet::ObjectFields); } }; // Guard on an object's class.
--- a/js/src/vm/HelperThreads.cpp +++ b/js/src/vm/HelperThreads.cpp @@ -143,54 +143,60 @@ GetSelectorRuntime(const CompilationSele { struct Matcher { JSRuntime* match(JSScript* script) { return script->runtimeFromActiveCooperatingThread(); } JSRuntime* match(JSCompartment* comp) { return comp->runtimeFromActiveCooperatingThread(); } 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()); } static bool JitDataStructuresExist(const CompilationSelector& selector) { struct Matcher { bool match(JSScript* script) { return !!script->compartment()->jitCompartment(); } bool match(JSCompartment* comp) { return !!comp->jitCompartment(); } 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 js::CancelOffThreadIonCompile(const CompilationSelector& selector, bool discardLazyLinkList) { if (!JitDataStructuresExist(selector)) return; @@ -198,30 +204,30 @@ js::CancelOffThreadIonCompile(const Comp if (!HelperThreadState().threads) return; /* Cancel any pending entries for which processing hasn't started. */ 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); } } /* Wait for in progress entries to finish up. */ bool cancelled; do { cancelled = false; 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) { helper.pause = false; unpaused = true; } cancelled = true; } @@ -231,32 +237,32 @@ js::CancelOffThreadIonCompile(const Comp if (cancelled) HelperThreadState().wait(lock, GlobalHelperThreadState::CONSUMER); } while (cancelled); /* Cancel code generation for any completed entries. */ 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); } } /* Cancel lazy linking for pending builders (attached to the ionScript). */ if (discardLazyLinkList) { MOZ_ASSERT(!selector.is<AllCompilations>()); JSRuntime* runtime = GetSelectorRuntime(selector); for (ZoneGroupsIter group(runtime); !group.done(); group.next()) { 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; } } } } #ifdef DEBUG
--- a/js/src/vm/HelperThreads.h +++ b/js/src/vm/HelperThreads.h @@ -489,21 +489,23 @@ StartOffThreadIonCompile(JSContext* cx, /* * Schedule deletion of Ion compilation data. */ bool StartOffThreadIonFree(jit::IonBuilder* builder, const AutoLockHelperThreadState& lock); struct AllCompilations {}; struct ZonesInState { JSRuntime* runtime; JS::Zone::GCState state; }; +struct CompilationsUsingNursery { JSRuntime* runtime; }; using CompilationSelector = mozilla::Variant<JSScript*, JSCompartment*, ZonesInState, JSRuntime*, + CompilationsUsingNursery, AllCompilations>; /* * Cancel scheduled or in progress Ion compilations. */ void CancelOffThreadIonCompile(const CompilationSelector& selector, bool discardLazyLinkList); @@ -527,16 +529,22 @@ CancelOffThreadIonCompile(JSRuntime* run inline void CancelOffThreadIonCompile(JSRuntime* runtime) { CancelOffThreadIonCompile(CompilationSelector(runtime), true); } inline void +CancelOffThreadIonCompilesUsingNurseryPointers(JSRuntime* runtime) +{ + CancelOffThreadIonCompile(CompilationSelector(CompilationsUsingNursery{runtime}), true); +} + +inline void CancelOffThreadIonCompile() { CancelOffThreadIonCompile(CompilationSelector(AllCompilations()), false); } #ifdef DEBUG bool HasOffThreadIonCompile(JSCompartment* comp);
--- a/js/src/vm/Interpreter-inl.h +++ b/js/src/vm/Interpreter-inl.h @@ -393,17 +393,17 @@ InitGlobalLexicalOperation(JSContext* cx JSScript* script, jsbytecode* pc, HandleValue value) { MOZ_ASSERT_IF(!script->hasNonSyntacticScope(), lexicalEnvArg == &cx->global()->lexicalEnvironment()); MOZ_ASSERT(*pc == JSOP_INITGLEXICAL); Rooted<LexicalEnvironmentObject*> lexicalEnv(cx, lexicalEnvArg); RootedShape shape(cx, lexicalEnv->lookup(cx, script->getName(pc))); MOZ_ASSERT(shape); - lexicalEnv->setSlot(shape->slot(), value); + lexicalEnv->setSlotWithType(cx, shape, value); } inline bool InitPropertyOperation(JSContext* cx, JSOp op, HandleObject obj, HandleId id, HandleValue rhs) { if (obj->is<PlainObject>() || obj->is<JSFunction>()) { unsigned propAttrs = GetInitDataPropAttrs(op); return NativeDefineProperty(cx, obj.as<NativeObject>(), id, rhs, nullptr, nullptr,
--- a/js/src/vm/TypeInference.cpp +++ b/js/src/vm/TypeInference.cpp @@ -2106,17 +2106,17 @@ class ConstraintDataConstantProperty JSCompartment* maybeCompartment() { return nullptr; } }; } /* anonymous namespace */ bool HeapTypeSetKey::constant(CompilerConstraintList* constraints, Value* valOut) { - if (nonData(constraints)) + if (nonData(constraints) || !object()->isSingleton()) return false; // Only singleton object properties can be marked as constants. JSObject* obj = object()->singleton(); if (!obj || !obj->isNative()) return false; if (maybeTypes() && maybeTypes()->nonConstantProperty())
--- a/js/xpconnect/crashtests/732870.html +++ b/js/xpconnect/crashtests/732870.html @@ -13,11 +13,11 @@ function boom() frameDoc.close(); document.documentElement.removeAttribute("class"); } </script> </head> <body onload="boom();"> -<iframe id="f" src="data:text/html,1"></iframe> +<iframe id="f" srcdoc="<html>1</html>"></iframe> </body> </html>
--- a/js/xpconnect/crashtests/751995.html +++ b/js/xpconnect/crashtests/751995.html @@ -27,10 +27,10 @@ function boom() frameDoc().write("2"); // All done. document.documentElement.removeAttribute("class"); } </script> </head> -<body onload="boom();"><iframe id="f" src="data:text/html,1"></iframe></body> +<body onload="boom();"><iframe id="f" srcdoc="1"></iframe></body> </html>
--- a/js/xpconnect/crashtests/753162.html +++ b/js/xpconnect/crashtests/753162.html @@ -14,10 +14,10 @@ function boom() }; frameDoc.addEventListener("DOMAttrModified", f); frameDoc.write("<html d>"); } </script> </head> -<body onload="boom();"><iframe id="f" src="data:text/html,1"></iframe></body> +<body onload="boom();"><iframe id="f" srcdoc="1"></iframe></body> </html>
--- a/js/xpconnect/crashtests/806751.html +++ b/js/xpconnect/crashtests/806751.html @@ -16,11 +16,11 @@ function boom() function finish() { document.documentElement.removeAttribute('class'); } </script> </head> <body onload="boom();"> -<iframe id="frame" src="data:text/html,1"></iframe> +<iframe id="frame" srcdoc="1"></iframe> </body> </html>
--- a/js/xpconnect/crashtests/851418.html +++ b/js/xpconnect/crashtests/851418.html @@ -13,11 +13,11 @@ function boom() frameRoot.onload; document.documentElement.removeAttribute("class"); } </script> </head> <body onload="boom();"> -<iframe id="f" src="data:text/html,1"></iframe> +<iframe id="f" srcdoc="1"></iframe> </body> </html>
--- a/js/xpconnect/crashtests/898939.html +++ b/js/xpconnect/crashtests/898939.html @@ -8,11 +8,11 @@ function boom() { document.documentElement.appendChild(document.getElementById("f").contentDocument.getElementById("m")); document.documentElement.removeAttribute("class"); } </script> </head> <body onload="boom();"> -<iframe id="f" src="data:text/html;charset=UTF-8,<!DOCTYPE html><body><marquee id=m>"></iframe> +<iframe id="f" srcdoc="<!DOCTYPE html><body><marquee id=m>"></iframe> </body> </html>
--- a/js/xpconnect/loader/XPCOMUtils.jsm +++ b/js/xpconnect/loader/XPCOMUtils.jsm @@ -452,19 +452,19 @@ this.XPCOMUtils = { /** * Allows you to fake a relative import. Expects the global object from the * module that's calling us, and the relative filename that we wish to import. */ importRelative: function XPCOMUtils__importRelative(that, path, scope) { 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); }, /** * generates a singleton nsIFactory implementation that can be used as * the _xpcom_factory of the component. * @param aServiceConstructor * Constructor function of the component. */
--- a/js/xpconnect/loader/mozJSComponentLoader.cpp +++ b/js/xpconnect/loader/mozJSComponentLoader.cpp @@ -191,16 +191,17 @@ ReportOnCallerUTF8(JSCLContextHelper& he va_end(ap); return NS_OK; } mozJSComponentLoader::mozJSComponentLoader() : mModules(16), mImports(16), mInProgressImports(16), + mLocations(16), mInitialized(false) { MOZ_ASSERT(!sSelf, "mozJSComponentLoader should be a singleton"); sSelf = this; } #define ENSURE_DEP(name) { nsresult rv = Ensure##name(); NS_ENSURE_SUCCESS(rv, rv); } @@ -241,40 +242,37 @@ class MOZ_STACK_CLASS ComponentLoaderInf nullptr, // aLoadGroup nullptr, // aCallbacks nsIRequest::LOAD_NORMAL, mIOService); } 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; } + 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) { nsresult rv = EnsureURI(); NS_ENSURE_SUCCESS(rv, rv); return mURI->GetSpec(aLocation); } private: const nsACString& mLocation; nsCOMPtr<nsIIOService> mIOService; nsCOMPtr<nsIURI> mURI; nsCOMPtr<nsIChannel> mScriptChannel; nsCOMPtr<nsIURI> mResolvedURI; - Maybe<nsAutoCString> mKey; // This is safe because we're MOZ_STACK_CLASS }; #undef BEGIN_ENSURE #undef ENSURE_DEPS #undef ENSURE_DEP mozJSComponentLoader::~mozJSComponentLoader() { @@ -444,16 +442,17 @@ SizeOfTableExcludingThis(const nsBaseHas } size_t 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; } void mozJSComponentLoader::CreateLoaderGlobal(JSContext* aCx, nsACString& aLocation, JSAddonId* aAddonID, @@ -608,18 +607,20 @@ mozJSComponentLoader::ObjectForLocation( // Before compiling the script, first check to see if we have it in // the startupcache. Note: as a rule, startupcache errors are not fatal // to loading the script, since we can always slow-load. 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); if (!script && cache) { ReadCachedScript(cache, cachePath, cx, &script); } if (script) { @@ -756,16 +757,17 @@ mozJSComponentLoader::ObjectForLocation( void mozJSComponentLoader::UnloadModules() { mInitialized = false; mInProgressImports.Clear(); mImports.Clear(); + mLocations.Clear(); for (auto iter = mModules.Iter(); !iter.Done(); iter.Next()) { iter.Data()->Clear(); iter.Remove(); } } nsresult @@ -899,59 +901,66 @@ mozJSComponentLoader::ImportInto(const n nsresult rv; if (!mInitialized) { rv = ReallyInit(); NS_ENSURE_SUCCESS(rv, rv); } ComponentLoaderInfo info(aLocation); - rv = info.EnsureResolvedURI(); - NS_ENSURE_SUCCESS(rv, rv); - - // get the JAR if there is one - nsCOMPtr<nsIJARURI> jarURI; - jarURI = do_QueryInterface(info.ResolvedURI(), &rv); - nsCOMPtr<nsIFileURL> baseFileURL; - if (NS_SUCCEEDED(rv)) { - nsCOMPtr<nsIURI> 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<nsIFile> sourceFile; - rv = baseFileURL->GetFile(getter_AddRefs(sourceFile)); - NS_ENSURE_SUCCESS(rv, rv); - - nsCOMPtr<nsIFile> sourceLocalFile; - sourceLocalFile = do_QueryInterface(sourceFile, &rv); - NS_ENSURE_SUCCESS(rv, rv); rv = info.EnsureKey(); NS_ENSURE_SUCCESS(rv, rv); ModuleEntry* mod; nsAutoPtr<ModuleEntry> newEntry; if (!mImports.Get(info.Key(), &mod) && !mInProgressImports.Get(info.Key(), &mod)) { 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<nsIJARURI> jarURI; + jarURI = do_QueryInterface(info.ResolvedURI(), &rv); + nsCOMPtr<nsIFileURL> baseFileURL; + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIURI> 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<nsIFile> sourceFile; + 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(); 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); mInProgressImports.Remove(info.Key()); if (NS_FAILED(rv)) { if (!exception.isUndefined()) { // An exception was thrown during compilation. Propagate it @@ -1107,16 +1116,17 @@ mozJSComponentLoader::Unload(const nsACS return NS_OK; } ComponentLoaderInfo info(aLocation); rv = info.EnsureKey(); NS_ENSURE_SUCCESS(rv, rv); ModuleEntry* mod; if (mImports.Get(info.Key(), &mod)) { + mLocations.Remove(mod->resolvedURL); mImports.Remove(info.Key()); } return NS_OK; } NS_IMETHODIMP mozJSComponentLoader::Observe(nsISupports* subject, const char* topic,
--- a/js/xpconnect/loader/mozJSComponentLoader.h +++ b/js/xpconnect/loader/mozJSComponentLoader.h @@ -139,29 +139,35 @@ class mozJSComponentLoader : public mozi size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; static already_AddRefed<nsIFactory> GetFactory(const mozilla::Module& module, const mozilla::Module::CIDEntry& entry); nsCOMPtr<xpcIJSGetFactory> getfactoryobj; JS::PersistentRootedObject obj; JS::PersistentRootedScript thisObjectKey; - char* location; + char* location; + nsCString resolvedURL; }; friend class ModuleEntry; static size_t DataEntrySizeOfExcludingThis(const nsACString& aKey, ModuleEntry* const& aData, mozilla::MallocSizeOf aMallocSizeOf, void* arg); static size_t ClassEntrySizeOfExcludingThis(const nsACString& aKey, const nsAutoPtr<ModuleEntry>& aData, mozilla::MallocSizeOf aMallocSizeOf, void* arg); // Modules are intentionally leaked, but still cleared. nsDataHashtable<nsCStringHashKey, ModuleEntry*> mModules; nsClassHashtable<nsCStringHashKey, ModuleEntry> mImports; nsDataHashtable<nsCStringHashKey, ModuleEntry*> 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<nsCStringHashKey, nsCString> mLocations; + bool mInitialized; }; #endif
--- a/js/xpconnect/tests/components/js/xpctest_attributes.js +++ b/js/xpconnect/tests/components/js/xpctest_attributes.js @@ -1,12 +1,12 @@ /* 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 = { /* Boilerplate */ QueryInterface: XPCOMUtils.generateQI([Components.interfaces["nsIXPCTestObjectReadWrite"]]), contractID: "@mozilla.org/js/xpc/test/js/ObjectReadWrite;1", classID: Components.ID("{8ff41d9c-66e9-4453-924a-7d8de0a5e966}"),
--- a/js/xpconnect/tests/components/js/xpctest_bug809674.js +++ b/js/xpconnect/tests/components/js/xpctest_bug809674.js @@ -1,12 +1,12 @@ /* 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 = { /* Boilerplate */ QueryInterface: XPCOMUtils.generateQI([Components.interfaces["nsIXPCTestBug809674"]]), contractID: "@mozilla.org/js/xpc/test/js/Bug809674;1", classID: Components.ID("{2df46559-da21-49bf-b863-0d7b7bbcbc73}"),
--- a/js/xpconnect/tests/components/js/xpctest_interfaces.js +++ b/js/xpconnect/tests/components/js/xpctest_interfaces.js @@ -1,12 +1,12 @@ /* 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 = { /* Boilerplate */ QueryInterface: XPCOMUtils.generateQI([Components.interfaces["nsIXPCTestInterfaceA"]]), contractID: "@mozilla.org/js/xpc/test/js/TestInterfaceA;1", classID: Components.ID("{3c8fd2f5-970c-42c6-b5dd-cda1c16dcfd8}"),
--- a/js/xpconnect/tests/components/js/xpctest_params.js +++ b/js/xpconnect/tests/components/js/xpctest_params.js @@ -1,12 +1,12 @@ /* 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() { } /* For once I'm happy that JS is weakly typed. */ function f(a, b) { var rv = b.value; b.value = a;
--- a/js/xpconnect/tests/components/js/xpctest_returncode_child.js +++ b/js/xpconnect/tests/components/js/xpctest_returncode_child.js @@ -1,13 +1,13 @@ /* 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/. */ 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 = { /* Boilerplate */ QueryInterface: XPCOMUtils.generateQI([Ci["nsIXPCTestReturnCodeChild"]]), contractID: "@mozilla.org/js/xpc/test/js/ReturnCodeChild;1", classID: Components.ID("{38dd78aa-467f-4fad-8dcf-4383a743e235}"),
--- a/js/xpconnect/tests/components/js/xpctest_utils.js +++ b/js/xpconnect/tests/components/js/xpctest_utils.js @@ -1,13 +1,13 @@ /* 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 TestUtils() {} TestUtils.prototype = { /* Boilerplate */ QueryInterface: XPCOMUtils.generateQI([Components.interfaces["nsIXPCTestUtils"]]), contractID: "@mozilla.org/js/xpc/test/js/TestUtils;1", classID: Components.ID("{e86573c4-a384-441a-8c92-7b99e8575b28}"),
--- a/js/xpconnect/tests/unit/test_import.js +++ b/js/xpconnect/tests/unit/test_import.js @@ -34,21 +34,18 @@ function run_test() { // try on a new object using the resolved URL var res = Components.classes["@mozilla.org/network/protocol;1?name=resource"] .getService(Components.interfaces.nsIResProtocolHandler); var resURI = res.newURI("resource://gre/modules/XPCOMUtils.jsm"); 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; try { Components.utils.import("resource://gre/modules/XPCOMUtils.jsm", "wrong"); } catch (ex) { print("exception (expected): " + ex); didThrow = true;
--- a/js/xpconnect/tests/unit/test_isModuleLoaded.js +++ b/js/xpconnect/tests/unit/test_isModuleLoaded.js @@ -16,18 +16,9 @@ function run_test() { "isModuleLoaded returned correct value for non-loaded module"); try { Cu.import("resource://gre/modules/ISO8601DateUtils1.jsm"); do_check_true(false, "Should have thrown while trying to load a non existing file"); } 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"); - } }
--- a/js/xpconnect/tests/unit/test_returncode.js +++ b/js/xpconnect/tests/unit/test_returncode.js @@ -1,15 +1,15 @@ /* 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/. */ 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); let messages = consoleService.getMessageArray().map((m) => m.toString()); // reset ready for the next call. consoleService.reset(); return messages; }
--- a/layout/base/crashtests/606432-1.html +++ b/layout/base/crashtests/606432-1.html @@ -15,10 +15,10 @@ function boom() function finish() { document.documentElement.className = ""; } </script> </head> -<body onload="setTimeout(boom, 200);"><iframe id="f" src="data:application/xhtml+xml,<html xmlns='http://www.w3.org/1999/xhtml'></html>"></iframe></body> +<body onload="setTimeout(boom, 200);"><iframe id="f" srcdoc="<html xmlns='http://www.w3.org/1999/xhtml'></html>"></iframe></body> </html>
--- a/layout/base/crashtests/645572-1.html +++ b/layout/base/crashtests/645572-1.html @@ -13,17 +13,17 @@ function start(){ tmp.id='ifr42257'; o230.ownerDocument.documentElement.appendChild(tmp); start_dataiframe11(); //window.setTimeout('start_dataiframe11()',100); }function start_dataiframe11(){ o232=o230.ownerDocument.getElementById('ifr42257').contentDocument.documentElement; o234=o196; tmp=o234.ownerDocument.createElement('iframe'); - tmp.src='data:text/html,' + escape("<q id='element2'><q id='element3'><q id='element4'><dd style id='element6'>"); + tmp.srcdoc="<q id='element2'><q id='element3'><q id='element4'><dd style id='element6'>"; tmp.id='ifr22371'; tmp.addEventListener("load", start_dataiframe12); o234.ownerDocument.documentElement.appendChild(tmp); }function start_dataiframe12(){ o239=o234.ownerDocument.getElementById('ifr22371').contentDocument.getElementById('element2'); o240=o234.ownerDocument.getElementById('ifr22371').contentDocument.getElementById('element3'); o241=o234.ownerDocument.getElementById('ifr22371').contentDocument.getElementById('element4'); o243=o234.ownerDocument.getElementById('ifr22371').contentDocument.getElementById('element6');
--- a/layout/base/nsLayoutUtils.cpp +++ b/layout/base/nsLayoutUtils.cpp @@ -6020,17 +6020,18 @@ nsLayoutUtils::PaintTextShadow(const nsI nsRect shadowRect(aTextRect); shadowRect.MoveBy(shadowOffset); nsPresContext* presCtx = aFrame->PresContext(); nsContextBoxBlur contextBoxBlur; gfxContext* shadowContext = contextBoxBlur.Init(shadowRect, 0, blurRadius, presCtx->AppUnitsPerDevPixel(), - aDestCtx, aDirtyRect, nullptr); + aDestCtx, aDirtyRect, nullptr, + nsContextBoxBlur::DISABLE_HARDWARE_ACCELERATION_BLUR); if (!shadowContext) continue; nscolor shadowColor; if (shadowDetails->mHasColor) shadowColor = shadowDetails->mColor; else shadowColor = aForegroundColor;
--- a/layout/base/tests/mochitest.ini +++ b/layout/base/tests/mochitest.ini @@ -321,17 +321,17 @@ support-files = [test_remote_frame.html] [test_resize_flush.html] support-files = resize_flush_iframe.html [test_scroll_event_ordering.html] [test_scroll_selection_into_view.html] skip-if = toolkit == 'android' # Bug 1355844 support-files = scroll_selection_into_view_window.html [test_scroll_snapping.html] -skip-if = toolkit == 'android' # Bug 1355851 +skip-if = toolkit == 'android' || os == 'win' # Bug 1355851, win Bug 1379810 [test_scroll_snapping_scrollbars.html] skip-if = toolkit == 'android' # Bug 1355851 [test_transformed_scrolling_repaints.html] [test_transformed_scrolling_repaints_2.html] [test_transformed_scrolling_repaints_3.html] support-files = transformed_scrolling_repaints_3_window.html # *** Please maintain alphabetical ordering when adding new tests ***
--- a/layout/base/tests/test_bug399284.html +++ b/layout/base/tests/test_bug399284.html @@ -97,17 +97,17 @@ function encodeUTF16LE(string) encodedString += encodeURI(string.charAt(i)); encodedString += "%00"; } return encodedString; } function testFontSize(frame) { - var iframeDoc = $(frame).contentDocument; + var iframeDoc = SpecialPowers.wrap($(frame)).contentDocument; var size = parseInt(iframeDoc.defaultView. getComputedStyle(iframeDoc.getElementById("testPara"), null). getPropertyValue("font-size")); ok(size > 0, "font size assigned for " + frame); } </script> </pre>
--- a/layout/generic/crashtests/324318-1.html +++ b/layout/generic/crashtests/324318-1.html @@ -19,11 +19,11 @@ function init() { } window.addEventListener("load", init); document.documentElement.setAttribute("class", "reftest-wait"); </script> <frameset resizable="yes" rows="50%,*"> - <frame name="one" src="data:text/html,<table border='1'><tr><td>tdc</td></tr></table>"> - <frame name="two" src="data:text/html,"> + <frame name="one" src="file_324318-1.html"> + <frame name="two" src="empty.html"> </frameset>
new file mode 100644 --- /dev/null +++ b/layout/generic/crashtests/empty.html @@ -0,0 +1,1 @@ +<html></html>
new file mode 100644 --- /dev/null +++ b/layout/generic/crashtests/file_324318-1.html @@ -0,0 +1,1 @@ +<table border='1'><tr><td>tdc</td></tr></table>
--- a/layout/generic/nsTextFrame.cpp +++ b/layout/generic/nsTextFrame.cpp @@ -6967,22 +6967,24 @@ nsTextFrame::PaintShadows(nsCSSShadowArr // Add bounds of text decorations gfxRect decorationRect(0, -shadowMetrics.mAscent, shadowMetrics.mAdvanceWidth, shadowMetrics.mAscent + shadowMetrics.mDescent); shadowMetrics.mBoundingBox.UnionRect(shadowMetrics.mBoundingBox, decorationRect); // If the textrun uses any color or SVG fonts, we need to force use of a mask // for shadow rendering even if blur radius is zero. - uint32_t blurFlags = 0; + // Force disable hardware acceleration for text shadows since it's usually + // more expensive than just doing it on the CPU. + uint32_t blurFlags = nsContextBoxBlur::DISABLE_HARDWARE_ACCELERATION_BLUR; uint32_t numGlyphRuns; const gfxTextRun::GlyphRun* run = mTextRun->GetGlyphRuns(&numGlyphRuns); while (numGlyphRuns-- > 0) { if (run->mFont->AlwaysNeedsMaskForShadow()) { - blurFlags = nsContextBoxBlur::FORCE_MASK; + blurFlags |= nsContextBoxBlur::FORCE_MASK; break; } run++; } if (mTextRun->IsVertical()) { Swap(shadowMetrics.mBoundingBox.x, shadowMetrics.mBoundingBox.y); Swap(shadowMetrics.mBoundingBox.width, shadowMetrics.mBoundingBox.height);
--- a/layout/painting/nsCSSRendering.cpp +++ b/layout/painting/nsCSSRendering.cpp @@ -4282,23 +4282,26 @@ nsContextBoxBlur::Init(const nsRect& aRe gfxMatrix transform = aDestinationCtx->CurrentMatrix(); rect = transform.TransformBounds(rect); mPreTransformed = !transform.IsIdentity(); // Create the temporary surface for blurring dirtyRect = transform.TransformBounds(dirtyRect); + bool useHardwareAccel = !(aFlags & DISABLE_HARDWARE_ACCELERATION_BLUR); if (aSkipRect) { gfxRect skipRect = transform.TransformBounds(*aSkipRect); mContext = mAlphaBoxBlur.Init(aDestinationCtx, rect, spreadRadius, - blurRadius, &dirtyRect, &skipRect); + blurRadius, &dirtyRect, &skipRect, + useHardwareAccel); } else { mContext = mAlphaBoxBlur.Init(aDestinationCtx, rect, spreadRadius, - blurRadius, &dirtyRect, nullptr); + blurRadius, &dirtyRect, nullptr, + useHardwareAccel); } if (mContext) { // we don't need to blur if skipRect is equal to rect // and mContext will be nullptr mContext->Multiply(transform); } return mContext;
--- a/layout/painting/nsCSSRendering.h +++ b/layout/painting/nsCSSRendering.h @@ -710,17 +710,18 @@ protected: */ class nsContextBoxBlur { typedef mozilla::gfx::Color Color; typedef mozilla::gfx::DrawTarget DrawTarget; typedef mozilla::gfx::RectCornerRadii RectCornerRadii; public: enum { - FORCE_MASK = 0x01 + FORCE_MASK = 0x01, + DISABLE_HARDWARE_ACCELERATION_BLUR = 0x02 }; /** * Prepares a gfxContext to draw on. Do not call this twice; if you want * to get the gfxContext again use GetContext(). * * @param aRect The coordinates of the surface to create. * All coordinates must be in app units. * This must not include the blur radius, pass
new file mode 100644 --- /dev/null +++ b/layout/reftests/bugs/381746-1-framea.html @@ -0,0 +1,1 @@ +<body onload="parent.frameOnload();">text
new file mode 100644 --- /dev/null +++ b/layout/reftests/bugs/381746-1-frameb.html @@ -0,0 +1,1 @@ +text
--- a/layout/reftests/bugs/381746-1-ref.html +++ b/layout/reftests/bugs/381746-1-ref.html @@ -4,12 +4,12 @@ </script> </head> <frameset cols="48%,52%"> - <frame src="data:text/html;charset=utf-8,text"> - <frame src="data:text/html;charset=utf-8,text"> + <frame src="381746-1-frameb.html"> + <frame src="381746-1-frameb.html"> </frameset> </html>
--- a/layout/reftests/bugs/381746-1.html +++ b/layout/reftests/bugs/381746-1.html @@ -1,22 +1,22 @@ <html class="reftest-wait"><head> <title>Testcase Bug 381746 - odd and changing border in frameset</title> -<script> -var frameOnloadCalled = false; -function frameOnload() { - if (!frameOnloadCalled) { - frameOnloadCalled = true; - return; - } - document.documentElement.removeAttribute("class"); +<script> +var frameOnloadCalled = false; +function frameOnload() { + if (!frameOnloadCalled) { + frameOnloadCalled = true; + return; + } + document.documentElement.removeAttribute("class"); } function doe() { -document.getElementsByTagName('frame')[0].src = 'data:text/html;charset=utf-8,<body onload="parent.frameOnload();">text'; -document.getElementsByTagName('frame')[1].src = 'data:text/html;charset=utf-8,<body onload="parent.frameOnload();">text'; +document.getElementsByTagName('frame')[0].src = '381746-1-framea.html'; +document.getElementsByTagName('frame')[1].src = '381746-1-framea.html'; } window.onload=doe; </script> </head> <frameset cols="48%,52%"> <frame src=""> <frame src=""> </frameset>
new file mode 100644 --- /dev/null +++ b/layout/reftests/bugs/537507-1-frame.xul @@ -0,0 +1,1 @@ +<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'><label value='Here'/></window>
--- a/layout/reftests/bugs/537507-1-ref.xul +++ b/layout/reftests/bugs/537507-1-ref.xul @@ -1,8 +1,8 @@ <?xml version="1.0"?> <?xml-stylesheet href="chrome://global/skin" type="text/css"?> <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" orient="vertical"> <label value="The iframe below should show the string 'Here'"/> -<iframe src="data:application/vnd.mozilla.xul+xml,<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'><label value='Here'/></window>"></iframe> +<iframe src="537507-1-frame.xul"></iframe> </window>
--- a/layout/reftests/bugs/537507-1.xul +++ b/layout/reftests/bugs/537507-1.xul @@ -1,10 +1,8 @@ <?xml version="1.0"?> <?xml-stylesheet href="chrome://global/skin" type="text/css"?> <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" orient="vertical"> <label value="The iframe below should show the string 'Here'"/> -<iframe src="data:application/vnd.mozilla.xul+xml,<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'><label value='Here'/></window>" - style="display: none" - onload="this.style.display = ''"></iframe> +<iframe src="537507-1-frame.xul" style="display: none" onload="this.style.display = ''"></iframe> </window>
new file mode 100644 --- /dev/null +++ b/layout/reftests/bugs/537507-2-frame.xul @@ -0,0 +1,1 @@ +<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'><label value='Here'/></window>
--- a/layout/reftests/bugs/537507-2-ref.html +++ b/layout/reftests/bugs/537507-2-ref.html @@ -1,5 +1,5 @@ <!DOCTYPE html> <body> The iframe below should show the text 'Here'<br> - <iframe src="data:application/vnd.mozilla.xul+xml,<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'><label value='Here'/></window>"></iframe> + <iframe src="537507-2-frame.xul"></iframe> </body>
--- a/layout/reftests/bugs/537507-2.html +++ b/layout/reftests/bugs/537507-2.html @@ -1,7 +1,5 @@ <!DOCTYPE html> <body> The iframe below should show the text 'Here'<br> - <iframe src="data:application/vnd.mozilla.xul+xml,<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'><label value='Here'/></window>" - style="display: none" - onload="this.style.display = ''"></iframe> + <iframe src="537507-2-frame.xul" style="display: none" onload="this.style.display = ''"></iframe> </body>
new file mode 100644 --- /dev/null +++ b/layout/reftests/bugs/858803-1-frame.xhtml @@ -0,0 +1,2 @@ +<?xml version='1.0' encoding='UTF-8'?> +<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul' style='background:yellow'></window>
--- a/layout/reftests/bugs/858803-1.html +++ b/layout/reftests/bugs/858803-1.html @@ -1,8 +1,7 @@ <!DOCTYPE HTML> <html> <body> -<iframe src="data:application/vnd.mozilla.xul+xml,<?xml version='1.0' encoding='UTF-8'?><window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul' style='background:yellow'></window>" - style="position:absolute; left:100px; top:100px; width:300px; height:300px; border:1px solid black;"> +<iframe src="858803-1-frame.xhtml" style="position:absolute; left:100px; top:100px; width:300px; height:300px; border:1px solid black;"> </iframe> </body> </html>
new file mode 100644 --- /dev/null +++ b/layout/reftests/scrolling/iframe-border-radius-frame.html @@ -0,0 +1,1 @@ +<body style='font-size:100px; overflow:hidden; background:white;'><p>Hello<p>Kitty<p>Hello<p>Kitty<p>Hello<p>Kitty<p>Hello<p>Kitty<p>Hello<p>Kitty<p>Hello<p>Kitty
--- a/layout/reftests/scrolling/iframe-border-radius-ref.html +++ b/layout/reftests/scrolling/iframe-border-radius-ref.html @@ -1,13 +1,12 @@ <!DOCTYPE HTML> <html class="reftest-wait"> <body> -<iframe src="data:text/html,<body style='font-size:100px; overflow:hidden; background:white;'><p>Hello<p>Kitty<p>Hello<p>Kitty<p>Hello<p>Kitty<p>Hello<p>Kitty<p>Hello<p>Kitty<p>Hello<p>Kitty" - id="f" style="width:500px; height:500px; border-radius:100px; border:none;"></iframe> +<iframe src="iframe-border-radius-frame.html" id="f" style="width:500px; height:500px; border-radius:100px; border:none;"></iframe> <script> var f = document.getElementById("f"); function doTest() { f.contentWindow.scrollTo(0, 80); document.documentElement.removeAttribute('class'); } document.addEventListener("MozReftestInvalidate", doTest); </script>
--- a/layout/reftests/scrolling/iframe-border-radius.html +++ b/layout/reftests/scrolling/iframe-border-radius.html @@ -1,13 +1,12 @@ <!DOCTYPE HTML> <html class="reftest-wait"> <body> -<iframe src="data:text/html,<body style='font-size:100px; overflow:hidden; background:white;'><p>Hello<p>Kitty<p>Hello<p>Kitty<p>Hello<p>Kitty<p>Hello<p>Kitty<p>Hello<p>Kitty<p>Hello<p>Kitty" - id="f" style="width:500px; height:500px; border-radius:100px; border:none;"></iframe> +<iframe src="iframe-border-radius-frame.html" id="f" style="width:500px; height:500px; border-radius:100px; border:none;"></iframe> <script> var f = document.getElementById("f"); var count = 0; function doTest() { ++count; f.contentWindow.scrollTo(0, count*20); if (count == 4) { document.documentElement.removeAttribute("class");
--- a/layout/style/crashtests/592698-1.html +++ b/layout/style/crashtests/592698-1.html @@ -1,12 +1,12 @@ <!DOCTYPE html> <html class="reftest-wait"> <iframe id="x" - src="data:text/html;charset=utf-8,%3Cdiv%20id%3D%22a%22%3Eaaa"></iframe> + srcdoc="<div id='a'>aaa"></iframe> <script> window.onload = function() { window.frames[0].document.getElementById("a").setAttribute("style", '-moz-transition-property: color;' + '-moz-transition-duration: 10s;' + 'transition-property: color;' + 'transition-duration: 10s; ' +
--- a/layout/style/test/test_media_queries.html +++ b/layout/style/test/test_media_queries.html @@ -153,18 +153,19 @@ function run() { // Test cloning var sheet = "@media " + q + " { body { text-decoration: underline } }" var sheeturl = "data:text/css," + escape(sheet); var link = "<link rel='stylesheet' href='" + sheeturl + "'>"; var htmldoc = "<!DOCTYPE HTML>" + link + link + "<body>"; 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; clonedsheet.insertRule("#nonexistent { color: purple}", 1); // remove the uncloned sheet links[0].remove(); var ser3 = clonedsheet.cssRules[0].media.mediaText;
--- a/layout/style/test/test_namespace_rule.html +++ b/layout/style/test/test_namespace_rule.html @@ -11,19 +11,19 @@ <script class="testbody" type="text/javascript"> SimpleTest.waitForExplicitFinish(); var HTML_NS = "http://www.w3.org/1999/xhtml"; 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() { var style_elem = ifdoc.createElement("style"); style_elem.setAttribute("type", "text/css"); ifdoc.getElementsByTagName("head")[0].appendChild(style_elem); var style_text = ifdoc.createCDATASection(""); style_elem.appendChild(style_text);
--- a/layout/style/test/test_property_syntax_errors.html +++ b/layout/style/test/test_property_syntax_errors.html @@ -79,17 +79,19 @@ function check_empty_value_rejected(decl "empty value '" + property + ":" + emptyval + "' is balanced and does not lead to parsing errors afterwards"); decl.cssText = ""; } 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) { var info = gCSSProperties[property]; check_empty_value_rejected(gDeclaration, "", property); check_empty_value_rejected(gDeclaration, " ", property);
--- a/layout/style/test/test_selectors.html +++ b/layout/style/test/test_selectors.html @@ -129,18 +129,19 @@ function run() { var html_doc = "<!DOCTYPE HTML>" + style_sheet_link + style_sheet_link + "<body>"; if (typeof(body_contents) == "string") { html_doc += body_contents; } 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); } var links = clonedoc.getElementsByTagName("link"); // cause a clone links[1].sheet.insertRule("#nonexistent { color: purple}", idx + 1); @@ -216,18 +217,19 @@ function run() { var style_sheet_link = "<link rel='stylesheet' href='" + style_sheet + "'>"; var html_doc = "<!DOCTYPE HTML>" + style_sheet_link + style_sheet_link + "<p></p>"; 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); // remove the uncloned sheet links[0].remove(); should_match = clonedoc.getElementsByTagName("p")[0]; is(clonewin.getComputedStyle(should_match).zIndex, String(zi),
--- a/layout/style/test/test_value_cloning.html +++ b/layout/style/test/test_value_cloning.html @@ -106,24 +106,26 @@ function doTest() function iframe_loaded(event) { if (event.target != iframe) return; 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 == "") { isnot(cur_ser, "", "serialization should be nonempty for " + current_item.prop + ": " + current_item.value); } start_ser.push(cur_ser);
--- a/media/webrtc/trunk/webrtc/modules/desktop_capture/screen_capturer_mac.mm +++ b/media/webrtc/trunk/webrtc/modules/desktop_capture/screen_capturer_mac.mm @@ -24,16 +24,17 @@ #include <dlfcn.h> #include <IOKit/pwr_mgt/IOPMLib.h> #include <OpenGL/CGLMacro.h> #include <OpenGL/OpenGL.h> #include "webrtc/base/checks.h" #include "webrtc/base/constructormagic.h" #include "webrtc/base/macutils.h" +#include "webrtc/base/criticalsection.h" #include "webrtc/base/timeutils.h" #include "webrtc/modules/desktop_capture/desktop_capturer.h" #include "webrtc/modules/desktop_capture/desktop_capture_options.h" #include "webrtc/modules/desktop_capture/desktop_frame.h" #include "webrtc/modules/desktop_capture/desktop_geometry.h" #include "webrtc/modules/desktop_capture/desktop_region.h" #include "webrtc/modules/desktop_capture/mac/desktop_configuration.h" #include "webrtc/modules/desktop_capture/mac/desktop_configuration_monitor.h" @@ -326,16 +327,24 @@ class ScreenCapturerMac : public Desktop const CGRect *rect_array); static void ScreenRefreshCallback(CGRectCount count, const CGRect *rect_array, void *user_parameter); static void ScreenUpdateMoveCallback(CGScreenUpdateMoveDelta delta, size_t count, const CGRect *rect_array, void *user_parameter); + struct ScreenCallbackData { + explicit ScreenCallbackData(ScreenCapturerMac* capturer) + : capturer(capturer) {} + rtc::CriticalSection crit_sect_; + ScreenCapturerMac* GUARDED_BY(crit_sect_) capturer; + }; + + ScreenCallbackData* screen_callback_data_; #endif std::unique_ptr<DesktopFrame> CreateFrame(); Callback* callback_ = nullptr; CGLContextObj cgl_context_ = nullptr; ScopedPixelBufferObject pixel_buffer_object_; @@ -412,26 +421,28 @@ class InvertedDesktopFrame : public Desk private: std::unique_ptr<DesktopFrame> original_frame_; RTC_DISALLOW_COPY_AND_ASSIGN(InvertedDesktopFrame); }; ScreenCapturerMac::ScreenCapturerMac( rtc::scoped_refptr<DesktopConfigurationMonitor> desktop_config_monitor) - : desktop_config_monitor_(desktop_config_monitor) { + : screen_callback_data_(new ScreenCallbackData(this)) + , desktop_config_monitor_(desktop_config_monitor) { #if defined(MAC_OS_X_VERSION_10_8) && \ (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_8) display_stream_manager_ = new DisplayStreamManager; #endif } ScreenCapturerMac::~ScreenCapturerMac() { ReleaseBuffers(); - UnregisterRefreshAndMoveHandlers(); + rtc::CritScope lock(&screen_callback_data_->crit_sect_); + screen_callback_data_->capturer = nullptr; #if defined(MAC_OS_X_VERSION_10_8) && \ (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_8) display_stream_manager_->PrepareForSelfDestruction(); #endif dlclose(app_services_library_); dlclose(opengl_library_); } @@ -1029,24 +1040,24 @@ bool ScreenCapturerMac::RegisterRefreshA CFRunLoopSourceRef source = CGDisplayStreamGetRunLoopSource(display_stream); CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode); display_stream_manager_->SaveStream(unique_id, display_stream); } } #else CGError err = CGRegisterScreenRefreshCallback( - ScreenCapturerMac::ScreenRefreshCallback, this); + ScreenCapturerMac::ScreenRefreshCallback, screen_callback_data_); if (err != kCGErrorSuccess) { LOG(LS_ERROR) << "CGRegisterScreenRefreshCallback " << err; return false; } err = CGScreenRegisterMoveCallback( - ScreenCapturerMac::ScreenUpdateMoveCallback, this); + ScreenCapturerMac::ScreenUpdateMoveCallback, screen_callback_data_); if (err != kCGErrorSuccess) { LOG(LS_ERROR) << "CGScreenRegisterMoveCallback " << err; return false; } #endif return true; } @@ -1093,31 +1104,53 @@ void ScreenCapturerMac::ScreenUpdateMove // Currently we just treat move events the same as refreshes. ScreenRefresh(count, refresh_rects); } void ScreenCapturerMac::ScreenRefreshCallback(CGRectCount count, const CGRect* rect_array, void* user_parameter) { - ScreenCapturerMac* capturer = - reinterpret_cast<ScreenCapturerMac*>(user_parameter); - if (capturer->screen_pixel_bounds_.is_empty()) - capturer->ScreenConfigurationChanged(); - capturer->ScreenRefresh(count, rect_array); + ScreenCallbackData* screen_callback_data = + reinterpret_cast<ScreenCallbackData*>(user_parameter); + + rtc::CritScope lock(&screen_callback_data->crit_sect_); + if (!screen_callback_data->capturer) { + CGUnregisterScreenRefreshCallback( + ScreenCapturerMac::ScreenRefreshCallback, screen_callback_data); + CGScreenUnregisterMoveCallback( + ScreenCapturerMac::ScreenUpdateMoveCallback, screen_callback_data); + delete screen_callback_data; + return; + } + + if (screen_callback_data->capturer->screen_pixel_bounds_.is_empty()) + screen_callback_data->capturer->ScreenConfigurationChanged(); + screen_callback_data->capturer->ScreenRefresh(count, rect_array); } void ScreenCapturerMac::ScreenUpdateMoveCallback( CGScreenUpdateMoveDelta delta, size_t count, const CGRect* rect_array, void* user_parameter) { - ScreenCapturerMac* capturer = - reinterpret_cast<ScreenCapturerMac*>(user_parameter); - capturer->ScreenUpdateMove(delta, count, rect_array); + ScreenCallbackData* screen_callback_data = + reinterpret_cast<ScreenCallbackData*>(user_parameter); + + rtc::CritScope lock(&screen_callback_data->crit_sect_); + if (!screen_callback_data->capturer) { + CGUnregisterScreenRefreshCallback( + ScreenCapturerMac::ScreenRefreshCallback, screen_callback_data); + CGScreenUnregisterMoveCallback( + ScreenCapturerMac::ScreenUpdateMoveCallback, screen_callback_data); + delete screen_callback_data; + return; + } + + screen_callback_data->capturer->ScreenUpdateMove(delta, count, rect_array); } #endif std::unique_ptr<DesktopFrame> ScreenCapturerMac::CreateFrame() { std::unique_ptr<DesktopFrame> frame( new BasicDesktopFrame(screen_pixel_bounds_.size())); frame->set_dpi(DesktopVector(kStandardDPI * dip_to_pixel_scale_, kStandardDPI * dip_to_pixel_scale_));
--- a/modules/libjar/nsJAR.cpp +++ b/modules/libjar/nsJAR.cpp @@ -320,29 +320,29 @@ nsJAR::GetInputStream(const nsACString & NS_IMETHODIMP nsJAR::GetInputStreamWithSpec(const nsACString& aJarDirSpec, const nsACString &aEntryName, nsIInputStream** result) { NS_ENSURE_ARG_POINTER(result); // Watch out for the jar:foo.zip!/ (aDir is empty) top-level special case! nsZipItem *item = nullptr; - const char *entry = PromiseFlatCString(aEntryName).get(); - if (*entry) { + const nsCString& entry = PromiseFlatCString(aEntryName); + if (*entry.get()) { // First check if item exists in jar - item = mZip->GetItem(entry); + item = mZip->GetItem(entry.get()); if (!item) return NS_ERROR_FILE_TARGET_DOES_NOT_EXIST; } nsJARInputStream* jis = new nsJARInputStream(); // addref now so we can call InitFile/InitDirectory() NS_ADDREF(*result = jis); nsresult rv = NS_OK; if (!item || item->IsDirectory()) { - rv = jis->InitDirectory(this, aJarDirSpec, entry); + rv = jis->InitDirectory(this, aJarDirSpec, entry.get()); } else { rv = jis->InitFile(this, item); } if (NS_FAILED(rv)) { NS_RELEASE(*result); } return rv; }
--- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -5756,16 +5756,22 @@ pref("security.mixed_content.hsts_primin // TODO: Bug 1324406: Treat 'data:' documents as unique, opaque origins // If true, data: URIs will be treated as unique opaque origins, hence will use // a NullPrincipal as the security context. // Otherwise it will inherit the origin from parent node, this is the legacy // 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); #else pref("dom.storageManager.enabled", false); #endif pref("dom.storageManager.prompt.testing", false);
--- a/netwerk/base/nsIOService.cpp +++ b/netwerk/base/nsIOService.cpp @@ -162,16 +162,17 @@ static const char kProfileChangeNetTeard static const char kProfileChangeNetRestoreTopic[] = "profile-change-net-restore"; static const char kProfileDoChange[] = "profile-do-change"; // Necko buffer defaults uint32_t nsIOService::gDefaultSegmentSize = 4096; uint32_t nsIOService::gDefaultSegmentCount = 24; bool nsIOService::sIsDataURIUniqueOpaqueOrigin = false; +bool nsIOService::sBlockToplevelDataUriNavigations = false; //////////////////////////////////////////////////////////////////////////////// nsIOService::nsIOService() : mOffline(true) , mOfflineForProfileChange(false) , mManageLinkStatus(false) , mConnectivity(true) @@ -245,16 +246,18 @@ nsIOService::Init() observerService->AddObserver(this, NS_NETWORK_LINK_TOPIC, true); observerService->AddObserver(this, NS_WIDGET_WAKE_OBSERVER_TOPIC, true); } else NS_WARNING("failed to get observer service"); 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; InitializeNetworkLinkService(); SetOffline(false); @@ -1926,10 +1929,16 @@ nsIOService::SpeculativeAnonymousConnect } /*static*/ bool nsIOService::IsDataURIUniqueOpaqueOrigin() { return sIsDataURIUniqueOpaqueOrigin; } +/*static*/ bool +nsIOService::BlockToplevelDataUriNavigations() +{ + return sBlockToplevelDataUriNavigations; +} + } // namespace net } // namespace mozilla
--- a/netwerk/base/nsIOService.h +++ b/netwerk/base/nsIOService.h @@ -91,16 +91,17 @@ public: // caused problems (bug 1242755) so we doing it in this way. // As soon as nsIOService gets notification that it is shutdown it is going to // reset mHttpHandlerAlreadyShutingDown. void SetHttpHandlerAlreadyShutingDown(); bool IsLinkUp(); static bool IsDataURIUniqueOpaqueOrigin(); + static bool BlockToplevelDataUriNavigations(); // Used to count the total number of HTTP requests made void IncrementRequestNumber() { mTotalRequests++; } uint32_t GetTotalRequestNumber() { return mTotalRequests; } // Used to keep "race cache with network" stats void IncrementCacheWonRequestNumber() { mCacheWon++; } uint32_t GetCacheWonRequestNumber() { return mCacheWon; } void IncrementNetWonRequestNumber() { mNetWon++; } @@ -181,16 +182,17 @@ private: // cached categories nsCategoryCache<nsIChannelEventSink> mChannelEventSinks; nsTArray<int32_t> mRestrictedPortList; bool mNetworkNotifyChanged; static bool sIsDataURIUniqueOpaqueOrigin; + static bool sBlockToplevelDataUriNavigations; uint32_t mTotalRequests; uint32_t mCacheWon; uint32_t mNetWon; // These timestamps are needed for collecting telemetry on PR_Connect, // PR_ConnectContinue and PR_Close blocking time. If we spend very long // time in any of these functions we want to know if and what network
--- a/netwerk/protocol/http/nsHttpChannel.cpp +++ b/netwerk/protocol/http/nsHttpChannel.cpp @@ -525,21 +525,28 @@ nsHttpChannel::Connect() if (!mFallbackChannel && !mFallbackKey.IsEmpty()) { return AsyncCall(&nsHttpChannel::HandleAsyncFallback); } return NS_ERROR_DOCUMENT_NOT_CACHED; } // 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); } return TriggerNetwork(0); } nsresult nsHttpChannel::TryHSTSPriming() @@ -2397,16 +2404,17 @@ nsHttpChannel::ContinueProcessResponse2( return NS_OK; } rv = NS_OK; 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 // nsHttpResponseHead::MustValidate; if you change this switch, update that // one switch (httpStatus) { case 200: case 203: @@ -2420,19 +2428,22 @@ nsHttpChannel::ContinueProcessResponse2( rv = CallOnStartRequest(); break; } // these can normally be cached rv = ProcessNormal(); 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(); } break; case 300: case 301: case 302: case 307: @@ -2540,16 +2551,26 @@ nsHttpChannel::ContinueProcessResponse2( } break; default: rv = ProcessNormal(); MaybeInvalidateCacheEntryForSubsequentGet(); 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) { cacheDisposition = kCacheMissed; } else if (successfulReval) { cacheDisposition = kCacheHitViaReval; } else { cacheDisposition = kCacheMissedViaReval; @@ -4525,17 +4546,24 @@ nsHttpChannel::OnCacheEntryAvailableInte return rv; } // We may be waiting for more callbacks... if (AwaitingCacheCallbacks()) { 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); } return TriggerNetwork(0); } nsresult nsHttpChannel::OnNormalCacheEntryAvailable(nsICacheEntry *aEntry,
--- a/parser/htmlparser/tests/crashtests/445171-1-inner.svg +++ b/parser/htmlparser/tests/crashtests/445171-1-inner.svg @@ -1,5 +1,5 @@ -<svg xmlns="http://www.w3.org/2000/svg" onload="location = 'data:text/html,<script>parent.done()</script>';"> +<svg xmlns="http://www.w3.org/2000/svg" onload="location = 'file_445171-1.html'"> <rect x="5" y="5" width="50" height="50" /> </svg>
new file mode 100644 --- /dev/null +++ b/parser/htmlparser/tests/crashtests/file_445171-1.html @@ -0,0 +1,1 @@ +<script>parent.done()</script>
--- a/security/manager/ssl/nsNSSComponent.cpp +++ b/security/manager/ssl/nsNSSComponent.cpp @@ -1710,33 +1710,34 @@ InitializeNSSWithFallbacks(const nsACStr { if (nocertdb || profilePath.IsEmpty()) { MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("nocertdb mode or empty profile path -> NSS_NoDB_Init")); SECStatus srv = NSS_NoDB_Init(nullptr); return srv == SECSuccess ? NS_OK : NS_ERROR_FAILURE; } - const char* profilePathCStr = PromiseFlatCString(profilePath).get(); + + const nsCString& profilePathCStr = PromiseFlatCString(profilePath); // Try read/write mode. If we're in safeMode, we won't load PKCS#11 modules. #ifndef ANDROID PRErrorCode savedPRErrorCode1; #endif // ifndef ANDROID - SECStatus srv = ::mozilla::psm::InitializeNSS(profilePathCStr, false, + SECStatus srv = ::mozilla::psm::InitializeNSS(profilePathCStr.get(), false, !safeMode); if (srv == SECSuccess) { MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("initialized NSS in r/w mode")); return NS_OK; } #ifndef ANDROID savedPRErrorCode1 = PR_GetError(); PRErrorCode savedPRErrorCode2; #endif // ifndef ANDROID // That failed. Try read-only mode. - srv = ::mozilla::psm::InitializeNSS(profilePathCStr, true, !safeMode); + srv = ::mozilla::psm::InitializeNSS(profilePathCStr.get(), true, !safeMode); if (srv == SECSuccess) { MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("initialized NSS in r-o mode")); return NS_OK; } #ifndef ANDROID savedPRErrorCode2 = PR_GetError(); #endif // ifndef ANDROID @@ -1747,38 +1748,38 @@ InitializeNSSWithFallbacks(const nsACStr if (!safeMode && (savedPRErrorCode1 == SEC_ERROR_LEGACY_DATABASE || savedPRErrorCode2 == SEC_ERROR_LEGACY_DATABASE)) { MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("attempting no-module db init")); // It would make sense to initialize NSS in read-only mode here since this // is just a test to see if the PKCS#11 module DB being in FIPS mode is the // problem, but for some reason the combination of read-only and no-moddb // flags causes NSS initialization to fail, so unfortunately we have to use // read-write mode. - srv = ::mozilla::psm::InitializeNSS(profilePathCStr, false, false); + srv = ::mozilla::psm::InitializeNSS(profilePathCStr.get(), false, false); if (srv == SECSuccess) { MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("FIPS may be the problem")); // Unload NSS so we can attempt to fix this situation for the user. srv = NSS_Shutdown(); if (srv != SECSuccess) { return NS_ERROR_FAILURE; } MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("trying to rename module db")); // If this fails non-catastrophically, we'll attempt to initialize NSS // again in r/w then r-o mode (both of which will fail), and then we'll // fall back to NSS_NoDB_Init, which is the behavior we want. nsresult rv = AttemptToRenamePKCS11ModuleDB(profilePath); if (NS_FAILED(rv)) { return rv; } - srv = ::mozilla::psm::InitializeNSS(profilePathCStr, false, true); + srv = ::mozilla::psm::InitializeNSS(profilePathCStr.get(), false, true); if (srv == SECSuccess) { MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("initialized in r/w mode")); return NS_OK; } - srv = ::mozilla::psm::InitializeNSS(profilePathCStr, true, true); + srv = ::mozilla::psm::InitializeNSS(profilePathCStr.get(), true, true); if (srv == SECSuccess) { MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("initialized in r-o mode")); return NS_OK; } } } #endif
--- a/security/sandbox/linux/Sandbox.cpp +++ b/security/sandbox/linux/Sandbox.cpp @@ -680,26 +680,28 @@ SandboxEarlyInit(GeckoProcessType aType) #ifdef MOZ_CONTENT_SANDBOX /** * Starts the seccomp sandbox for a content process. Should be called * only once, and before any potentially harmful content is loaded. * * Will normally make the process exit on failure. */ bool -SetContentProcessSandbox(int aBrokerFd, std::vector<int>& aSyscallWhitelist) +SetContentProcessSandbox(int aBrokerFd, bool aFileProcess, + std::vector<int>& aSyscallWhitelist) { if (!SandboxInfo::Get().Test(SandboxInfo::kEnabledForContent)) { if (aBrokerFd >= 0) { close(aBrokerFd); } 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<SandboxBrokerClient> sBroker; if (aBrokerFd >= 0) { sBroker.emplace(aBrokerFd); } SetCurrentProcessSandbox(GetContentSandboxPolicy(sBroker.ptrOr(nullptr),
--- a/security/sandbox/linux/Sandbox.h +++ b/security/sandbox/linux/Sandbox.h @@ -20,17 +20,19 @@ namespace mozilla { // This must be called early, while the process is still single-threaded. MOZ_EXPORT void SandboxEarlyInit(GeckoProcessType aType); #ifdef MOZ_CONTENT_SANDBOX // Call only if SandboxInfo::CanSandboxContent() returns true. // (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<int>& aSyscallWhitelist); #endif #ifdef MOZ_GMP_SANDBOX // Call only if SandboxInfo::CanSandboxMedia() returns true. // (No-op if MOZ_DISABLE_GMP_SANDBOX is set.) // aFilePath is the path to the plugin file. MOZ_EXPORT void SetMediaPluginSandbox(const char *aFilePath);
--- a/security/sandbox/linux/SandboxBrokerClient.cpp +++ b/security/sandbox/linux/SandboxBrokerClient.cpp @@ -138,17 +138,17 @@ SandboxBrokerClient::DoCall(const Reques } return resp.mError; } if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) { // Keep in mind that "rejected" files can include ones that don't // 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) { close(openedFd); } return resp.mError; }
--- a/security/sandbox/linux/broker/SandboxBroker.cpp +++ b/security/sandbox/linux/broker/SandboxBroker.cpp @@ -16,16 +16,17 @@ #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> #ifdef XP_LINUX #include <sys/prctl.h> #endif +#include "base/string_util.h" #include "mozilla/Assertions.h" #include "mozilla/DebugOnly.h" #include "mozilla/Move.h" #include "mozilla/NullPtr.h" #include "mozilla/Sprintf.h" #include "mozilla/ipc/FileDescriptor.h" #include "sandbox/linux/system_headers/linux_syscalls.h" @@ -195,24 +196,30 @@ SandboxBroker::Policy::AddDir(int aPerms if (stat(aPath, &statBuf) != 0) { return; } if (!S_ISDIR(statBuf.st_mode)) { 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); - 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 SandboxBroker::Policy::AddPrefix(int aPerms, const char* aPath) { Policy::AddPrefixInternal(aPerms, nsDependentCString(aPath)); } @@ -418,27 +425,84 @@ DoLink(const char* aPath, const char* aP } size_t 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); } // ValidatePath will handle failure to translate } 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) { char threadName[16]; SprintfLiteral(threadName, "FS Broker %d", mChildPid); PlatformThread::SetName(threadName); // Permissive mode can only be enabled through an environment variable, @@ -447,18 +511,18 @@ SandboxBroker::ThreadMain(void) bool permissive = SandboxInfo::Get().Test(SandboxInfo::kPermissive); while (true) { struct iovec ios[2]; // We will receive the path strings in 1 buffer and split them back up. 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; int respfd; // Make sure stat responses fit in the response buffer MOZ_ASSERT((kMaxPathLen + 1) > sizeof(struct stat)); @@ -533,16 +597,28 @@ SandboxBroker::ThreadMain(void) // First string is guaranteed to be 0-terminated. pathLen = first_len; // Look up the first pathname but first translate relative paths. 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) { // Force 0 termination. pathBuf2[pathLen2] = '\0'; pathLen2 = ConvertToRealPath(pathBuf2, sizeof(pathBuf2), pathLen2); int perms2 = mPolicy->Lookup(nsDependentCString(pathBuf2, pathLen2)); @@ -693,16 +769,44 @@ SandboxBroker::ThreadMain(void) AuditDenial(req.mOp, req.mFlags, perms, pathBuf); } break; case SANDBOX_FILE_READLINK: 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; } else { resp.mError = -errno; } } else { AuditDenial(req.mOp, req.mFlags, perms, pathBuf); @@ -742,16 +846,16 @@ SandboxBroker::AuditPermissive(int aOp, " permissive=1 error=\"%s\"", aOp, aFlags, aPerms, aPath, mChildPid, strerror(errno)); } 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); } } } // namespace mozilla
--- a/security/sandbox/linux/broker/SandboxBroker.h +++ b/security/sandbox/linux/broker/SandboxBroker.h @@ -51,20 +51,20 @@ class SandboxBroker final // (This overrides all other flags.) CRASH_INSTEAD = 1 << 4, // Applies to everything below this path, including subdirs created // at runtime RECURSIVE = 1 << 5, }; // 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<nsCStringHashKey, int> PathMap; + typedef nsDataHashtable<nsCStringHashKey, int> PathPermissionMap; class Policy { - PathMap mMap; + PathPermissionMap mMap; public: Policy(); Policy(const Policy& aOther); ~Policy(); enum AddCondition { AddIfExistsNow, AddAlways, @@ -115,23 +115,32 @@ class SandboxBroker final virtual ~SandboxBroker(); private: PlatformThreadHandle mThread; int mFileDesc; const int mChildPid; const UniquePtr<const Policy> mPolicy; + typedef nsDataHashtable<nsCStringHashKey, nsCString> PathMap; + PathMap mSymlinkMap; + SandboxBroker(UniquePtr<const Policy> aPolicy, int aChildPid, int& aClientFd); void ThreadMain(void) override; void AuditPermissive(int aOp, int aFlags, int aPerms, const char* aPath); 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; void operator=(const SandboxBroker&) = delete; }; } // namespace mozilla
--- a/security/sandbox/linux/broker/SandboxBrokerCommon.cpp +++ b/security/sandbox/linux/broker/SandboxBrokerCommon.cpp @@ -25,16 +25,30 @@ // In the future, if the broker becomes a dedicated executable, this // can change. #error "No MSG_CMSG_CLOEXEC?" #endif // XP_LINUX #endif // MSG_CMSG_CLOEXEC 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) { struct msghdr msg = {}; msg.msg_iov = const_cast<iovec*>(aIO); msg.msg_iovlen = aNumIO;
--- a/security/sandbox/linux/broker/SandboxBrokerCommon.h +++ b/security/sandbox/linux/broker/SandboxBrokerCommon.h @@ -33,16 +33,18 @@ public: SANDBOX_FILE_LINK, SANDBOX_FILE_SYMLINK, SANDBOX_FILE_MKDIR, SANDBOX_FILE_RENAME, SANDBOX_FILE_RMDIR, SANDBOX_FILE_UNLINK, SANDBOX_FILE_READLINK, }; + // String versions of the above + static const char* OperationDescription[]; struct Request { Operation mOp; // For open, flags; for access, "mode"; for stat, O_NOFOLLOW for lstat. int mFlags; // Size of return value buffer, if any size_t mBufSize; // The rest of the packet is the pathname.
--- a/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp +++ b/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp @@ -6,20 +6,23 @@ #include "SandboxBrokerPolicyFactory.h" #include "SandboxInfo.h" #include "SandboxLogging.h" #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 #include "cutils/properties.h" #endif #ifdef MOZ_WIDGET_GTK #include <glib.h> @@ -37,24 +40,26 @@ static const int rdwrcr = rdwr | Sandbox #endif SandboxBrokerPolicyFactory::SandboxBrokerPolicyFactory() { // Policy entries that are the same in every process go here, and // 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<nsIFile> tmpDir; nsresult rv = GetSpecialSystemDirectory(OS_TemporaryDirectory, getter_AddRefs(tmpDir)); + if (NS_SUCCEEDED(rv)) { nsAutoCString tmpPath; rv = tmpDir->GetNativePath(tmpPath); if (NS_SUCCEEDED(rv)) { policy->AddDir(rdwrcr, tmpPath.get()); } } // If the above fails at any point, fall back to a very good guess. @@ -77,48 +82,188 @@ SandboxBrokerPolicyFactory::SandboxBroke // Bug 1321134: DConf's single bit of shared memory if (const auto userDir = g_get_user_runtime_dir()) { // The leaf filename is "user" by default, but is configurable. nsPrintfCString shmPath("%s/dconf/", userDir); policy->AddPrefix(rdwrcr, shmPath.get()); } #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<const char*, 3> confDirs = { + ".config", + ".themes", + ".fonts", + }; + + nsCOMPtr<nsIFile> homeDir; + rv = GetSpecialSystemDirectory(Unix_HomeDirectory, getter_AddRefs(homeDir)); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIFile> 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"));